Metadata Migrations script (#453)

* Create a package

* wip

* wip

* wip

* wip

* Move migration script to invoices

* Add migration script

* Update SDK to fix getAll method

* Add restoring migration script

* Add migrations consts

* Add changesets

* cr fixes
This commit is contained in:
Lukasz Ostrowski 2023-05-18 14:55:52 +02:00 committed by GitHub
parent f76d8fe8da
commit 5e903aed00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 235 additions and 1 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-invoices": patch
---
Update app-sdk to 0.38.0

View file

@ -0,0 +1,40 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { createClient } from "../../src/lib/graphql";
import { createSettingsManager } from "../../src/modules/app-configuration/metadata-manager";
import { SaleorCloudAPL } from "@saleor/app-sdk/APL";
export const getMetadataManagerForEnv = (apiUrl: string, appToken: string) => {
const client = createClient(apiUrl, async () => ({
token: appToken,
}));
return createSettingsManager(client);
};
export const safeParse = (json?: string) => {
if (!json) return null;
try {
return JSON.parse(json);
} catch (e) {
return null;
}
};
export const verifyRequiredEnvs = () => {
const requiredEnvs = ["SALEOR_CLOUD_TOKEN", "SALEOR_CLOUD_RESOURCE_URL", "SECRET_KEY"];
if (!requiredEnvs.every((env) => process.env[env])) {
throw new Error(`Missing envs: ${requiredEnvs.join(" | ")}`);
}
};
export const fetchCloudAplEnvs = () => {
const saleorAPL = new SaleorCloudAPL({
token: process.env.SALEOR_CLOUD_TOKEN!,
resourceUrl: process.env.SALEOR_CLOUD_RESOURCE_URL!,
});
return saleorAPL.getAll();
};

View file

@ -0,0 +1,4 @@
export const MigrationV1toV2Consts = {
appConfigV2metadataKey: "app-config-v2",
appConfigV1metadataKey: "app-config",
};

View file

@ -0,0 +1,3 @@
Run `npx tsx run-report.ts` to print report (dry-run)
Run `npx tsx run-migration.ts` to migrate
Run `npx tsx restore-migration.ts` to restore migration (remove metadata v2)

View file

@ -0,0 +1,45 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "../migration-utils";
import { createClient } from "../../../src/lib/graphql";
import { RemoveMetadataDocument } from "../../../generated/graphql";
import { MigrationV1toV2Consts } from "./const";
dotenv.config();
const runMigration = async () => {
verifyRequiredEnvs();
const allEnvs = await fetchCloudAplEnvs();
const results = await Promise.all(
allEnvs.map((env) => {
const client = createClient(env.saleorApiUrl, async () => ({
token: env.token,
}));
return client
.mutation(RemoveMetadataDocument, {
id: env.appId,
keys: [MigrationV1toV2Consts.appConfigV2metadataKey],
})
.toPromise()
.then((r) => {
if (r.error) {
console.error("❌ Error removing metadata", r.error.message);
throw r.error.message;
}
return r;
})
.catch((e) => {
console.error("❌ Error removing metadata", e);
});
})
);
console.log(results);
};
runMigration();

View file

@ -0,0 +1,67 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import {
fetchCloudAplEnvs,
getMetadataManagerForEnv,
safeParse,
verifyRequiredEnvs,
} from "../migration-utils";
import { ConfigV1ToV2Transformer } from "../../../src/modules/app-configuration/schema-v2/config-v1-to-v2-transformer";
import { AppConfigV2MetadataManager } from "../../../src/modules/app-configuration/schema-v2/app-config-v2-metadata-manager";
import { AppConfigV2 } from "../../../src/modules/app-configuration/schema-v2/app-config";
import { MigrationV1toV2Consts } from "./const";
dotenv.config();
const runMigration = async () => {
verifyRequiredEnvs();
const allEnvs = await fetchCloudAplEnvs();
const results = await Promise.all(
allEnvs.map((env) => {
const metadataManager = getMetadataManagerForEnv(env.saleorApiUrl, env.token);
return Promise.all([
metadataManager.get(MigrationV1toV2Consts.appConfigV1metadataKey, env.saleorApiUrl),
metadataManager.get(MigrationV1toV2Consts.appConfigV2metadataKey),
])
.then(([v1, v2]) => {
if (v2 && v2 !== "undefined") {
console.log("▶️ v2 already exists for ", env.saleorApiUrl);
return;
}
if (!v1) {
console.log("🚫 v1 does not exist for ", env.saleorApiUrl);
return new AppConfigV2MetadataManager(metadataManager)
.set(new AppConfigV2().serialize())
.then((r) => {
console.log(`✅ created empty config for ${env.saleorApiUrl}`);
})
.catch((e) => {
console.log(
`🚫 failed to create empty config for ${env.saleorApiUrl}. Env may not exist.`,
e.message
);
});
}
const v2Config = new ConfigV1ToV2Transformer().transform(JSON.parse(v1));
return new AppConfigV2MetadataManager(metadataManager)
.set(v2Config.serialize())
.then((r) => {
console.log(`✅ migrated ${env.saleorApiUrl}`);
});
})
.catch((e) => {
console.error("🚫 Failed to migrate ", env.saleorApiUrl, e);
});
})
);
};
runMigration();

View file

@ -0,0 +1,61 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import {
fetchCloudAplEnvs,
getMetadataManagerForEnv,
safeParse,
verifyRequiredEnvs,
} from "../migration-utils";
import { MigrationV1toV2Consts } from "./const";
dotenv.config();
const runReport = async () => {
verifyRequiredEnvs();
const allEnvs = await fetchCloudAplEnvs().catch((r) => {
console.error(r);
process.exit(1);
});
const results = await Promise.all(
allEnvs.map((env) => {
const metadataManager = getMetadataManagerForEnv(env.saleorApiUrl, env.token);
return Promise.all([
metadataManager.get(MigrationV1toV2Consts.appConfigV1metadataKey, env.saleorApiUrl),
metadataManager.get(MigrationV1toV2Consts.appConfigV2metadataKey),
])
.then(([v1, v2]) => {
return {
schemaV1: safeParse(v1),
schemaV2: safeParse(v2),
};
})
.then((metadata) => ({
metadata: metadata,
env: env.saleorApiUrl,
}));
})
);
const report = results.map((r: any) => ({
env: r.env,
hasV1: !!r.metadata.schemaV1,
hasV2: !!r.metadata.schemaV2,
}));
const notMigratedCount = report.reduce((acc: number, curr: any) => {
if (!curr.hasV2) {
return acc + 1;
}
return acc;
}, 0);
console.table(report);
console.log(`Envs left to migrate: ${notMigratedCount}`);
};
runReport();

View file

@ -35,6 +35,16 @@ gql`
} }
`; `;
gql`
mutation RemoveMetadata($id: ID!, $keys: [String!]!) {
deletePrivateMetadata(id: $id, keys: $keys) {
errors {
message
}
}
}
`;
export type SimpleGraphqlClient = Pick<Client, "mutation" | "query">; export type SimpleGraphqlClient = Pick<Client, "mutation" | "query">;
export async function fetchAllMetadata(client: SimpleGraphqlClient): Promise<MetadataEntry[]> { export async function fetchAllMetadata(client: SimpleGraphqlClient): Promise<MetadataEntry[]> {

View file

@ -5536,7 +5536,6 @@ packages:
uuid: 8.3.2 uuid: 8.3.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: false
/@saleor/app-sdk@0.38.0(next@13.3.0)(react-dom@18.2.0)(react@18.2.0): /@saleor/app-sdk@0.38.0(next@13.3.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-JS/E3YODFHc+1DI+PczbV8jB09nLwzdQcwNs681RlwvR3JUC892hdBYYRdBKG5lauAcr4IxKw1IbrsxJKngtWA==} resolution: {integrity: sha512-JS/E3YODFHc+1DI+PczbV8jB09nLwzdQcwNs681RlwvR3JUC892hdBYYRdBKG5lauAcr4IxKw1IbrsxJKngtWA==}