Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add find and replace for passport variable via cli prompts #9

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:passportValue": "node passportValue/index.js",
"precommit": "prettier --write **/*.js && git add ."
},
"keywords": [],
Expand Down
22 changes: 22 additions & 0 deletions passportValue/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry - still not personally convinced this regex that protects against partial matches is better approach than plain string replaceAll() and I think is going to be ultimately limiting in use of this against "real" content examples!

In your example alterchange (but not alterationchangeation) that this regex protects against, I think you're overlooking the option to simply pass alter. as the find variable then replaceAll() as expected !!

In recent file naming, there were other examples of partial matches like councilTaxBill (find = councilTax) → councilBill (replace = council) that wouldn't be supported by this regex.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm maybe being too risk averse with this approach. As far as I understand the regex would approach would still allow all renaming but might require more runs as matches would need to be made explicitly councilTaxBill → councilBill vs the freedom / risk of partial match renames.

Passing in variables with . as in alter. is definitely something I hadn't considered and does overcome the issue I'd outlined although I guess it does require protecting against edge cases at the time of running the code and a deeper familiarity with the project types versus having that edge case protection in the code?

Although, I'm happy to defer to your judgement though as you're much more familiar with this process and the data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated here: f073806

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks ! My theory on these is: always start quick & flexible based on known content examples, and introduce strictness as future use cases necessitate. Because since these scripts are only manually run in very controlled scenarios right now (eg not exposed to editors), we don't need to get hung up anticipating uses beyond the task at hand!

}

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}`]
newFlowData[nodeId]["data"][`${passportKey}`] = replaceAllOccurrences(currentFn, currentPassportVariable, newPassportVariable)
})
return newFlowData;
}

module.exports = { updateNodeFn };
95 changes: 95 additions & 0 deletions passportValue/index.js
Original file line number Diff line number Diff line change
@@ -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`));
}
})();
22 changes: 22 additions & 0 deletions passportValue/prompts.js
Original file line number Diff line number Diff line change
@@ -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 };
Loading