diff --git a/.changeset/real-onions-move.md b/.changeset/real-onions-move.md new file mode 100644 index 0000000..f348915 --- /dev/null +++ b/.changeset/real-onions-move.md @@ -0,0 +1,5 @@ +--- +"saleor-app-invoices": patch +--- + +Update app-sdk to 0.38.0 diff --git a/apps/invoices/scripts/migrations/migration-utils.ts b/apps/invoices/scripts/migrations/migration-utils.ts new file mode 100644 index 0000000..adfbd65 --- /dev/null +++ b/apps/invoices/scripts/migrations/migration-utils.ts @@ -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(); +}; diff --git a/apps/invoices/scripts/migrations/v1-to-v2/const.ts b/apps/invoices/scripts/migrations/v1-to-v2/const.ts new file mode 100644 index 0000000..6488ecf --- /dev/null +++ b/apps/invoices/scripts/migrations/v1-to-v2/const.ts @@ -0,0 +1,4 @@ +export const MigrationV1toV2Consts = { + appConfigV2metadataKey: "app-config-v2", + appConfigV1metadataKey: "app-config", +}; diff --git a/apps/invoices/scripts/migrations/v1-to-v2/readme.MD b/apps/invoices/scripts/migrations/v1-to-v2/readme.MD new file mode 100644 index 0000000..8bc8029 --- /dev/null +++ b/apps/invoices/scripts/migrations/v1-to-v2/readme.MD @@ -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) \ No newline at end of file diff --git a/apps/invoices/scripts/migrations/v1-to-v2/restore-migration.ts b/apps/invoices/scripts/migrations/v1-to-v2/restore-migration.ts new file mode 100644 index 0000000..7528d14 --- /dev/null +++ b/apps/invoices/scripts/migrations/v1-to-v2/restore-migration.ts @@ -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(); diff --git a/apps/invoices/scripts/migrations/v1-to-v2/run-migration.ts b/apps/invoices/scripts/migrations/v1-to-v2/run-migration.ts new file mode 100644 index 0000000..350f194 --- /dev/null +++ b/apps/invoices/scripts/migrations/v1-to-v2/run-migration.ts @@ -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(); diff --git a/apps/invoices/scripts/migrations/v1-to-v2/run-report.ts b/apps/invoices/scripts/migrations/v1-to-v2/run-report.ts new file mode 100644 index 0000000..0514084 --- /dev/null +++ b/apps/invoices/scripts/migrations/v1-to-v2/run-report.ts @@ -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(); diff --git a/apps/invoices/src/modules/app-configuration/metadata-manager.ts b/apps/invoices/src/modules/app-configuration/metadata-manager.ts index 2ef2f99..db761e0 100644 --- a/apps/invoices/src/modules/app-configuration/metadata-manager.ts +++ b/apps/invoices/src/modules/app-configuration/metadata-manager.ts @@ -35,6 +35,16 @@ gql` } `; +gql` + mutation RemoveMetadata($id: ID!, $keys: [String!]!) { + deletePrivateMetadata(id: $id, keys: $keys) { + errors { + message + } + } + } +`; + export type SimpleGraphqlClient = Pick; export async function fetchAllMetadata(client: SimpleGraphqlClient): Promise { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22720c0..c38bf04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5536,7 +5536,6 @@ packages: uuid: 8.3.2 transitivePeerDependencies: - supports-color - dev: false /@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==}