📧 Add runtime migrations to schema v2 (#535)

* Add runtime migrations to schema v2

* V2 migration scripts (#536)
This commit is contained in:
Krzysztof Wolski 2023-06-06 11:51:59 +02:00 committed by GitHub
parent 235a7d92b7
commit 86c2b7b10b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1171 additions and 90 deletions

View file

@ -34,6 +34,7 @@
"@urql/exchange-auth": "^1.0.0",
"@vitejs/plugin-react": "4.0.0",
"clsx": "^1.2.1",
"dotenv": "^16.0.3",
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6",
"handlebars": "^4.7.7",

View file

@ -0,0 +1,7 @@
# Metadata migration scripts
To run dry-run (check migration without mutating the data):
`npx tsx scripts/migrations/run-report.ts`
To update date and save it:
`npx tsx scripts/migrations/run-migration.ts`

View file

@ -0,0 +1,30 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { createClient } from "../../src/lib/create-graphql-client";
import { SaleorCloudAPL } from "@saleor/app-sdk/APL";
import { createSettingsManager } from "../../src/lib/metadata-manager";
export const getMetadataManagerForEnv = (apiUrl: string, appToken: string, appId: string) => {
const client = createClient(apiUrl, async () => ({
token: appToken,
}));
return createSettingsManager(client, appId);
};
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,76 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import { fetchCloudAplEnvs, getMetadataManagerForEnv, verifyRequiredEnvs } from "./migration-utils";
import { SendgridPrivateMetadataManager } from "../../src/modules/sendgrid/configuration/sendgrid-metadata-manager";
import { SmtpPrivateMetadataManager } from "../../src/modules/smtp/configuration/smtp-metadata-manager";
dotenv.config();
const runMigration = async () => {
console.log("Starting running migration");
verifyRequiredEnvs();
console.log("Envs verified, fetching envs");
const allEnvs = await fetchCloudAplEnvs().catch((r) => {
console.error(r);
process.exit(1);
});
const report = {
smtp: [] as string[],
sendgrid: [] as string[],
none: [] as string[],
};
for (const env of allEnvs) {
let isSmtpMigrated = false;
let isSendgridMigrated = false;
console.log("Working on env: ", env.saleorApiUrl);
const metadataManager = getMetadataManagerForEnv(env.saleorApiUrl, env.token, env.appId);
const sendgridMetadataManager = new SendgridPrivateMetadataManager(
metadataManager,
env.saleorApiUrl
);
const sendgridUpdatedSchema = await sendgridMetadataManager.getConfig();
if (sendgridUpdatedSchema) {
console.log("Migrated sendgrid configuration found, overriding");
isSendgridMigrated = true;
await sendgridMetadataManager.setConfig(sendgridUpdatedSchema);
}
const smtpMetadataManager = new SmtpPrivateMetadataManager(metadataManager, env.saleorApiUrl);
const smtpUpdatedSchema = await smtpMetadataManager.getConfig();
if (smtpUpdatedSchema) {
console.log("Migrated smtp configuration found, overriding");
isSmtpMigrated = true;
await smtpMetadataManager.setConfig(smtpUpdatedSchema);
}
if (isSendgridMigrated) {
report.sendgrid.push(env.saleorApiUrl);
}
if (isSmtpMigrated) {
report.smtp.push(env.saleorApiUrl);
}
if (!isSmtpMigrated && !isSendgridMigrated) {
report.none.push(env.saleorApiUrl);
}
}
console.log("Report", report);
};
runMigration();

View file

@ -0,0 +1,97 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import { fetchCloudAplEnvs, getMetadataManagerForEnv, verifyRequiredEnvs } from "./migration-utils";
import { AppConfigPrivateMetadataManager } from "../../src/modules/app-configuration/app-config-metadata-manager";
import { SendgridPrivateMetadataManagerV1 } from "../../src/modules/sendgrid/configuration/sendgrid-metadata-manager-v1";
import { MjmlPrivateMetadataManager } from "../../src/modules/smtp/configuration/mjml-metadata-manager";
import { smtpTransformV1toV2 } from "../../src/modules/smtp/configuration/migrations/smtp-transform-v1-to-v2";
import { sendgridTransformV1toV2 } from "../../src/modules/sendgrid/configuration/migrations/sendgrid-transform-v1-to-v2";
dotenv.config();
const runReport = async () => {
console.log("Starting running report");
verifyRequiredEnvs();
console.log("Envs verified, fetching envs");
const allEnvs = await fetchCloudAplEnvs().catch((r) => {
console.error(r);
process.exit(1);
});
const report = {
smtp: [] as string[],
sendgrid: [] as string[],
none: [] as string[],
};
for (const env of allEnvs) {
console.log("Working on env: ", env.saleorApiUrl);
let isSmtpMigrated = false;
let isSendgridMigrated = false;
const metadataManager = getMetadataManagerForEnv(env.saleorApiUrl, env.token, env.appId);
const sendgridMetadataManagerV1 = new SendgridPrivateMetadataManagerV1(
metadataManager,
env.saleorApiUrl
);
const appMetadataManager = new AppConfigPrivateMetadataManager(
metadataManager,
env.saleorApiUrl
);
const appConfiguration = await appMetadataManager.getConfig();
const sendgridConfigurationV1 = await sendgridMetadataManagerV1.getConfig();
if (sendgridConfigurationV1) {
console.log("Found old sendgrid config, migrating");
isSendgridMigrated = true;
const v2 = sendgridTransformV1toV2({
configV1: sendgridConfigurationV1,
appConfigV1: appConfiguration,
});
console.log("Old config", sendgridConfigurationV1);
console.log("New config", v2);
}
const mjmlMetadataManagerV1 = new MjmlPrivateMetadataManager(metadataManager, env.saleorApiUrl);
const mjmlConfiguration = await mjmlMetadataManagerV1.getConfig();
if (mjmlConfiguration) {
console.log("Found old mjml config, migrating");
isSmtpMigrated = true;
const v2 = smtpTransformV1toV2({
configV1: mjmlConfiguration,
appConfigV1: appConfiguration,
});
console.log("Old config", mjmlConfiguration);
console.log("New config", v2);
}
if (isSendgridMigrated) {
report.sendgrid.push(env.saleorApiUrl);
}
if (isSmtpMigrated) {
report.smtp.push(env.saleorApiUrl);
}
if (!isSmtpMigrated && !isSendgridMigrated) {
report.none.push(env.saleorApiUrl);
}
}
console.log("Report", report);
};
runReport();

View file

@ -0,0 +1,32 @@
// TODO: MIGRATION CODE FROM CONFIG VERSION V1. REMOVE THIS FILE AFTER MIGRATION
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { AppConfig } from "./app-config-schema";
export class AppConfigPrivateMetadataManager {
private metadataKey = "app-config";
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
getConfig(): Promise<AppConfig | undefined> {
return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => {
if (!data) {
return data;
}
try {
return JSON.parse(data);
} catch (e) {
throw new Error("Invalid metadata value, cant be parsed");
}
});
}
setConfig(config: AppConfig): Promise<void> {
return this.metadataManager.set({
key: this.metadataKey,
value: JSON.stringify(config),
domain: this.saleorApiUrl,
});
}
}

View file

@ -0,0 +1,13 @@
// TODO: MIGRATION CODE FROM CONFIG VERSION V1. REMOVE THIS FILE AFTER MIGRATION
export interface AppConfigurationPerChannel {
active: boolean;
mjmlConfigurationId?: string;
sendgridConfigurationId?: string;
}
export type AppConfigurationsChannelMap = Record<string, AppConfigurationPerChannel>;
export type AppConfig = {
configurationsPerChannel: AppConfigurationsChannelMap;
};

View file

@ -0,0 +1,100 @@
import { expect, describe, it } from "vitest";
import { getChannelsAssignedToConfigId } from "./get-channels-assigned-to-config-id";
describe("getChannelsAssignedToConfigId", function () {
it("Do not assign to any channel, when theres no app configuration", () => {
const channels = getChannelsAssignedToConfigId("id", "sendgrid", undefined);
expect(channels).toEqual({
channels: [],
mode: "restrict",
override: true,
});
});
it("Do not assign sendgrid configuration to any channel, when app configuration did not assigned it", () => {
const channels = getChannelsAssignedToConfigId("id", "sendgrid", {
configurationsPerChannel: {
"default-channel": {
active: true,
sendgridConfigurationId: "other-id",
mjmlConfigurationId: "id",
},
"other-channel": {
active: true,
mjmlConfigurationId: "id",
},
},
});
expect(channels).toEqual({
channels: [],
mode: "restrict",
override: true,
});
});
it("Assign sendgrid configuration to channel, when app configuration has assigned it", () => {
const channels = getChannelsAssignedToConfigId("id", "sendgrid", {
configurationsPerChannel: {
"default-channel": {
active: true,
sendgridConfigurationId: "id",
},
"other-channel": {
active: true,
sendgridConfigurationId: "id",
},
},
});
expect(channels).toEqual({
channels: ["default-channel", "other-channel"],
mode: "restrict",
override: true,
});
});
it("Do not assign mjml configuration to any channel, when app configuration did not assigned it", () => {
const channels = getChannelsAssignedToConfigId("id", "mjml", {
configurationsPerChannel: {
"default-channel": {
active: true,
mjmlConfigurationId: "other-id",
sendgridConfigurationId: "id",
},
"other-channel": {
active: true,
sendgridConfigurationId: "id",
},
},
});
expect(channels).toEqual({
channels: [],
mode: "restrict",
override: true,
});
});
it("Assign mjml configuration to channel, when app configuration has assigned it", () => {
const channels = getChannelsAssignedToConfigId("id", "mjml", {
configurationsPerChannel: {
"default-channel": {
active: true,
mjmlConfigurationId: "id",
},
"other-channel": {
active: true,
mjmlConfigurationId: "id",
},
},
});
expect(channels).toEqual({
channels: ["default-channel", "other-channel"],
mode: "restrict",
override: true,
});
});
});

View file

@ -0,0 +1,37 @@
import { ChannelConfiguration } from "../../channels/channel-configuration-schema";
import { AppConfig } from "../app-config-schema";
export const getChannelsAssignedToConfigId = (
configId: string,
moduleName: "sendgrid" | "mjml",
appConfig?: AppConfig
): ChannelConfiguration => {
if (!appConfig) {
return {
channels: [],
mode: "restrict",
override: true,
};
}
const channels = [];
if (moduleName === "sendgrid") {
for (const key in appConfig.configurationsPerChannel) {
if (appConfig.configurationsPerChannel[key].sendgridConfigurationId === configId) {
channels.push(key);
}
}
} else {
for (const key in appConfig.configurationsPerChannel) {
if (appConfig.configurationsPerChannel[key].mjmlConfigurationId === configId) {
channels.push(key);
}
}
}
return {
channels,
mode: "restrict",
override: true,
};
};

View file

@ -0,0 +1,38 @@
import { AppConfigPrivateMetadataManager } from "../../../app-configuration/app-config-metadata-manager";
import { SendgridPrivateMetadataManagerV1 } from "../sendgrid-metadata-manager-v1";
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { sendgridTransformV1toV2 } from "./sendgrid-transform-v1-to-v2";
import { createLogger } from "@saleor/apps-shared";
const logger = createLogger({
fn: "sendgridConfigMigrationV1ToV2",
});
interface SendgridConfigMigrationV1ToV1Args {
settingsManager: SettingsManager;
saleorApiUrl: string;
}
export const sendgridConfigMigrationV1ToV2 = async ({
settingsManager,
saleorApiUrl,
}: SendgridConfigMigrationV1ToV1Args) => {
logger.debug("Detect if theres data to migrate");
const appConfigManager = new AppConfigPrivateMetadataManager(settingsManager, saleorApiUrl);
const metadataManagerV1 = new SendgridPrivateMetadataManagerV1(settingsManager, saleorApiUrl);
const configV1 = await metadataManagerV1.getConfig();
if (!configV1) {
logger.debug("No migration required - no previous data");
return undefined;
}
logger.debug("Migrating data");
const appConfigV1 = await appConfigManager.getConfig();
const migratedConfigurationRoot = sendgridTransformV1toV2({ configV1, appConfigV1 });
logger.debug("Data transformed");
return migratedConfigurationRoot;
};

View file

@ -0,0 +1,29 @@
import { z } from "zod";
import { messageEventTypes } from "../../../event-handlers/message-event-types";
export const sendgridEventConfigurationV1Schema = z.object({
active: z.boolean().default(false),
eventType: z.enum(messageEventTypes),
template: z.string().optional(),
});
export type SendgridEventConfigurationV1 = z.infer<typeof sendgridEventConfigurationV1Schema>;
export const sendgridConfigurationV1Schema = z.object({
id: z.string().min(1),
active: z.boolean().default(true),
configurationName: z.string().min(1),
sandboxMode: z.boolean().default(false),
senderName: z.string().optional(),
senderEmail: z.string().optional(),
apiKey: z.string().min(1),
events: z.array(sendgridEventConfigurationV1Schema),
});
export type SendgridConfigurationV1 = z.infer<typeof sendgridConfigurationV1Schema>;
export const sendgridConfigV1Schema = z.object({
configurations: z.array(sendgridConfigurationV1Schema),
});
export type SendgridConfigV1 = z.infer<typeof sendgridConfigV1Schema>;

View file

@ -0,0 +1,32 @@
import { z } from "zod";
import { messageEventTypes } from "../../../event-handlers/message-event-types";
import { channelConfigurationSchema } from "../../../channels/channel-configuration-schema";
export const sendgridConfigurationEventV2Schema = z.object({
active: z.boolean().default(false),
eventType: z.enum(messageEventTypes),
template: z.string().optional(),
});
export type SendgridEventConfigurationV2 = z.infer<typeof sendgridConfigurationEventV2Schema>;
export const sendgridConfigurationV2Schema = z.object({
id: z.string().min(1),
active: z.boolean().default(true),
name: z.string().min(1),
sandboxMode: z.boolean().default(false),
apiKey: z.string().min(1),
sender: z.string().min(1).optional(),
senderEmail: z.string().email().optional(),
senderName: z.string().optional(),
channels: channelConfigurationSchema,
events: z.array(sendgridConfigurationEventV2Schema),
});
export type SendgridConfigurationV2 = z.infer<typeof sendgridConfigurationV2Schema>;
export const sendgridConfigV2Schema = z.object({
configurations: z.array(sendgridConfigurationV2Schema),
});
export type SendgridConfigV2 = z.infer<typeof sendgridConfigV2Schema>;

View file

@ -0,0 +1,128 @@
import { expect, describe, it } from "vitest";
import { sendgridTransformV1toV2 } from "./sendgrid-transform-v1-to-v2";
describe("sendgridTransformV1toV2", function () {
it("No configurations, when no defined previously", () => {
const migratedConfig = sendgridTransformV1toV2({
configV1: {
configurations: [],
},
appConfigV1: undefined,
});
expect(migratedConfig).toEqual({
configurations: [],
});
});
it("Migrate and do not assign to any channel, when no app configuration passed", () => {
const migratedConfig = sendgridTransformV1toV2({
configV1: {
configurations: [
{
id: "id",
configurationName: "name",
active: true,
apiKey: "key",
sandboxMode: true,
senderEmail: "email",
senderName: "name",
events: [
{
active: true,
eventType: "ORDER_CREATED",
template: "template",
},
],
},
],
},
appConfigV1: undefined,
});
expect(migratedConfig).toEqual({
configurations: [
{
id: "id",
name: "name",
active: true,
apiKey: "key",
sandboxMode: true,
senderEmail: "email",
senderName: "name",
channels: {
override: true,
mode: "restrict",
channels: [],
},
events: [
{
active: true,
eventType: "ORDER_CREATED",
template: "template",
},
],
},
],
});
});
it("Migrate and assign to channel, when app configuration is passed", () => {
const migratedConfig = sendgridTransformV1toV2({
configV1: {
configurations: [
{
id: "id",
configurationName: "name",
active: true,
apiKey: "key",
sandboxMode: true,
senderEmail: "email",
senderName: "name",
events: [
{
active: true,
eventType: "ORDER_CREATED",
template: "template",
},
],
},
],
},
appConfigV1: {
configurationsPerChannel: {
"default-channel": {
active: true,
sendgridConfigurationId: "id",
},
},
},
});
expect(migratedConfig).toEqual({
configurations: [
{
id: "id",
name: "name",
active: true,
apiKey: "key",
sandboxMode: true,
senderEmail: "email",
senderName: "name",
channels: {
override: true,
mode: "restrict",
channels: ["default-channel"],
},
events: [
{
active: true,
eventType: "ORDER_CREATED",
template: "template",
},
],
},
],
});
});
});

View file

@ -0,0 +1,33 @@
import { AppConfig } from "../../../app-configuration/app-config-schema";
import { getChannelsAssignedToConfigId } from "../../../app-configuration/migrations/get-channels-assigned-to-config-id";
import { SendgridConfigV1 } from "./sendgrid-config-schema-v1";
import { SendgridConfigV2 } from "./sendgrid-config-schema-v2";
interface SendgridTransformV1toV2Args {
configV1: SendgridConfigV1;
appConfigV1?: AppConfig;
}
export const sendgridTransformV1toV2 = ({ configV1, appConfigV1 }: SendgridTransformV1toV2Args) => {
const migratedConfigurationRoot: SendgridConfigV2 = {
configurations: [],
};
configV1.configurations.forEach((config) => {
const channels = getChannelsAssignedToConfigId(config.id, "sendgrid", appConfigV1);
migratedConfigurationRoot.configurations.push({
id: config.id,
name: config.configurationName,
active: config.active,
apiKey: config.apiKey,
channels,
sandboxMode: config.sandboxMode,
senderEmail: config.senderEmail,
senderName: config.senderName,
events: config.events,
});
});
return migratedConfigurationRoot;
};

View file

@ -1,32 +1,20 @@
import { z } from "zod";
import { messageEventTypes } from "../../event-handlers/message-event-types";
import { channelConfigurationSchema } from "../../channels/channel-configuration-schema";
import {
SendgridConfigV2,
SendgridConfigurationV2,
SendgridEventConfigurationV2,
sendgridConfigV2Schema,
sendgridConfigurationEventV2Schema,
sendgridConfigurationV2Schema,
} from "./migrations/sendgrid-config-schema-v2";
export const sendgridConfigurationEventSchema = z.object({
active: z.boolean().default(false),
eventType: z.enum(messageEventTypes),
template: z.string().optional(),
});
export const sendgridConfigurationEventSchema = sendgridConfigurationEventV2Schema;
export type SendgridEventConfiguration = z.infer<typeof sendgridConfigurationEventSchema>;
export type SendgridEventConfiguration = SendgridEventConfigurationV2;
export const sendgridConfigurationSchema = z.object({
id: z.string().min(1),
active: z.boolean().default(true),
name: z.string().min(1),
sandboxMode: z.boolean().default(false),
apiKey: z.string().min(1),
sender: z.string().min(1).optional(),
senderEmail: z.string().email().optional(),
senderName: z.string().optional(),
channels: channelConfigurationSchema,
events: z.array(sendgridConfigurationEventSchema),
});
export const sendgridConfigurationSchema = sendgridConfigurationV2Schema;
export type SendgridConfiguration = z.infer<typeof sendgridConfigurationSchema>;
export type SendgridConfiguration = SendgridConfigurationV2;
export const sendgridConfigSchema = z.object({
configurations: z.array(sendgridConfigurationSchema),
});
export const sendgridConfigSchema = sendgridConfigV2Schema;
export type SendgridConfig = z.infer<typeof sendgridConfigSchema>;
export type SendgridConfig = SendgridConfigV2;

View file

@ -1,4 +1,3 @@
import { SendgridPrivateMetadataManager } from "./sendgrid-metadata-manager";
import { createLogger } from "@saleor/apps-shared";
import { sendgridDefaultEmptyConfigurations } from "./sendgrid-default-empty-configurations";
import {
@ -9,6 +8,7 @@ import {
import { MessageEventTypes } from "../../event-handlers/message-event-types";
import { generateRandomId } from "../../../lib/generate-random-id";
import { filterConfigurations } from "../../app-configuration/filter-configurations";
import { SendgridPrivateMetadataManager } from "./sendgrid-metadata-manager";
const logger = createLogger({
service: "SendgridConfigurationService",

View file

@ -0,0 +1,32 @@
// TODO: MIGRATION CODE FROM CONFIG VERSION V1. REMOVE THIS FILE AFTER MIGRATION
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { SendgridConfigV1 } from "./migrations/sendgrid-config-schema-v1";
export class SendgridPrivateMetadataManagerV1 {
private metadataKey = "sendgrid-config";
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
getConfig(): Promise<SendgridConfigV1 | undefined> {
return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => {
if (!data) {
return data;
}
try {
return JSON.parse(data);
} catch (e) {
throw new Error("Invalid metadata value, cant be parsed");
}
});
}
setConfig(config: SendgridConfigV1): Promise<void> {
return this.metadataManager.set({
key: this.metadataKey,
value: JSON.stringify(config),
domain: this.saleorApiUrl,
});
}
}

View file

@ -0,0 +1,31 @@
import { SendgridConfig } from "./sendgrid-config-schema";
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { SendgridConfigV2 } from "./migrations/sendgrid-config-schema-v2";
export class SendgridPrivateMetadataManagerV2 {
private metadataKey = "sendgrid-config-v2";
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
getConfig(): Promise<SendgridConfigV2 | undefined> {
return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => {
if (!data) {
return data;
}
try {
return JSON.parse(data);
} catch (e) {
throw new Error("Invalid metadata value, cant be parsed");
}
});
}
setConfig(config: SendgridConfigV2): Promise<void> {
return this.metadataManager.set({
key: this.metadataKey,
value: JSON.stringify(config),
domain: this.saleorApiUrl,
});
}
}

View file

@ -1,23 +1,41 @@
import { SendgridConfig } from "./sendgrid-config-schema";
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { SendgridPrivateMetadataManagerV2 } from "./sendgrid-metadata-manager-v2";
import { sendgridConfigMigrationV1ToV2 } from "./migrations/sendgrid-config-migration-v1-to-v2";
import { createLogger } from "@saleor/apps-shared";
const logger = createLogger({
fn: "SendgridPrivateMetadataManager",
});
export class SendgridPrivateMetadataManager {
private metadataKey = "sendgrid-config";
private metadataKey = "sendgrid-config-v2";
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
getConfig(): Promise<SendgridConfig | undefined> {
return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => {
if (!data) {
return data;
async getConfig() {
logger.debug("Fetching config in the current version");
const currentVersionManager = new SendgridPrivateMetadataManagerV2(
this.metadataManager,
this.saleorApiUrl
);
const currentVersionConfig = await currentVersionManager.getConfig();
if (currentVersionConfig) {
// We have the current version, no need to migrate so we can return it
return currentVersionConfig;
}
try {
return JSON.parse(data);
} catch (e) {
throw new Error("Invalid metadata value, cant be parsed");
}
logger.debug("No config in the current version, trying to migrate from v1");
// TODO: MIGRATION CODE FROM CONFIG VERSION V1. REMOVE AFTER MIGRATION
const migratedSchema = await sendgridConfigMigrationV1ToV2({
saleorApiUrl: this.saleorApiUrl,
settingsManager: this.metadataManager,
});
return migratedSchema;
}
setConfig(config: SendgridConfig): Promise<void> {

View file

@ -0,0 +1,30 @@
import { MessageEventTypes } from "../../../event-handlers/message-event-types";
export interface MjmlEventConfiguration {
active: boolean;
eventType: MessageEventTypes;
template: string;
subject: string;
}
export const smtpEncryptionTypes = ["NONE", "TLS", "SSL"] as const;
export type SmtpEncryptionType = (typeof smtpEncryptionTypes)[number];
export interface MjmlConfiguration {
id: string;
active: boolean;
configurationName: string;
senderName: string;
senderEmail: string;
smtpHost: string;
smtpPort: string;
smtpUser: string;
smtpPassword: string;
encryption: SmtpEncryptionType;
events: MjmlEventConfiguration[];
}
export type MjmlConfig = {
configurations: MjmlConfiguration[];
};

View file

@ -0,0 +1,45 @@
import { AppConfigPrivateMetadataManager } from "../../../app-configuration/app-config-metadata-manager";
import { AppConfig } from "../../../app-configuration/app-config-schema";
import { ChannelConfiguration } from "../../../channels/channel-configuration-schema";
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { MjmlPrivateMetadataManager } from "../mjml-metadata-manager";
import { SmtpConfigV2 } from "./smtp-config-schema-v2";
import { getChannelsAssignedToConfigId } from "../../../app-configuration/migrations/get-channels-assigned-to-config-id";
import { smtpTransformV1toV2 } from "./smtp-transform-v1-to-v2";
import { createLogger } from "@saleor/apps-shared";
const logger = createLogger({
fn: "smtpConfigMigrationV1ToV2",
});
interface SmtpConfigMigrationV1ToV1Args {
settingsManager: SettingsManager;
saleorApiUrl: string;
}
export const smtpConfigMigrationV1ToV2 = async ({
settingsManager,
saleorApiUrl,
}: SmtpConfigMigrationV1ToV1Args) => {
logger.debug("Hello, I'm migrating smtp config from v1 to v2");
const appConfigManager = new AppConfigPrivateMetadataManager(settingsManager, saleorApiUrl);
const metadataManagerV1 = new MjmlPrivateMetadataManager(settingsManager, saleorApiUrl);
const configV1 = await metadataManagerV1.getConfig();
if (!configV1) {
logger.debug("No migration required - no previous data");
return undefined;
}
const appConfigV1 = await appConfigManager.getConfig();
const migratedConfigurationRoot = smtpTransformV1toV2({
configV1,
appConfigV1,
});
logger.debug("Migrated config v1 to v2!");
return migratedConfigurationRoot;
};

View file

@ -0,0 +1,37 @@
import { z } from "zod";
import { messageEventTypes } from "../../../event-handlers/message-event-types";
import { channelConfigurationSchema } from "../../../channels/channel-configuration-schema";
export const smtpEncryptionTypes = ["NONE", "TLS", "SSL"] as const;
export const smtpConfigurationEventV2Schema = z.object({
active: z.boolean().default(false),
eventType: z.enum(messageEventTypes),
template: z.string(),
subject: z.string(),
});
export type SmtpEventConfigurationV2 = z.infer<typeof smtpConfigurationEventV2Schema>;
export const smtpConfigurationV2Schema = z.object({
id: z.string().min(1),
active: z.boolean().default(true),
name: z.string().min(1),
senderName: z.string().optional(),
senderEmail: z.string().email().min(5).optional(),
smtpHost: z.string().min(1),
smtpPort: z.string().min(1),
smtpUser: z.string().optional(),
smtpPassword: z.string().optional(),
encryption: z.enum(smtpEncryptionTypes).default("NONE"),
channels: channelConfigurationSchema,
events: z.array(smtpConfigurationEventV2Schema),
});
export type SmtpConfigurationV2 = z.infer<typeof smtpConfigurationV2Schema>;
export const smtpConfigV2Schema = z.object({
configurations: z.array(smtpConfigurationV2Schema),
});
export type SmtpConfigV2 = z.infer<typeof smtpConfigV2Schema>;

View file

@ -0,0 +1,144 @@
import { expect, describe, it } from "vitest";
import { smtpTransformV1toV2 } from "./smtp-transform-v1-to-v2";
describe("smtpTransformV1toV2", function () {
it("No configurations, when no defined previously", () => {
const migratedConfig = smtpTransformV1toV2({
configV1: {
configurations: [],
},
appConfigV1: undefined,
});
expect(migratedConfig).toEqual({
configurations: [],
});
});
it("Migrate and do not assign to any channel, when no app configuration passed", () => {
const migratedConfig = smtpTransformV1toV2({
configV1: {
configurations: [
{
id: "id",
configurationName: "name",
active: true,
encryption: "NONE",
smtpHost: "host",
smtpPort: "1234",
smtpPassword: "password",
smtpUser: "user",
senderEmail: "email",
senderName: "name",
events: [
{
active: true,
eventType: "ORDER_CREATED",
template: "template",
subject: "subject",
},
],
},
],
},
appConfigV1: undefined,
});
expect(migratedConfig).toEqual({
configurations: [
{
id: "id",
name: "name",
active: true,
encryption: "NONE",
smtpHost: "host",
smtpPort: "1234",
smtpPassword: "password",
smtpUser: "user",
senderEmail: "email",
senderName: "name",
events: [
{
active: true,
eventType: "ORDER_CREATED",
template: "template",
subject: "subject",
},
],
channels: {
override: true,
mode: "restrict",
channels: [],
},
},
],
});
});
it("Migrate and assign to channel, when app configuration is passed", () => {
const migratedConfig = smtpTransformV1toV2({
configV1: {
configurations: [
{
id: "id",
configurationName: "name",
active: true,
encryption: "NONE",
smtpHost: "host",
smtpPort: "1234",
smtpPassword: "password",
smtpUser: "user",
senderEmail: "email",
senderName: "name",
events: [
{
active: true,
eventType: "ORDER_CREATED",
template: "template",
subject: "subject",
},
],
},
],
},
appConfigV1: {
configurationsPerChannel: {
"default-channel": {
active: true,
mjmlConfigurationId: "id",
},
},
},
});
expect(migratedConfig).toEqual({
configurations: [
{
id: "id",
name: "name",
active: true,
encryption: "NONE",
smtpHost: "host",
smtpPort: "1234",
smtpPassword: "password",
smtpUser: "user",
senderEmail: "email",
senderName: "name",
events: [
{
active: true,
eventType: "ORDER_CREATED",
template: "template",
subject: "subject",
},
],
channels: {
override: true,
mode: "restrict",
channels: ["default-channel"],
},
},
],
});
});
});

View file

@ -0,0 +1,36 @@
import { AppConfig } from "../../../app-configuration/app-config-schema";
import { getChannelsAssignedToConfigId } from "../../../app-configuration/migrations/get-channels-assigned-to-config-id";
import { MjmlConfig } from "./mjml-config-schema-v1";
import { SmtpConfigV2 } from "./smtp-config-schema-v2";
interface SmtpTransformV1toV2Args {
configV1: MjmlConfig;
appConfigV1?: AppConfig;
}
export const smtpTransformV1toV2 = ({ configV1, appConfigV1 }: SmtpTransformV1toV2Args) => {
const migratedConfigurationRoot: SmtpConfigV2 = {
configurations: [],
};
configV1.configurations.forEach((config) => {
const channels = getChannelsAssignedToConfigId(config.id, "mjml", appConfigV1);
migratedConfigurationRoot.configurations.push({
id: config.id,
name: config.configurationName,
active: config.active,
channels,
senderEmail: config.senderEmail,
senderName: config.senderName,
events: config.events,
encryption: config.encryption,
smtpHost: config.smtpHost,
smtpPort: config.smtpPort,
smtpPassword: config.smtpPassword,
smtpUser: config.smtpUser,
});
});
return migratedConfigurationRoot;
};

View file

@ -0,0 +1,32 @@
// TODO: MIGRATION CODE FROM CONFIG VERSION V1. REMOVE THIS FILE AFTER MIGRATION
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { MjmlConfig } from "./migrations/mjml-config-schema-v1";
export class MjmlPrivateMetadataManager {
private metadataKey = "mjml-config";
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
getConfig(): Promise<MjmlConfig | undefined> {
return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => {
if (!data) {
return data;
}
try {
return JSON.parse(data);
} catch (e) {
throw new Error("Invalid metadata value, can't be parsed");
}
});
}
setConfig(config: MjmlConfig): Promise<void> {
return this.metadataManager.set({
key: this.metadataKey,
value: JSON.stringify(config),
domain: this.saleorApiUrl,
});
}
}

View file

@ -1,37 +1,22 @@
import { z } from "zod";
import { messageEventTypes } from "../../event-handlers/message-event-types";
import { channelConfigurationSchema } from "../../channels/channel-configuration-schema";
import {
SmtpConfigV2,
SmtpConfigurationV2,
SmtpEventConfigurationV2,
smtpConfigV2Schema,
smtpConfigurationEventV2Schema,
smtpConfigurationV2Schema,
} from "./migrations/smtp-config-schema-v2";
export const smtpEncryptionTypes = ["NONE", "TLS", "SSL"] as const;
export const smtpConfigurationEventSchema = z.object({
active: z.boolean().default(false),
eventType: z.enum(messageEventTypes),
template: z.string(),
subject: z.string(),
});
export const smtpConfigurationEventSchema = smtpConfigurationEventV2Schema;
export type SmtpEventConfiguration = z.infer<typeof smtpConfigurationEventSchema>;
export type SmtpEventConfiguration = SmtpEventConfigurationV2;
export const smtpConfigurationSchema = z.object({
id: z.string().min(1),
active: z.boolean().default(true),
name: z.string().min(1),
senderName: z.string().optional(),
senderEmail: z.string().email().min(5).optional(),
smtpHost: z.string().min(1),
smtpPort: z.string().min(1),
smtpUser: z.string().optional(),
smtpPassword: z.string().optional(),
encryption: z.enum(smtpEncryptionTypes).default("NONE"),
channels: channelConfigurationSchema,
events: z.array(smtpConfigurationEventSchema),
});
export const smtpConfigurationSchema = smtpConfigurationV2Schema;
export type SmtpConfiguration = z.infer<typeof smtpConfigurationSchema>;
export type SmtpConfiguration = SmtpConfigurationV2;
export const smtpConfigSchema = z.object({
configurations: z.array(smtpConfigurationSchema),
});
export const smtpConfigSchema = smtpConfigV2Schema;
export type SmtpConfig = z.infer<typeof smtpConfigSchema>;
export type SmtpConfig = SmtpConfigV2;

View file

@ -0,0 +1,30 @@
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { SmtpConfig } from "./smtp-config-schema";
export class SmtpPrivateMetadataManagerV2 {
private metadataKey = "smtp-config-v2";
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
getConfig(): Promise<SmtpConfig | undefined> {
return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => {
if (!data) {
return data;
}
try {
return JSON.parse(data);
} catch (e) {
throw new Error("Invalid metadata value, can't be parsed");
}
});
}
setConfig(config: SmtpConfig): Promise<void> {
return this.metadataManager.set({
key: this.metadataKey,
value: JSON.stringify(config),
domain: this.saleorApiUrl,
});
}
}

View file

@ -1,23 +1,41 @@
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { SmtpConfig } from "./smtp-config-schema";
import { SmtpPrivateMetadataManagerV2 } from "./smtp-metadata-manager-v2";
import { smtpConfigMigrationV1ToV2 } from "./migrations/smtp-config-migration-v1-to-v2";
import { createLogger } from "@saleor/apps-shared";
const logger = createLogger({
fn: "SmtpPrivateMetadataManager",
});
export class SmtpPrivateMetadataManager {
private metadataKey = "smtp-config";
private metadataKey = "smtp-config-v2";
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
getConfig(): Promise<SmtpConfig | undefined> {
return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => {
if (!data) {
return data;
async getConfig() {
logger.debug("Fetching config in the current version");
const currentVersionManager = new SmtpPrivateMetadataManagerV2(
this.metadataManager,
this.saleorApiUrl
);
const currentVersionConfig = await currentVersionManager.getConfig();
if (currentVersionConfig) {
// We have the current version, no need to migrate so we can return it
return currentVersionConfig;
}
try {
return JSON.parse(data);
} catch (e) {
throw new Error("Invalid metadata value, can't be parsed");
}
logger.debug("No config in the current version, trying to migrate from v1");
// TODO: MIGRATION CODE FROM CONFIG VERSION V1. REMOVE AFTER MIGRATION
const migratedSchema = await smtpConfigMigrationV1ToV2({
saleorApiUrl: this.saleorApiUrl,
settingsManager: this.metadataManager,
});
return migratedSchema;
}
setConfig(config: SmtpConfig): Promise<void> {

View file

@ -524,6 +524,9 @@ importers:
clsx:
specifier: ^1.2.1
version: 1.2.1
dotenv:
specifier: ^16.0.3
version: 16.0.3
graphql:
specifier: ^16.6.0
version: 16.6.0
@ -6167,7 +6170,7 @@ packages:
'@graphql-tools/utils': 9.2.1(graphql@16.6.0)
'@whatwg-node/fetch': 0.7.0(@types/node@18.13.0)
graphql: 16.6.0
tslib: 2.5.0
tslib: 2.5.2
transitivePeerDependencies:
- '@types/node'
- encoding
@ -6194,7 +6197,7 @@ packages:
'@graphql-tools/utils': 9.2.1(graphql@16.6.0)
globby: 11.1.0
graphql: 16.6.0
tslib: 2.5.0
tslib: 2.5.2
unixify: 1.0.0
transitivePeerDependencies:
- '@babel/core'
@ -6309,7 +6312,7 @@ packages:
graphql: 16.6.0
is-glob: 4.0.3
micromatch: 4.0.5
tslib: 2.5.0
tslib: 2.5.2
unixify: 1.0.0
transitivePeerDependencies:
- '@babel/core'
@ -6344,7 +6347,7 @@ packages:
'@graphql-tools/utils': 9.2.1(graphql@16.6.0)
'@whatwg-node/fetch': 0.7.0(@types/node@18.13.0)
graphql: 16.6.0
tslib: 2.5.0
tslib: 2.5.2
transitivePeerDependencies:
- '@babel/core'
- '@types/node'
@ -6361,7 +6364,7 @@ packages:
'@graphql-tools/utils': 9.2.1(graphql@16.6.0)
globby: 11.1.0
graphql: 16.6.0
tslib: 2.5.0
tslib: 2.5.2
unixify: 1.0.0
dev: true
@ -6401,7 +6404,7 @@ packages:
'@graphql-tools/utils': 9.2.1(graphql@16.6.0)
globby: 11.1.0
graphql: 16.6.0
tslib: 2.5.0
tslib: 2.5.2
unixify: 1.0.0
dev: true
@ -6414,7 +6417,7 @@ packages:
'@graphql-tools/utils': 9.2.1(graphql@16.6.0)
graphql: 16.6.0
p-limit: 3.1.0
tslib: 2.5.0
tslib: 2.5.2
dev: true
/@graphql-tools/merge@8.3.18(graphql@16.6.0):
@ -6492,7 +6495,7 @@ packages:
jsonwebtoken: 9.0.0
lodash: 4.17.21
scuid: 1.1.0
tslib: 2.5.0
tslib: 2.5.2
yaml-ast-parser: 0.0.43
transitivePeerDependencies:
- '@types/node'
@ -6586,7 +6589,7 @@ packages:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
dependencies:
graphql: 16.6.0
tslib: 2.4.1
tslib: 2.5.2
dev: true
/@graphql-tools/utils@9.2.1(graphql@16.6.0):
@ -13744,7 +13747,6 @@ packages:
/dotenv@16.0.3:
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'}
dev: true
/dotenv@8.6.0:
resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==}
@ -14436,7 +14438,7 @@ packages:
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
dependencies:
'@babel/runtime': 7.20.13
'@babel/runtime': 7.22.3
aria-query: 5.1.3
array-includes: 3.1.6
array.prototype.flatmap: 1.3.1
@ -16075,7 +16077,7 @@ packages:
jiti: 1.17.1
minimatch: 4.2.3
string-env-interpolation: 1.0.1
tslib: 2.5.0
tslib: 2.5.2
transitivePeerDependencies:
- '@types/node'
- bufferutil