From 15865be34a0991ae62e439a6e0a914a055991231 Mon Sep 17 00:00:00 2001 From: Mike Heneghan Date: Wed, 14 Feb 2024 17:28:21 +0000 Subject: [PATCH 1/5] feat: add find and replace for passport variable via cli prompts --- findAndReplacePassportValue/helpers.js | 23 +++++++ findAndReplacePassportValue/index.js | 95 ++++++++++++++++++++++++++ findAndReplacePassportValue/prompts.js | 22 ++++++ 3 files changed, 140 insertions(+) create mode 100644 findAndReplacePassportValue/helpers.js create mode 100644 findAndReplacePassportValue/index.js create mode 100644 findAndReplacePassportValue/prompts.js diff --git a/findAndReplacePassportValue/helpers.js b/findAndReplacePassportValue/helpers.js new file mode 100644 index 0000000..62a4cc9 --- /dev/null +++ b/findAndReplacePassportValue/helpers.js @@ -0,0 +1,23 @@ +/** + * Find a current passport variable (fn) or (val) and replace it in each node in a flow (live or published + */ + +const updateNodeFn = (flowData, currentPassportVariable, newPassportVariable) => { + let newFlowData = flowData; + Object.entries(flowData) + .filter(([_nodeId, nodeData]) => nodeData?.["data"]?.["fn"] || nodeData?.["data"]?.["val"]) + .forEach(([nodeId, nodeData]) => { + const passportKey = nodeData["data"]["fn"] ? "fn" : "val" + const currentFn = nodeData["data"][`${passportKey}`] + const currentValueSegments = currentFn.split(".") + const matchIndexPosition = currentValueSegments.indexOf(currentPassportVariable) + if (matchIndexPosition !==-1) { + currentValueSegments.splice(matchIndexPosition, 1, newPassportVariable) + const updatedPassportVariable = currentValueSegments.join(".") + newFlowData[nodeId]["data"][`${passportKey}`] = updatedPassportVariable + } + }) + return newFlowData; +} + +module.exports = { updateNodeFn }; diff --git a/findAndReplacePassportValue/index.js b/findAndReplacePassportValue/index.js new file mode 100644 index 0000000..18bd2b0 --- /dev/null +++ b/findAndReplacePassportValue/index.js @@ -0,0 +1,95 @@ +const ask = require("prompt"); +const chalk = require("chalk"); +const Client = require("../client"); + +const { findAndReplacePrompts } = require("./prompts"); +const { updateNodeFn } = require("./helpers"); + +ask.start(); + +(async function go() { + // greeting + console.log( + chalk.cyan( + `Hello! These prompts will step you through updating a PlanX passport variables.\nType values when prompted or click 'enter' to accept ${chalk.white( + "(default)" + )} values.\n~~~~~~~~~~~~~~ ${chalk.bold("LET'S START")} ~~~~~~~~~~~~~~` + ) + ); + + // authentication & setup + const { hasuraEnvironment, hasuraSecret, flowSlug, currentPassportVariable, newPassportVariable } = await ask.get( + findAndReplacePrompts + ); + const url = { + production: "https://hasura.editor.planx.uk/v1/graphql", + staging: "https://hasura.editor.planx.dev/v1/graphql", + local: "http://localhost:7100/v1/graphql", + }; + + // create graphQL client + const client = new Client({ + hasuraSecret, + targetURL: url[hasuraEnvironment], + }); + + const formattedSlug = flowSlug.toLowerCase().trim().replaceAll(" ", "-"); + + // Fetch flows matching slugs + const flows = await client.getFlowData(formattedSlug); + if (flows?.length > 0) { + console.log(chalk.white(`Fetched ${flows.length} flows`)); + + flows.forEach(async (flow, i) => { + let liveFlowData; + let publishedFlowData; + let response; + + try { + if (flow.publishedFlows.length > 0) { + // Proceed with migration for flows that are published + console.log(chalk.white(`Updating published flow ${i+1}/${flows.length}: ${flow.team.slug}/${flow.slug}`)); + + // Find nodes in live flow data, update them + // This does NOT require a corresponding operation because we are not creating the flow for the first time + liveFlowData = updateNodeFn(flow.data, currentPassportVariable, newPassportVariable); + + // Find nodes in published flow data, update them directly too + publishedFlowData = updateNodeFn(flow.publishedFlows?.[0]?.data, currentPassportVariable, newPassportVariable); + + // Write update in a single mutation block for postgres transaction-like rollback behavior on error + response = await client.updateFlowAndPublishedFlow(flow.id, liveFlowData, flow.publishedFlows?.[0]?.id, publishedFlowData); + if (response?.update_flows_by_pk?.id) { + console.log( + chalk.green(`Successfully updated flow: ${flow.team.slug}/${flow.slug}`) + ); + } + if (response?.update_published_flows_by_pk?.id) { + console.log( + chalk.green(`Successfully updated published version of flow: ${flow.team.slug}/${flow.slug}`) + ); + } + } else { + // Proceed with migration for flows that are not published + console.log(chalk.white(`Updating unpublished flow ${i+1}/${flows.length}: ${flow.team.slug}/${flow.slug}`)); + + // Find nodes in live flow data, update them + // This does NOT require a corresponding operation because we are not creating the flow for the first time + liveFlowData = updateNodeFn(flow.data, currentPassportVariable, newPassportVariable); + + // Write update + response = await client.updateFlow(flow.id, liveFlowData); + if (response?.update_flows_by_pk?.id) { + console.log( + chalk.green(`Successfully updated flow: ${flow.team.slug}/${flow.slug}`) + ); + } + } + } catch (error) { + console.log(chalk.red(error)); + } + }); + } else { + console.log(chalk.red(`Cannot find any flows matching slug: ${formattedSlug}. Exiting migration script`)); + } +})(); diff --git a/findAndReplacePassportValue/prompts.js b/findAndReplacePassportValue/prompts.js new file mode 100644 index 0000000..0d98f4e --- /dev/null +++ b/findAndReplacePassportValue/prompts.js @@ -0,0 +1,22 @@ +const { setupPrompts } = require("../prompts"); + +const findAndReplacePrompts= [ + ...setupPrompts, + { + name: "currentPassportVariable", + description: "What is the current passport variable to be replaced?", + default: "outbuildings", + type: "string", + required: true, + }, + { + name: "newPassportVariable", + description: "What should this passport variable be replaced with?", + default: "outbuilding", + type: "string", + required: true, + }, + +]; + +module.exports = { findAndReplacePrompts }; From 769d6748064229be47ee89fb642d8aa164829592 Mon Sep 17 00:00:00 2001 From: Mike Heneghan Date: Fri, 16 Feb 2024 16:55:15 +0000 Subject: [PATCH 2/5] chore: add missing start script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 4316c67..7485643 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "test": "echo \"Error: no test specified\" && exit 1", "start:fileNames": "node fileNames/index.js", "start:titleBoundary": "node titleBoundary/index.js", + "start:findAndReplacePassportValue": "node findAndReplacePassportValue/index.js", "precommit": "prettier --write **/*.js && git add ." }, "keywords": [], From 4bd01282d2359b91010b784c76374f7d879f32ff Mon Sep 17 00:00:00 2001 From: Mike Heneghan Date: Mon, 19 Feb 2024 12:08:50 +0000 Subject: [PATCH 3/5] fix: update replace to match all occurrences and update values rather than segments --- findAndReplacePassportValue/helpers.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/findAndReplacePassportValue/helpers.js b/findAndReplacePassportValue/helpers.js index 62a4cc9..ce89ce2 100644 --- a/findAndReplacePassportValue/helpers.js +++ b/findAndReplacePassportValue/helpers.js @@ -2,6 +2,11 @@ * Find a current passport variable (fn) or (val) and replace it in each node in a flow (live or published */ +function replaceAllOccurrences(fullPassportValue, currentPassportVariable, newPassportVariable) { + const regex = new RegExp('\\b' + currentPassportVariable + '\\b', 'g'); + return fullPassportValue.replace(regex, newPassportVariable); +} + const updateNodeFn = (flowData, currentPassportVariable, newPassportVariable) => { let newFlowData = flowData; Object.entries(flowData) @@ -9,13 +14,7 @@ const updateNodeFn = (flowData, currentPassportVariable, newPassportVariable) => .forEach(([nodeId, nodeData]) => { const passportKey = nodeData["data"]["fn"] ? "fn" : "val" const currentFn = nodeData["data"][`${passportKey}`] - const currentValueSegments = currentFn.split(".") - const matchIndexPosition = currentValueSegments.indexOf(currentPassportVariable) - if (matchIndexPosition !==-1) { - currentValueSegments.splice(matchIndexPosition, 1, newPassportVariable) - const updatedPassportVariable = currentValueSegments.join(".") - newFlowData[nodeId]["data"][`${passportKey}`] = updatedPassportVariable - } + newFlowData[nodeId]["data"][`${passportKey}`] = replaceAllOccurrences(currentFn, currentPassportVariable, newPassportVariable) }) return newFlowData; } From 7fcb89b466ccf012bc60881a855e341a42f59c67 Mon Sep 17 00:00:00 2001 From: Mike Heneghan Date: Mon, 19 Feb 2024 12:28:24 +0000 Subject: [PATCH 4/5] chore: rename subdirectory --- package.json | 2 +- {findAndReplacePassportValue => passportValue}/helpers.js | 0 {findAndReplacePassportValue => passportValue}/index.js | 0 {findAndReplacePassportValue => passportValue}/prompts.js | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename {findAndReplacePassportValue => passportValue}/helpers.js (100%) rename {findAndReplacePassportValue => passportValue}/index.js (100%) rename {findAndReplacePassportValue => passportValue}/prompts.js (100%) diff --git a/package.json b/package.json index 7485643..b3a2647 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test": "echo \"Error: no test specified\" && exit 1", "start:fileNames": "node fileNames/index.js", "start:titleBoundary": "node titleBoundary/index.js", - "start:findAndReplacePassportValue": "node findAndReplacePassportValue/index.js", + "start:passportValue": "node passportValue/index.js", "precommit": "prettier --write **/*.js && git add ." }, "keywords": [], diff --git a/findAndReplacePassportValue/helpers.js b/passportValue/helpers.js similarity index 100% rename from findAndReplacePassportValue/helpers.js rename to passportValue/helpers.js diff --git a/findAndReplacePassportValue/index.js b/passportValue/index.js similarity index 100% rename from findAndReplacePassportValue/index.js rename to passportValue/index.js diff --git a/findAndReplacePassportValue/prompts.js b/passportValue/prompts.js similarity index 100% rename from findAndReplacePassportValue/prompts.js rename to passportValue/prompts.js From f073806941afb401f0575a2a4c8eb8afa649d7df Mon Sep 17 00:00:00 2001 From: Mike Heneghan Date: Mon, 19 Feb 2024 14:33:53 +0000 Subject: [PATCH 5/5] refactor: update from regex replace to replaceAll - As per: https://github.com/theopensystemslab/planx-data-migrations/pull/9#discussion_r1494585500 - Allows more flexibility --- passportValue/helpers.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/passportValue/helpers.js b/passportValue/helpers.js index ce89ce2..12f8f87 100644 --- a/passportValue/helpers.js +++ b/passportValue/helpers.js @@ -2,11 +2,6 @@ * Find a current passport variable (fn) or (val) and replace it in each node in a flow (live or published */ -function replaceAllOccurrences(fullPassportValue, currentPassportVariable, newPassportVariable) { - const regex = new RegExp('\\b' + currentPassportVariable + '\\b', 'g'); - return fullPassportValue.replace(regex, newPassportVariable); -} - const updateNodeFn = (flowData, currentPassportVariable, newPassportVariable) => { let newFlowData = flowData; Object.entries(flowData) @@ -14,7 +9,7 @@ const updateNodeFn = (flowData, currentPassportVariable, newPassportVariable) => .forEach(([nodeId, nodeData]) => { const passportKey = nodeData["data"]["fn"] ? "fn" : "val" const currentFn = nodeData["data"][`${passportKey}`] - newFlowData[nodeId]["data"][`${passportKey}`] = replaceAllOccurrences(currentFn, currentPassportVariable, newPassportVariable) + newFlowData[nodeId]["data"][`${passportKey}`] = currentFn.replaceAll(currentPassportVariable, newPassportVariable) }) return newFlowData; }