add ts-reset and adjust codebase
This commit is contained in:
parent
7e0755ec9e
commit
7fa1648349
65 changed files with 228 additions and 617 deletions
|
@ -57,6 +57,7 @@
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
|
|
1
apps/cms-v2/reset.d.ts
vendored
Normal file
1
apps/cms-v2/reset.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "@total-typescript/ts-reset";
|
|
@ -19,7 +19,7 @@ export class AppConfig {
|
||||||
connections: [],
|
connections: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(initialData?: RootConfig.Shape) {
|
constructor(initialData?: RootConfig.Shape | unknown) {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
this.rootData = RootConfig.Schema.parse(initialData);
|
this.rootData = RootConfig.Schema.parse(initialData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { BuilderIoProviderConfig } from "@/modules/configuration";
|
||||||
import { WebhookProductVariantFragment } from "../../../../generated/graphql";
|
import { WebhookProductVariantFragment } from "../../../../generated/graphql";
|
||||||
import { createLogger } from "@saleor/apps-shared";
|
import { createLogger } from "@saleor/apps-shared";
|
||||||
import { FieldsMapper } from "../fields-mapper";
|
import { FieldsMapper } from "../fields-mapper";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
// https://www.builder.io/c/docs/write-api
|
// https://www.builder.io/c/docs/write-api
|
||||||
export class BuilderIoClient {
|
export class BuilderIoClient {
|
||||||
|
@ -43,7 +44,7 @@ export class BuilderIoClient {
|
||||||
|
|
||||||
private async updateProductVariantCall(
|
private async updateProductVariantCall(
|
||||||
builderIoEntryId: string,
|
builderIoEntryId: string,
|
||||||
variant: WebhookProductVariantFragment
|
variant: WebhookProductVariantFragment,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(this.endpoint + `/${builderIoEntryId}`, {
|
const response = await fetch(this.endpoint + `/${builderIoEntryId}`, {
|
||||||
|
@ -71,13 +72,13 @@ export class BuilderIoClient {
|
||||||
{
|
{
|
||||||
entriesToUpdate,
|
entriesToUpdate,
|
||||||
},
|
},
|
||||||
"Trying to update variants in builder.io with following IDs"
|
"Trying to update variants in builder.io with following IDs",
|
||||||
);
|
);
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
entriesToUpdate.map((id) => {
|
entriesToUpdate.map((id) => {
|
||||||
return this.updateProductVariantCall(id, variant);
|
return this.updateProductVariantCall(id, variant);
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +95,7 @@ export class BuilderIoClient {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
entriesToUpdate.map((id) => {
|
entriesToUpdate.map((id) => {
|
||||||
return this.updateProductVariantCall(id, variant);
|
return this.updateProductVariantCall(id, variant);
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,8 +113,8 @@ export class BuilderIoClient {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${this.config.privateApiKey}`,
|
Authorization: `Bearer ${this.config.privateApiKey}`,
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,15 +128,23 @@ export class BuilderIoClient {
|
||||||
variantID: variantId,
|
variantID: variantId,
|
||||||
variantFieldMapping: this.config.productVariantFieldsMapping.variantId,
|
variantFieldMapping: this.config.productVariantFieldsMapping.variantId,
|
||||||
},
|
},
|
||||||
"Trying to fetch variant from Builder.io"
|
"Trying to fetch variant from Builder.io",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const expectedSchema = z.object({
|
||||||
|
results: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
return fetch(
|
return fetch(
|
||||||
`https://cdn.builder.io/api/v3/content/${this.config.modelName}?apiKey=${this.config.publicApiKey}&query.data.${this.config.productVariantFieldsMapping.variantId}.$eq=${variantId}&limit=10&includeUnpublished=false&cacheSeconds=0`
|
`https://cdn.builder.io/api/v3/content/${this.config.modelName}?apiKey=${this.config.publicApiKey}&query.data.${this.config.productVariantFieldsMapping.variantId}.$eq=${variantId}&limit=10&includeUnpublished=false&cacheSeconds=0`,
|
||||||
)
|
)
|
||||||
.then((res) => res.json())
|
.then((res) => expectedSchema.parse(res.json()))
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data.results.map((result: any) => result.id) as string[];
|
return data.results.map((result) => result.id) as string[];
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.logger.error(err, "Failed to fetch builder.io entry id");
|
this.logger.error(err, "Failed to fetch builder.io entry id");
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/mailchimp__mailchimp_marketing": "^3.0.7",
|
"@types/mailchimp__mailchimp_marketing": "^3.0.7",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
|
|
1
apps/crm/reset.d.ts
vendored
Normal file
1
apps/crm/reset.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "@total-typescript/ts-reset";
|
|
@ -23,7 +23,7 @@ export const AppBridgePersistence = {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(storageItem as string);
|
return JSON.parse(storageItem as string) as AppBridgeStorageState;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { MailchimpConfigSettingsManagerV1 } from "./mailchimp-config-settings-manager";
|
import { MailchimpConfigSettingsManagerV1, MailchimpConfigType } from "./mailchimp-config-settings-manager";
|
||||||
import { Client } from "urql";
|
import { Client } from "urql";
|
||||||
import { SettingsManager, SettingsValue } from "@saleor/app-sdk/settings-manager";
|
import { SettingsManager, SettingsValue } from "@saleor/app-sdk/settings-manager";
|
||||||
|
|
||||||
|
@ -70,9 +70,11 @@ describe("MailchimpConfigSettingsManagerV1", () => {
|
||||||
dc: "us41",
|
dc: "us41",
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsedSetValue = JSON.parse(valueHasBeenSet!);
|
const parsedSetValue = JSON.parse(valueHasBeenSet!) as {
|
||||||
|
config: MailchimpConfigType
|
||||||
|
};
|
||||||
|
|
||||||
expect(parsedSetValue.config.customerCreateEvent.enabled).toBe(false);
|
expect(parsedSetValue.config.customerCreateEvent?.enabled).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Calls settings manager with default customerCreateEvent setting to be disabled", async () => {
|
it("Calls settings manager with default customerCreateEvent setting to be disabled", async () => {
|
||||||
|
@ -90,9 +92,11 @@ describe("MailchimpConfigSettingsManagerV1", () => {
|
||||||
dc: "us41",
|
dc: "us41",
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsedSetValue = JSON.parse(valueHasBeenSet!);
|
const parsedSetValue = JSON.parse(valueHasBeenSet!) as {
|
||||||
|
config: MailchimpConfigType
|
||||||
|
};
|
||||||
|
|
||||||
expect(parsedSetValue.config.customerCreateEvent.enabled).toBe(false);
|
expect(parsedSetValue.config.customerCreateEvent?.enabled).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(".get returns null if data doesnt match schema", async () => {
|
it(".get returns null if data doesnt match schema", async () => {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { NextApiHandler } from "next";
|
import { NextApiHandler } from "next";
|
||||||
import { MailchimpClientOAuth } from "../../../../modules/mailchimp/mailchimp-client";
|
import { MailchimpClientOAuth } from "../../../../modules/mailchimp/mailchimp-client";
|
||||||
import { createLogger } from "@saleor/apps-shared";
|
import { createLogger } from "@saleor/apps-shared";
|
||||||
|
import { z } from "zod";
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
|
||||||
export const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
|
export const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
|
||||||
const { host, "x-forwarded-proto": protocol = "http" } = headers;
|
const { host, "x-forwarded-proto": protocol = "http" } = headers;
|
||||||
|
@ -8,6 +10,17 @@ export const getBaseUrl = (headers: { [name: string]: string | string[] | undefi
|
||||||
return `${protocol}://${host}`;
|
return `${protocol}://${host}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tokenResponseSchema = z.object({
|
||||||
|
access_token: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadataResponseSchema = z.object({
|
||||||
|
dc: z.string().min(1),
|
||||||
|
login: z.object({
|
||||||
|
email: z.string().min(1),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const handler: NextApiHandler = async (req, res) => {
|
const handler: NextApiHandler = async (req, res) => {
|
||||||
const baseUrl = getBaseUrl(req.headers);
|
const baseUrl = getBaseUrl(req.headers);
|
||||||
|
|
||||||
|
@ -28,25 +41,46 @@ const handler: NextApiHandler = async (req, res) => {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { access_token } = await tokenResponse.json();
|
let accessToken: string;
|
||||||
|
|
||||||
logger.debug({ access_token }, "Received mailchimp access_token");
|
try {
|
||||||
|
const tokenResponseJson = await tokenResponse.json();
|
||||||
|
const parsedTokenResponse = tokenResponseSchema.parse(tokenResponseJson);
|
||||||
|
|
||||||
const metadataResponse = await fetch("https://login.mailchimp.com/oauth2/metadata", {
|
accessToken = parsedTokenResponse.access_token;
|
||||||
headers: {
|
} catch {
|
||||||
Authorization: `OAuth ${access_token}`,
|
Sentry.captureException(
|
||||||
},
|
"Mailchimp token response doesnt contain access_token or can't be fetched",
|
||||||
});
|
);
|
||||||
|
|
||||||
const metadata = await metadataResponse.json();
|
return res.status(500).end();
|
||||||
|
}
|
||||||
|
|
||||||
const mc = new MailchimpClientOAuth(metadata.dc, access_token);
|
logger.debug({ access_token: accessToken }, "Received mailchimp access_token");
|
||||||
|
|
||||||
await mc.ping();
|
try {
|
||||||
|
const metadataResponse = await fetch("https://login.mailchimp.com/oauth2/metadata", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `OAuth ${accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return res.redirect(
|
const metadataJson = await metadataResponse.json();
|
||||||
`/configuration/mailchimp/oauth-success?token=${access_token}&email=${metadata.login.email}&dc=${metadata.dc}`
|
|
||||||
); // todo maybe move to cookie
|
const parsedMetadata = metadataResponseSchema.parse(metadataJson);
|
||||||
|
|
||||||
|
const mc = new MailchimpClientOAuth(parsedMetadata.dc, accessToken);
|
||||||
|
|
||||||
|
await mc.ping();
|
||||||
|
|
||||||
|
return res.redirect(
|
||||||
|
`/configuration/mailchimp/oauth-success?token=${accessToken}&email=${parsedMetadata.login.email}&dc=${parsedMetadata.dc}`,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
Sentry.captureException("Mailchimp oauth metadata cant be fetched or is malformed");
|
||||||
|
|
||||||
|
return res.status(500).end();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/dot-object": "^2.1.2",
|
"@types/dot-object": "^2.1.2",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
|
|
1
apps/data-importer/reset.d.ts
vendored
Normal file
1
apps/data-importer/reset.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "@total-typescript/ts-reset";
|
|
@ -61,6 +61,7 @@
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/html-to-text": "^9.0.0",
|
"@types/html-to-text": "^9.0.0",
|
||||||
"@types/mjml": "^4.7.0",
|
"@types/mjml": "^4.7.0",
|
||||||
"@types/nodemailer": "^6.4.7",
|
"@types/nodemailer": "^6.4.7",
|
||||||
|
|
1
apps/emails-and-messages/reset.d.ts
vendored
Normal file
1
apps/emails-and-messages/reset.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "@total-typescript/ts-reset";
|
|
@ -50,6 +50,7 @@
|
||||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||||
"@graphql-codegen/typescript-urql": "3.7.3",
|
"@graphql-codegen/typescript-urql": "3.7.3",
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"@types/rimraf": "^3.0.2",
|
"@types/rimraf": "^3.0.2",
|
||||||
|
|
1
apps/invoices/reset.d.ts
vendored
Normal file
1
apps/invoices/reset.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "@total-typescript/ts-reset";
|
|
@ -1,41 +0,0 @@
|
||||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
|
||||||
|
|
||||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
|
||||||
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 = createGraphQLClient({
|
|
||||||
saleorApiUrl: apiUrl,
|
|
||||||
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();
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
export const MigrationV1toV2Consts = {
|
|
||||||
appConfigV2metadataKey: "app-config-v2",
|
|
||||||
appConfigV1metadataKey: "app-config",
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
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)
|
|
|
@ -1,46 +0,0 @@
|
||||||
/* eslint-disable turbo/no-undeclared-env-vars */
|
|
||||||
|
|
||||||
import * as dotenv from "dotenv";
|
|
||||||
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "../migration-utils";
|
|
||||||
import { RemoveMetadataDocument } from "../../../generated/graphql";
|
|
||||||
import { MigrationV1toV2Consts } from "./const";
|
|
||||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const runMigration = async () => {
|
|
||||||
verifyRequiredEnvs();
|
|
||||||
|
|
||||||
const allEnvs = await fetchCloudAplEnvs();
|
|
||||||
|
|
||||||
const results = await Promise.all(
|
|
||||||
allEnvs.map((env) => {
|
|
||||||
const client = createGraphQLClient({
|
|
||||||
saleorApiUrl: env.saleorApiUrl,
|
|
||||||
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();
|
|
|
@ -1,67 +0,0 @@
|
||||||
/* 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();
|
|
|
@ -1,61 +0,0 @@
|
||||||
/* 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();
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SellerShopConfig } from "../modules/app-configuration/schema-v1/app-config-v1";
|
import { ShopAddress } from "../modules/shop-info/shop-address";
|
||||||
|
|
||||||
export const getMockAddress = (): SellerShopConfig["address"] => {
|
export const getMockAddress = (): ShopAddress => {
|
||||||
return {
|
return {
|
||||||
city: "Wrocław",
|
city: "Wrocław",
|
||||||
cityArea: "",
|
cityArea: "",
|
||||||
|
|
|
@ -3,11 +3,10 @@ import { z } from "zod";
|
||||||
import { protectedClientProcedure } from "../trpc/protected-client-procedure";
|
import { protectedClientProcedure } from "../trpc/protected-client-procedure";
|
||||||
import { router } from "../trpc/trpc-server";
|
import { router } from "../trpc/trpc-server";
|
||||||
import { createSettingsManager } from "./metadata-manager";
|
import { createSettingsManager } from "./metadata-manager";
|
||||||
|
import { AppConfigV2 } from "./schema-v2/app-config";
|
||||||
|
import { AddressV2Schema } from "./schema-v2/app-config-schema.v2";
|
||||||
import { AppConfigV2MetadataManager } from "./schema-v2/app-config-v2-metadata-manager";
|
import { AppConfigV2MetadataManager } from "./schema-v2/app-config-v2-metadata-manager";
|
||||||
import { GetAppConfigurationV2Service } from "./schema-v2/get-app-configuration.v2.service";
|
import { GetAppConfigurationV2Service } from "./schema-v2/get-app-configuration.v2.service";
|
||||||
import { ConfigV1ToV2MigrationService } from "./schema-v2/config-v1-to-v2-migration.service";
|
|
||||||
import { AddressV2Schema } from "./schema-v2/app-config-schema.v2";
|
|
||||||
import { AppConfigV2 } from "./schema-v2/app-config";
|
|
||||||
|
|
||||||
const UpsertAddressSchema = z.object({
|
const UpsertAddressSchema = z.object({
|
||||||
address: AddressV2Schema,
|
address: AddressV2Schema,
|
||||||
|
@ -47,7 +46,7 @@ export const appConfigurationRouter = router({
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
channelSlug: z.string(),
|
channelSlug: z.string(),
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const appConfigV2 =
|
const appConfigV2 =
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { SellerAddress } from "../address";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* Remove when SchemaV1 is migrated to SchemaV2
|
|
||||||
*/
|
|
||||||
export interface SellerShopConfig {
|
|
||||||
address: SellerAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* Remove when SchemaV1 is migrated to SchemaV2
|
|
||||||
*/
|
|
||||||
export type ShopConfigPerChannelSlug = Record<string, SellerShopConfig>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* Remove when SchemaV1 is migrated to SchemaV2
|
|
||||||
*/
|
|
||||||
export type AppConfigV1 = {
|
|
||||||
shopConfigPerChannel: ShopConfigPerChannelSlug;
|
|
||||||
};
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { AppConfigV1 } from "./app-config-v1";
|
|
||||||
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* Remove when SchemaV1 is migrated to SchemaV2
|
|
||||||
*/
|
|
||||||
export interface AppConfigurator {
|
|
||||||
setConfig(config: AppConfigV1): Promise<void>;
|
|
||||||
getConfig(): Promise<AppConfigV1 | undefined>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* Remove when SchemaV1 is migrated to SchemaV2
|
|
||||||
*/
|
|
||||||
export class PrivateMetadataAppConfiguratorV1 implements AppConfigurator {
|
|
||||||
private metadataKey = "app-config";
|
|
||||||
|
|
||||||
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
|
|
||||||
|
|
||||||
getConfig(): Promise<AppConfigV1 | 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: AppConfigV1): Promise<void> {
|
|
||||||
return this.metadataManager.set({
|
|
||||||
key: this.metadataKey,
|
|
||||||
value: JSON.stringify(config),
|
|
||||||
domain: this.saleorApiUrl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ import { z } from "zod";
|
||||||
export class AppConfigV2 {
|
export class AppConfigV2 {
|
||||||
private rootData: AppConfigV2Shape = { channelsOverrides: {} };
|
private rootData: AppConfigV2Shape = { channelsOverrides: {} };
|
||||||
|
|
||||||
constructor(initialData?: AppConfigV2Shape) {
|
constructor(initialData?: AppConfigV2Shape | unknown) {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
this.rootData = AppConfigV2Schema.parse(initialData);
|
this.rootData = AppConfigV2Schema.parse(initialData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { ConfigV1ToV2MigrationService } from "./config-v1-to-v2-migration.service";
|
|
||||||
import { SimpleGraphqlClient } from "../metadata-manager";
|
|
||||||
import { getMockAddress } from "../../../fixtures/mock-address";
|
|
||||||
import { AppConfigV2 } from "./app-config";
|
|
||||||
|
|
||||||
describe("config-v1-to-v2-migration.service", () => {
|
|
||||||
const mockClient: SimpleGraphqlClient = {
|
|
||||||
mutation: vi.fn(),
|
|
||||||
query: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let service: ConfigV1ToV2MigrationService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.resetAllMocks();
|
|
||||||
|
|
||||||
service = new ConfigV1ToV2MigrationService(mockClient, "https://example.com/graphql/");
|
|
||||||
|
|
||||||
vi.spyOn(service.configMetadataManager, "set").mockImplementationOnce(async () =>
|
|
||||||
Promise.resolve()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Returns a pure V2 config if V1 config is not present", async () => {
|
|
||||||
vi.spyOn(service.metadataV1AppConfigurator, "getConfig").mockImplementationOnce(async () =>
|
|
||||||
Promise.resolve(undefined)
|
|
||||||
);
|
|
||||||
|
|
||||||
const migrationResult = await service.migrate();
|
|
||||||
|
|
||||||
expect(migrationResult.getChannelsOverrides()).toEqual({});
|
|
||||||
expect(service.configMetadataManager.set).toHaveBeenCalledWith(migrationResult.serialize());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Returns a migrated V2 config from V1 if V1 config is present", async () => {
|
|
||||||
vi.spyOn(service.metadataV1AppConfigurator, "getConfig").mockImplementationOnce(async () =>
|
|
||||||
Promise.resolve({
|
|
||||||
shopConfigPerChannel: {
|
|
||||||
"default-channel": {
|
|
||||||
address: getMockAddress(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const migrationResult = await service.migrate();
|
|
||||||
|
|
||||||
expect(migrationResult.getChannelsOverrides()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
"default-channel": expect.objectContaining(getMockAddress()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Runs a beforeSave callback and saves modified state in metadata - missing v1 config scenario", async () => {
|
|
||||||
vi.spyOn(service.metadataV1AppConfigurator, "getConfig").mockImplementationOnce(async () =>
|
|
||||||
Promise.resolve(undefined)
|
|
||||||
);
|
|
||||||
|
|
||||||
const beforeSaveCb = vi.fn().mockImplementationOnce((config: AppConfigV2) => {
|
|
||||||
config.upsertOverride("test", getMockAddress());
|
|
||||||
});
|
|
||||||
|
|
||||||
const migrationResult = await service.migrate(beforeSaveCb);
|
|
||||||
|
|
||||||
expect(migrationResult.getChannelsOverrides()).toEqual({
|
|
||||||
test: expect.objectContaining(getMockAddress()),
|
|
||||||
});
|
|
||||||
expect(service.configMetadataManager.set).toHaveBeenCalledWith(migrationResult.serialize());
|
|
||||||
expect(beforeSaveCb).toHaveBeenCalledWith(migrationResult);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Runs a beforeSave callback and saves modified state in metadata - present v1 config scenario", async () => {
|
|
||||||
vi.spyOn(service.metadataV1AppConfigurator, "getConfig").mockImplementationOnce(async () =>
|
|
||||||
Promise.resolve({
|
|
||||||
shopConfigPerChannel: {
|
|
||||||
"default-channel": {
|
|
||||||
address: getMockAddress(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const beforeSaveCb = vi.fn().mockImplementationOnce((config: AppConfigV2) => {
|
|
||||||
config.removeOverride("default-channel");
|
|
||||||
});
|
|
||||||
|
|
||||||
const migrationResult = await service.migrate(beforeSaveCb);
|
|
||||||
|
|
||||||
expect(migrationResult.getChannelsOverrides()).toEqual({});
|
|
||||||
expect(service.configMetadataManager.set).toHaveBeenCalledWith(migrationResult.serialize());
|
|
||||||
expect(beforeSaveCb).toHaveBeenCalledWith(migrationResult);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { PrivateMetadataAppConfiguratorV1 } from "../schema-v1/app-configurator";
|
|
||||||
import { createSettingsManager, SimpleGraphqlClient } from "../metadata-manager";
|
|
||||||
import { AppConfigV2 } from "./app-config";
|
|
||||||
import { ConfigV1ToV2Transformer } from "./config-v1-to-v2-transformer";
|
|
||||||
import { AppConfigV2MetadataManager } from "./app-config-v2-metadata-manager";
|
|
||||||
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
|
||||||
|
|
||||||
export class ConfigV1ToV2MigrationService {
|
|
||||||
settingsManager: SettingsManager;
|
|
||||||
configMetadataManager: AppConfigV2MetadataManager;
|
|
||||||
metadataV1AppConfigurator: PrivateMetadataAppConfiguratorV1;
|
|
||||||
|
|
||||||
constructor(private client: SimpleGraphqlClient, private saleorApiUrl: string) {
|
|
||||||
this.settingsManager = createSettingsManager(client);
|
|
||||||
this.configMetadataManager = new AppConfigV2MetadataManager(this.settingsManager);
|
|
||||||
this.metadataV1AppConfigurator = new PrivateMetadataAppConfiguratorV1(
|
|
||||||
this.settingsManager,
|
|
||||||
this.saleorApiUrl
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async migrate(beforeSave?: (config: AppConfigV2) => void): Promise<AppConfigV2> {
|
|
||||||
const v1Config = await this.metadataV1AppConfigurator.getConfig();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If no v1 config, it means clean install - return pure config
|
|
||||||
*/
|
|
||||||
if (!v1Config) {
|
|
||||||
const pureConfig = new AppConfigV2();
|
|
||||||
|
|
||||||
if (beforeSave) {
|
|
||||||
beforeSave(pureConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.configMetadataManager.set(pureConfig.serialize());
|
|
||||||
|
|
||||||
return pureConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Otherwise, transform v1 config to v2 and save it
|
|
||||||
*/
|
|
||||||
const transformer = new ConfigV1ToV2Transformer();
|
|
||||||
const appConfigV2FromV1 = transformer.transform(v1Config);
|
|
||||||
|
|
||||||
if (beforeSave) {
|
|
||||||
beforeSave(appConfigV2FromV1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.configMetadataManager.set(appConfigV2FromV1.serialize());
|
|
||||||
|
|
||||||
return appConfigV2FromV1;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { ConfigV1ToV2Transformer } from "./config-v1-to-v2-transformer";
|
|
||||||
import { getMockAddress } from "../../../fixtures/mock-address";
|
|
||||||
|
|
||||||
describe("ConfigV1ToV2Transformer", function () {
|
|
||||||
it("Returns empty V2 instance if config is null", () => {
|
|
||||||
// @ts-expect-error
|
|
||||||
const v2 = new ConfigV1ToV2Transformer().transform(null);
|
|
||||||
|
|
||||||
expect(v2.serialize()).toMatchInlineSnapshot('"{\\"channelsOverrides\\":{}}"');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Maps V1 address overrides to V2 - single channel override", () => {
|
|
||||||
const v2 = new ConfigV1ToV2Transformer().transform({
|
|
||||||
shopConfigPerChannel: {
|
|
||||||
"default-channel": {
|
|
||||||
address: getMockAddress(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(v2.getChannelsOverrides()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
"default-channel": getMockAddress(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Maps V1 address overrides to V2 - multiple channels override", () => {
|
|
||||||
const v2 = new ConfigV1ToV2Transformer().transform({
|
|
||||||
shopConfigPerChannel: {
|
|
||||||
"default-channel": {
|
|
||||||
address: getMockAddress(),
|
|
||||||
},
|
|
||||||
"custom-channel": {
|
|
||||||
address: getMockAddress(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(v2.getChannelsOverrides()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
"default-channel": getMockAddress(),
|
|
||||||
"custom-channel": getMockAddress(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Falls back to empty string for address property if not set", () => {
|
|
||||||
const addressMock = getMockAddress();
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
delete addressMock.city;
|
|
||||||
|
|
||||||
const v2 = new ConfigV1ToV2Transformer().transform({
|
|
||||||
shopConfigPerChannel: {
|
|
||||||
"default-channel": {
|
|
||||||
address: addressMock,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(v2.getChannelsOverrides()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
"default-channel": {
|
|
||||||
...getMockAddress(),
|
|
||||||
city: "",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { AppConfigV1 } from "../schema-v1/app-config-v1";
|
|
||||||
import { AppConfigV2 } from "./app-config";
|
|
||||||
|
|
||||||
export class ConfigV1ToV2Transformer {
|
|
||||||
transform(v1Config: AppConfigV1): AppConfigV2 {
|
|
||||||
const configV2 = new AppConfigV2();
|
|
||||||
|
|
||||||
if (!v1Config || !v1Config.shopConfigPerChannel) {
|
|
||||||
return configV2;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(v1Config.shopConfigPerChannel).forEach(([channelSlug, channelConfigV1]) => {
|
|
||||||
const addressV1 = channelConfigV1.address;
|
|
||||||
|
|
||||||
configV2.upsertOverride(channelSlug, {
|
|
||||||
city: addressV1.city ?? "",
|
|
||||||
country: addressV1.country ?? "",
|
|
||||||
streetAddress2: addressV1.streetAddress2 ?? "",
|
|
||||||
postalCode: addressV1.postalCode ?? "",
|
|
||||||
companyName: addressV1.companyName ?? "",
|
|
||||||
streetAddress1: addressV1.streetAddress1 ?? "",
|
|
||||||
countryArea: addressV1.countryArea ?? "",
|
|
||||||
cityArea: addressV1.cityArea ?? "",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return configV2;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { OrderPayloadFragment } from "../../../../generated/graphql";
|
import { OrderPayloadFragment } from "../../../../generated/graphql";
|
||||||
import { SellerShopConfig } from "../../app-configuration/schema-v1/app-config-v1";
|
import { ShopAddress } from "../../shop-info/shop-address";
|
||||||
|
|
||||||
export interface InvoiceGenerator {
|
export interface InvoiceGenerator {
|
||||||
generate(input: {
|
generate(input: {
|
||||||
order: OrderPayloadFragment;
|
order: OrderPayloadFragment;
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
companyAddressData: SellerShopConfig["address"];
|
companyAddressData: ShopAddress;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { InvoiceGenerator } from "../invoice-generator";
|
import { OrderPayloadFragment } from "../../../../../generated/graphql";
|
||||||
import { Order, OrderPayloadFragment } from "../../../../../generated/graphql";
|
|
||||||
import { SellerShopConfig } from "../../../app-configuration/schema-v1/app-config-v1";
|
|
||||||
import { AddressV2Shape } from "../../../app-configuration/schema-v2/app-config-schema.v2";
|
import { AddressV2Shape } from "../../../app-configuration/schema-v2/app-config-schema.v2";
|
||||||
|
import { InvoiceGenerator } from "../invoice-generator";
|
||||||
const Microinvoice = require("microinvoice");
|
const Microinvoice = require("microinvoice");
|
||||||
|
|
||||||
export class MicroinvoiceInvoiceGenerator implements InvoiceGenerator {
|
export class MicroinvoiceInvoiceGenerator implements InvoiceGenerator {
|
||||||
constructor(
|
constructor(
|
||||||
private settings = {
|
private settings = {
|
||||||
locale: "en-US",
|
locale: "en-US",
|
||||||
}
|
},
|
||||||
) {}
|
) {}
|
||||||
async generate(input: {
|
async generate(input: {
|
||||||
order: OrderPayloadFragment;
|
order: OrderPayloadFragment;
|
||||||
|
|
10
apps/invoices/src/modules/shop-info/shop-address.ts
Normal file
10
apps/invoices/src/modules/shop-info/shop-address.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export type ShopAddress = {
|
||||||
|
city: string;
|
||||||
|
cityArea: string;
|
||||||
|
companyName: string;
|
||||||
|
country: string;
|
||||||
|
countryArea: string;
|
||||||
|
postalCode: string;
|
||||||
|
streetAddress1: string;
|
||||||
|
streetAddress2: string;
|
||||||
|
};
|
|
@ -1,30 +1,25 @@
|
||||||
|
import { SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
|
||||||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||||
|
import { createGraphQLClient, createLogger } from "@saleor/apps-shared";
|
||||||
import { gql } from "urql";
|
import { gql } from "urql";
|
||||||
import { saleorApp } from "../../../saleor-app";
|
|
||||||
import {
|
import {
|
||||||
InvoiceRequestedPayloadFragment,
|
InvoiceRequestedPayloadFragment,
|
||||||
OrderPayloadFragment,
|
OrderPayloadFragment,
|
||||||
} from "../../../../generated/graphql";
|
} from "../../../../generated/graphql";
|
||||||
import { SaleorInvoiceUploader } from "../../../modules/invoices/invoice-uploader/saleor-invoice-uploader";
|
import { AddressV2Shape } from "../../../modules/app-configuration/schema-v2/app-config-schema.v2";
|
||||||
|
import { GetAppConfigurationV2Service } from "../../../modules/app-configuration/schema-v2/get-app-configuration.v2.service";
|
||||||
import { InvoiceCreateNotifier } from "../../../modules/invoices/invoice-create-notifier/invoice-create-notifier";
|
import { InvoiceCreateNotifier } from "../../../modules/invoices/invoice-create-notifier/invoice-create-notifier";
|
||||||
|
import { hashInvoiceFilename } from "../../../modules/invoices/invoice-file-name/hash-invoice-filename";
|
||||||
|
import { resolveTempPdfFileLocation } from "../../../modules/invoices/invoice-file-name/resolve-temp-pdf-file-location";
|
||||||
|
import { MicroinvoiceInvoiceGenerator } from "../../../modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator";
|
||||||
import {
|
import {
|
||||||
InvoiceNumberGenerationStrategy,
|
InvoiceNumberGenerationStrategy,
|
||||||
InvoiceNumberGenerator,
|
InvoiceNumberGenerator,
|
||||||
} from "../../../modules/invoices/invoice-number-generator/invoice-number-generator";
|
} from "../../../modules/invoices/invoice-number-generator/invoice-number-generator";
|
||||||
import { MicroinvoiceInvoiceGenerator } from "../../../modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator";
|
import { SaleorInvoiceUploader } from "../../../modules/invoices/invoice-uploader/saleor-invoice-uploader";
|
||||||
import { hashInvoiceFilename } from "../../../modules/invoices/invoice-file-name/hash-invoice-filename";
|
|
||||||
import { resolveTempPdfFileLocation } from "../../../modules/invoices/invoice-file-name/resolve-temp-pdf-file-location";
|
|
||||||
import { createGraphQLClient, createLogger } from "@saleor/apps-shared";
|
|
||||||
import { SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
|
|
||||||
import { GetAppConfigurationV2Service } from "../../../modules/app-configuration/schema-v2/get-app-configuration.v2.service";
|
|
||||||
import { ShopInfoFetcher } from "../../../modules/shop-info/shop-info-fetcher";
|
import { ShopInfoFetcher } from "../../../modules/shop-info/shop-info-fetcher";
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
AddressV2Schema,
|
|
||||||
AddressV2Shape,
|
|
||||||
} from "../../../modules/app-configuration/schema-v2/app-config-schema.v2";
|
|
||||||
import { ConfigV1ToV2MigrationService } from "../../../modules/app-configuration/schema-v2/config-v1-to-v2-migration.service";
|
|
||||||
import { shopInfoQueryToAddressShape } from "../../../modules/shop-info/shop-info-query-to-address-shape";
|
import { shopInfoQueryToAddressShape } from "../../../modules/shop-info/shop-info-query-to-address-shape";
|
||||||
|
import { saleorApp } from "../../../saleor-app";
|
||||||
|
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
import { AppConfigV2 } from "../../../modules/app-configuration/schema-v2/app-config";
|
import { AppConfigV2 } from "../../../modules/app-configuration/schema-v2/app-config";
|
||||||
|
@ -156,7 +151,7 @@ const invoiceNumberGenerator = new InvoiceNumberGenerator();
|
||||||
export const handler: NextWebhookApiHandler<InvoiceRequestedPayloadFragment> = async (
|
export const handler: NextWebhookApiHandler<InvoiceRequestedPayloadFragment> = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
context
|
context,
|
||||||
) => {
|
) => {
|
||||||
const { authData, payload, baseUrl } = context;
|
const { authData, payload, baseUrl } = context;
|
||||||
const logger = createLogger({ domain: authData.saleorApiUrl, url: baseUrl });
|
const logger = createLogger({ domain: authData.saleorApiUrl, url: baseUrl });
|
||||||
|
@ -176,7 +171,7 @@ export const handler: NextWebhookApiHandler<InvoiceRequestedPayloadFragment> = a
|
||||||
*/
|
*/
|
||||||
const invoiceName = invoiceNumberGenerator.generateFromOrder(
|
const invoiceName = invoiceNumberGenerator.generateFromOrder(
|
||||||
order as OrderPayloadFragment,
|
order as OrderPayloadFragment,
|
||||||
InvoiceNumberGenerationStrategy.localizedDate("en-US") // todo connect locale -> where from?
|
InvoiceNumberGenerationStrategy.localizedDate("en-US"), // todo connect locale -> where from?
|
||||||
);
|
);
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
Sentry.addBreadcrumb({
|
||||||
|
@ -270,7 +265,7 @@ export const handler: NextWebhookApiHandler<InvoiceRequestedPayloadFragment> = a
|
||||||
await new InvoiceCreateNotifier(client).notifyInvoiceCreated(
|
await new InvoiceCreateNotifier(client).notifyInvoiceCreated(
|
||||||
orderId,
|
orderId,
|
||||||
invoiceName,
|
invoiceName,
|
||||||
uploadedFileUrl
|
uploadedFileUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
Sentry.addBreadcrumb({
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||||
"@graphql-codegen/typescript-urql": "3.7.3",
|
"@graphql-codegen/typescript-urql": "3.7.3",
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
|
|
1
apps/klaviyo/reset.d.ts
vendored
Normal file
1
apps/klaviyo/reset.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "@total-typescript/ts-reset";
|
|
@ -56,13 +56,13 @@ export const customerCreatedWebhook = new SaleorAsyncWebhook<CustomerCreatedWebh
|
||||||
event: "CUSTOMER_CREATED",
|
event: "CUSTOMER_CREATED",
|
||||||
apl: saleorApp.apl,
|
apl: saleorApp.apl,
|
||||||
query: UntypedCustomerCreatedDocument,
|
query: UntypedCustomerCreatedDocument,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const handler: NextWebhookApiHandler<CustomerCreatedWebhookPayloadFragment> = async (
|
const handler: NextWebhookApiHandler<CustomerCreatedWebhookPayloadFragment> = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
context
|
context,
|
||||||
) => {
|
) => {
|
||||||
console.debug("customerCreatedWebhook handler called");
|
console.debug("customerCreatedWebhook handler called");
|
||||||
|
|
||||||
|
@ -94,7 +94,8 @@ const handler: NextWebhookApiHandler<CustomerCreatedWebhookPayloadFragment> = as
|
||||||
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
|
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
|
||||||
|
|
||||||
if (klaviyoResponse.status !== 200) {
|
if (klaviyoResponse.status !== 200) {
|
||||||
const klaviyoMessage = ` Message: ${(await klaviyoResponse.json())?.message}.` || "";
|
const klaviyoMessage =
|
||||||
|
` Message: ${((await klaviyoResponse.json()) as { message: string })?.message}.` || "";
|
||||||
|
|
||||||
console.debug("Klaviyo returned error: ", klaviyoMessage);
|
console.debug("Klaviyo returned error: ", klaviyoMessage);
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ export const fulfillmentCreatedWebhook =
|
||||||
const handler: NextWebhookApiHandler<FulfillmentCreatedWebhookPayloadFragment> = async (
|
const handler: NextWebhookApiHandler<FulfillmentCreatedWebhookPayloadFragment> = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
context
|
context,
|
||||||
) => {
|
) => {
|
||||||
console.debug("fulfillmentCreatedWebhook handler called");
|
console.debug("fulfillmentCreatedWebhook handler called");
|
||||||
|
|
||||||
|
@ -98,7 +98,8 @@ const handler: NextWebhookApiHandler<FulfillmentCreatedWebhookPayloadFragment> =
|
||||||
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
|
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
|
||||||
|
|
||||||
if (klaviyoResponse.status !== 200) {
|
if (klaviyoResponse.status !== 200) {
|
||||||
const klaviyoMessage = ` Message: ${(await klaviyoResponse.json())?.message}.` || "";
|
const klaviyoMessage =
|
||||||
|
` Message: ${((await klaviyoResponse.json()) as { message: string })?.message}.` || "";
|
||||||
|
|
||||||
console.debug("Klaviyo returned error: ", klaviyoMessage);
|
console.debug("Klaviyo returned error: ", klaviyoMessage);
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderCreatedWebhookPay
|
||||||
const handler: NextWebhookApiHandler<OrderCreatedWebhookPayloadFragment> = async (
|
const handler: NextWebhookApiHandler<OrderCreatedWebhookPayloadFragment> = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
context
|
context,
|
||||||
) => {
|
) => {
|
||||||
console.debug("orderCreatedWebhook handler called");
|
console.debug("orderCreatedWebhook handler called");
|
||||||
|
|
||||||
|
@ -69,7 +69,8 @@ const handler: NextWebhookApiHandler<OrderCreatedWebhookPayloadFragment> = async
|
||||||
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
|
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
|
||||||
|
|
||||||
if (klaviyoResponse.status !== 200) {
|
if (klaviyoResponse.status !== 200) {
|
||||||
const klaviyoMessage = ` Message: ${(await klaviyoResponse.json())?.message}.` || "";
|
const klaviyoMessage =
|
||||||
|
` Message: ${((await klaviyoResponse.json()) as { message: string })?.message}.` || "";
|
||||||
|
|
||||||
console.debug("Klaviyo returned error: ", klaviyoMessage);
|
console.debug("Klaviyo returned error: ", klaviyoMessage);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const orderFullyPaidWebhook = new SaleorAsyncWebhook<OrderFullyPaidWebhoo
|
||||||
const handler: NextWebhookApiHandler<OrderFullyPaidWebhookPayloadFragment> = async (
|
const handler: NextWebhookApiHandler<OrderFullyPaidWebhookPayloadFragment> = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
context
|
context,
|
||||||
) => {
|
) => {
|
||||||
console.debug("orderFullyPaidWebhook handler called");
|
console.debug("orderFullyPaidWebhook handler called");
|
||||||
|
|
||||||
|
@ -70,7 +70,8 @@ const handler: NextWebhookApiHandler<OrderFullyPaidWebhookPayloadFragment> = asy
|
||||||
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
|
const klaviyoResponse = await klaviyoClient.send(klaviyoMetric, userEmail, payload);
|
||||||
|
|
||||||
if (klaviyoResponse.status !== 200) {
|
if (klaviyoResponse.status !== 200) {
|
||||||
const klaviyoMessage = ` Message: ${(await klaviyoResponse.json())?.message}.` || "";
|
const klaviyoMessage =
|
||||||
|
` Message: ${((await klaviyoResponse.json()) as { message: string })?.message}.` || "";
|
||||||
|
|
||||||
console.debug("Klaviyo returned error: ", klaviyoMessage);
|
console.debug("Klaviyo returned error: ", klaviyoMessage);
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"eslint": "8.46.0",
|
"eslint": "8.46.0",
|
||||||
|
|
1
apps/products-feed/reset.d.ts
vendored
Normal file
1
apps/products-feed/reset.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "@total-typescript/ts-reset";
|
|
@ -94,7 +94,6 @@ describe("AppConfig", function () {
|
||||||
expect(
|
expect(
|
||||||
() =>
|
() =>
|
||||||
new AppConfig({
|
new AppConfig({
|
||||||
// @ts-expect-error
|
|
||||||
foo: "bar",
|
foo: "bar",
|
||||||
}),
|
}),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
|
|
|
@ -72,7 +72,7 @@ export class AppConfig {
|
||||||
imageSize: imageSizeFieldSchema.parse(undefined),
|
imageSize: imageSizeFieldSchema.parse(undefined),
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(initialData?: RootConfig) {
|
constructor(initialData?: RootConfig | unknown) {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
try {
|
try {
|
||||||
this.rootData = rootAppConfigSchema.parse(initialData);
|
this.rootData = rootAppConfigSchema.parse(initialData);
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export class CacheConfigurator {
|
export class CacheConfigurator {
|
||||||
private metadataKeyPrefix = "cursor-cache-";
|
private metadataKeyPrefix = "cursor-cache-";
|
||||||
|
|
||||||
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
|
constructor(
|
||||||
|
private metadataManager: SettingsManager,
|
||||||
|
private saleorApiUrl: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
private constructKey(channel: string) {
|
private constructKey(channel: string) {
|
||||||
return this.metadataKeyPrefix + channel;
|
return this.metadataKeyPrefix + channel;
|
||||||
|
@ -16,7 +20,7 @@ export class CacheConfigurator {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(data);
|
return z.array(z.string()).parse(JSON.parse(data));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error("Invalid metadata value, can't be parsed");
|
throw new Error("Invalid metadata value, can't be parsed");
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||||
"@graphql-codegen/typescript-urql": "3.7.3",
|
"@graphql-codegen/typescript-urql": "3.7.3",
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"@vitejs/plugin-react": "4.0.4",
|
"@vitejs/plugin-react": "4.0.4",
|
||||||
|
|
1
apps/search/reset.d.ts
vendored
Normal file
1
apps/search/reset.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "@total-typescript/ts-reset";
|
|
@ -7,7 +7,9 @@ export const useWebhooksStatus = () => {
|
||||||
const fetch: typeof window.fetch = useAuthenticatedFetch();
|
const fetch: typeof window.fetch = useAuthenticatedFetch();
|
||||||
|
|
||||||
const fetchFn = useCallback(() => {
|
const fetchFn = useCallback(() => {
|
||||||
return fetch("/api/webhooks-status").then((resp) => resp.json());
|
return fetch("/api/webhooks-status").then(
|
||||||
|
(resp) => resp.json() as unknown as WebhooksStatusResponse,
|
||||||
|
);
|
||||||
/**
|
/**
|
||||||
* fetch from SDK is not wrapped with memo todo
|
* fetch from SDK is not wrapped with memo todo
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -27,7 +27,7 @@ export class AppConfig {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(initialData?: AppConfigRootSchemaFields) {
|
constructor(initialData?: AppConfigRootSchemaFields | unknown) {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
this.rootData = AppConfigRootSchema.parse(initialData);
|
this.rootData = AppConfigRootSchema.parse(initialData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { NextProtectedApiHandler } from "@saleor/app-sdk/handlers/next";
|
import { NextProtectedApiHandler, ProtectedHandlerContext } from "@saleor/app-sdk/handlers/next";
|
||||||
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { Client, OperationResult } from "urql";
|
import { Client, OperationResult } from "urql";
|
||||||
|
@ -8,11 +8,12 @@ import { IWebhookActivityTogglerService } from "../../domain/WebhookActivityTogg
|
||||||
import { SearchProvider } from "../../lib/searchProvider";
|
import { SearchProvider } from "../../lib/searchProvider";
|
||||||
import { webhooksStatusHandlerFactory } from "../../pages/api/webhooks-status";
|
import { webhooksStatusHandlerFactory } from "../../pages/api/webhooks-status";
|
||||||
import { AppConfig } from "../../modules/configuration/configuration";
|
import { AppConfig } from "../../modules/configuration/configuration";
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context provided from ProtectedApiHandler to handler body
|
* Context provided from ProtectedApiHandler to handler body
|
||||||
*/
|
*/
|
||||||
const mockWebhookContext = {
|
const mockWebhookContext: ProtectedHandlerContext = {
|
||||||
authData: {
|
authData: {
|
||||||
appId: "app-id",
|
appId: "app-id",
|
||||||
domain: "domain.saleor.io",
|
domain: "domain.saleor.io",
|
||||||
|
@ -20,6 +21,10 @@ const mockWebhookContext = {
|
||||||
saleorApiUrl: "https://domain.saleor.io/graphql",
|
saleorApiUrl: "https://domain.saleor.io/graphql",
|
||||||
},
|
},
|
||||||
baseUrl: "localhost:3000",
|
baseUrl: "localhost:3000",
|
||||||
|
user: {
|
||||||
|
email: "",
|
||||||
|
userPermissions: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const appWebhooksResponseData: Pick<OperationResult<FetchOwnWebhooksQuery, any>, "data"> = {
|
const appWebhooksResponseData: Pick<OperationResult<FetchOwnWebhooksQuery, any>, "data"> = {
|
||||||
|
@ -90,6 +95,7 @@ describe("webhooksStatusHandler", () => {
|
||||||
it("Disables webhooks if Algolia settings are not saved in Saleor Metadata", async function () {
|
it("Disables webhooks if Algolia settings are not saved in Saleor Metadata", async function () {
|
||||||
const { req, res } = createMocks({});
|
const { req, res } = createMocks({});
|
||||||
|
|
||||||
|
// @ts-expect-error - mock doesnt contain next-specific fields
|
||||||
await handler(req, res, mockWebhookContext);
|
await handler(req, res, mockWebhookContext);
|
||||||
|
|
||||||
expect(webhooksTogglerServiceMock.disableOwnWebhooks).toHaveBeenCalled();
|
expect(webhooksTogglerServiceMock.disableOwnWebhooks).toHaveBeenCalled();
|
||||||
|
@ -113,6 +119,7 @@ describe("webhooksStatusHandler", () => {
|
||||||
|
|
||||||
const { req, res } = createMocks({});
|
const { req, res } = createMocks({});
|
||||||
|
|
||||||
|
// @ts-expect-error - mock doesnt contain next-specific fields
|
||||||
await handler(req, res, mockWebhookContext);
|
await handler(req, res, mockWebhookContext);
|
||||||
|
|
||||||
expect(webhooksTogglerServiceMock.disableOwnWebhooks).toHaveBeenCalled();
|
expect(webhooksTogglerServiceMock.disableOwnWebhooks).toHaveBeenCalled();
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"eslint": "8.46.0",
|
"eslint": "8.46.0",
|
||||||
|
|
1
apps/segment/reset.d.ts
vendored
Normal file
1
apps/segment/reset.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "@total-typescript/ts-reset";
|
|
@ -4,7 +4,7 @@ import { RootConfig } from "./schemas/root-config.schema";
|
||||||
export class AppConfig {
|
export class AppConfig {
|
||||||
private rootData: RootConfig.Shape = null;
|
private rootData: RootConfig.Shape = null;
|
||||||
|
|
||||||
constructor(initialData?: RootConfig.Shape) {
|
constructor(initialData?: RootConfig.Shape | unknown) {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
this.rootData = RootConfig.Schema.parse(initialData);
|
this.rootData = RootConfig.Schema.parse(initialData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||||
"@graphql-codegen/typescript-urql": "3.7.3",
|
"@graphql-codegen/typescript-urql": "3.7.3",
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
||||||
|
|
1
apps/slack/reset.d.ts
vendored
Normal file
1
apps/slack/reset.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "@total-typescript/ts-reset";
|
|
@ -40,7 +40,7 @@ export const useAppApi = <D>({ url, options, skip }: UseFetchProps) => {
|
||||||
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
setData(json);
|
setData(json as D);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e as unknown);
|
setError(e as unknown);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
"@graphql-typed-document-node/core": "3.2.0",
|
"@graphql-typed-document-node/core": "3.2.0",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"@vitejs/plugin-react": "4.0.4",
|
"@vitejs/plugin-react": "4.0.4",
|
||||||
|
|
1
apps/taxes/reset.d.ts
vendored
Normal file
1
apps/taxes/reset.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import "@total-typescript/ts-reset";
|
|
@ -10,6 +10,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.35.1",
|
"@playwright/test": "^1.35.1",
|
||||||
"@saleor/app-sdk": "0.43.1",
|
"@saleor/app-sdk": "0.43.1",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint-config-saleor": "workspace:*",
|
"eslint-config-saleor": "workspace:*",
|
||||||
"zod": "3.21.4"
|
"zod": "3.21.4"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "eslint-config-saleor",
|
"name": "eslint-config-saleor",
|
||||||
"version": "0.4.5",
|
"version": "0.4.5",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"eslint": "8.46.0",
|
"eslint": "8.46.0",
|
||||||
"eslint-config-next": "13.4.8",
|
"eslint-config-next": "13.4.8",
|
||||||
"eslint-config-prettier": "8.8.0",
|
"eslint-config-prettier": "8.8.0",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"@storybook/react": "^7.0.12",
|
"@storybook/react": "^7.0.12",
|
||||||
"@storybook/react-vite": "^7.0.12",
|
"@storybook/react-vite": "^7.0.12",
|
||||||
"@storybook/testing-library": "^0.0.14-next.2",
|
"@storybook/testing-library": "^0.0.14-next.2",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"@vanilla-extract/vite-plugin": "^3.8.1",
|
"@vanilla-extract/vite-plugin": "^3.8.1",
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"@material-ui/lab": "4.0.0-alpha.61",
|
"@material-ui/lab": "4.0.0-alpha.61",
|
||||||
"@saleor/app-sdk": "0.43.1",
|
"@saleor/app-sdk": "0.43.1",
|
||||||
"@saleor/macaw-ui": "0.8.0-pre.127",
|
"@saleor/macaw-ui": "0.8.0-pre.127",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"@types/semver": "^7.5.0",
|
"@types/semver": "^7.5.0",
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@saleor/app-sdk": "0.41.1",
|
"@saleor/app-sdk": "0.41.1",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"eslint": "8.46.0",
|
"eslint": "8.46.0",
|
||||||
"eslint-config-saleor": "workspace:*",
|
"eslint-config-saleor": "workspace:*",
|
||||||
"next": "13.4.8",
|
"next": "13.4.8",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@saleor/app-sdk": "0.43.1",
|
"@saleor/app-sdk": "0.43.1",
|
||||||
"@saleor/macaw-ui": "0.8.0-pre.127",
|
"@saleor/macaw-ui": "0.8.0-pre.127",
|
||||||
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/react": "18.2.5",
|
"@types/react": "18.2.5",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"eslint-config-saleor": "workspace:*",
|
"eslint-config-saleor": "workspace:*",
|
||||||
|
|
|
@ -176,6 +176,9 @@ importers:
|
||||||
'@testing-library/react-hooks':
|
'@testing-library/react-hooks':
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/qs':
|
'@types/qs':
|
||||||
specifier: ^6.9.7
|
specifier: ^6.9.7
|
||||||
version: 6.9.7
|
version: 6.9.7
|
||||||
|
@ -312,6 +315,9 @@ importers:
|
||||||
'@testing-library/react-hooks':
|
'@testing-library/react-hooks':
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/mailchimp__mailchimp_marketing':
|
'@types/mailchimp__mailchimp_marketing':
|
||||||
specifier: ^3.0.7
|
specifier: ^3.0.7
|
||||||
version: 3.0.7
|
version: 3.0.7
|
||||||
|
@ -442,6 +448,9 @@ importers:
|
||||||
'@testing-library/react-hooks':
|
'@testing-library/react-hooks':
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/dot-object':
|
'@types/dot-object':
|
||||||
specifier: ^2.1.2
|
specifier: ^2.1.2
|
||||||
version: 2.1.2
|
version: 2.1.2
|
||||||
|
@ -605,6 +614,9 @@ importers:
|
||||||
'@testing-library/react-hooks':
|
'@testing-library/react-hooks':
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/html-to-text':
|
'@types/html-to-text':
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.0.0
|
version: 9.0.0
|
||||||
|
@ -741,6 +753,9 @@ importers:
|
||||||
'@graphql-typed-document-node/core':
|
'@graphql-typed-document-node/core':
|
||||||
specifier: 3.2.0
|
specifier: 3.2.0
|
||||||
version: 3.2.0(graphql@16.7.1)
|
version: 3.2.0(graphql@16.7.1)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.5
|
specifier: 18.2.5
|
||||||
version: 18.2.5
|
version: 18.2.5
|
||||||
|
@ -868,6 +883,9 @@ importers:
|
||||||
'@graphql-typed-document-node/core':
|
'@graphql-typed-document-node/core':
|
||||||
specifier: 3.2.0
|
specifier: 3.2.0
|
||||||
version: 3.2.0(graphql@16.7.1)
|
version: 3.2.0(graphql@16.7.1)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.5
|
specifier: 18.2.5
|
||||||
version: 18.2.5
|
version: 18.2.5
|
||||||
|
@ -1025,6 +1043,9 @@ importers:
|
||||||
'@testing-library/react-hooks':
|
'@testing-library/react-hooks':
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.5
|
specifier: 18.2.5
|
||||||
version: 18.2.5
|
version: 18.2.5
|
||||||
|
@ -1158,6 +1179,9 @@ importers:
|
||||||
'@graphql-typed-document-node/core':
|
'@graphql-typed-document-node/core':
|
||||||
specifier: 3.2.0
|
specifier: 3.2.0
|
||||||
version: 3.2.0(graphql@16.7.1)
|
version: 3.2.0(graphql@16.7.1)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.5
|
specifier: 18.2.5
|
||||||
version: 18.2.5
|
version: 18.2.5
|
||||||
|
@ -1309,6 +1333,9 @@ importers:
|
||||||
'@testing-library/react-hooks':
|
'@testing-library/react-hooks':
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.5
|
specifier: 18.2.5
|
||||||
version: 18.2.5
|
version: 18.2.5
|
||||||
|
@ -1427,6 +1454,9 @@ importers:
|
||||||
'@graphql-typed-document-node/core':
|
'@graphql-typed-document-node/core':
|
||||||
specifier: 3.2.0
|
specifier: 3.2.0
|
||||||
version: 3.2.0(graphql@16.7.1)
|
version: 3.2.0(graphql@16.7.1)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.5
|
specifier: 18.2.5
|
||||||
version: 18.2.5
|
version: 18.2.5
|
||||||
|
@ -1584,6 +1614,9 @@ importers:
|
||||||
'@testing-library/react-hooks':
|
'@testing-library/react-hooks':
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.5
|
specifier: 18.2.5
|
||||||
version: 18.2.5
|
version: 18.2.5
|
||||||
|
@ -1611,6 +1644,9 @@ importers:
|
||||||
'@saleor/app-sdk':
|
'@saleor/app-sdk':
|
||||||
specifier: 0.43.1
|
specifier: 0.43.1
|
||||||
version: 0.43.1(graphql@16.7.1)(next@13.4.8)(react-dom@18.2.0)(react@18.2.0)
|
version: 0.43.1(graphql@16.7.1)(next@13.4.8)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.3.1
|
specifier: ^16.3.1
|
||||||
version: 16.3.1
|
version: 16.3.1
|
||||||
|
@ -1623,6 +1659,9 @@ importers:
|
||||||
|
|
||||||
packages/eslint-config-saleor:
|
packages/eslint-config-saleor:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 8.46.0
|
specifier: 8.46.0
|
||||||
version: 8.46.0
|
version: 8.46.0
|
||||||
|
@ -1681,6 +1720,9 @@ importers:
|
||||||
'@storybook/testing-library':
|
'@storybook/testing-library':
|
||||||
specifier: ^0.0.14-next.2
|
specifier: ^0.0.14-next.2
|
||||||
version: 0.0.14-next.2
|
version: 0.0.14-next.2
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.5
|
specifier: 18.2.5
|
||||||
version: 18.2.5
|
version: 18.2.5
|
||||||
|
@ -1748,6 +1790,9 @@ importers:
|
||||||
'@saleor/macaw-ui':
|
'@saleor/macaw-ui':
|
||||||
specifier: 0.8.0-pre.127
|
specifier: 0.8.0-pre.127
|
||||||
version: 0.8.0-pre.127(@types/react-dom@18.2.5)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
version: 0.8.0-pre.127(@types/react-dom@18.2.5)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.5
|
specifier: 18.2.5
|
||||||
version: 18.2.5
|
version: 18.2.5
|
||||||
|
@ -1821,6 +1866,9 @@ importers:
|
||||||
'@saleor/app-sdk':
|
'@saleor/app-sdk':
|
||||||
specifier: 0.43.1
|
specifier: 0.43.1
|
||||||
version: 0.43.1(graphql@16.7.1)(next@13.4.8)(react-dom@18.2.0)(react@18.2.0)
|
version: 0.43.1(graphql@16.7.1)(next@13.4.8)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 8.46.0
|
specifier: 8.46.0
|
||||||
version: 8.46.0
|
version: 8.46.0
|
||||||
|
@ -1849,6 +1897,9 @@ importers:
|
||||||
'@saleor/macaw-ui':
|
'@saleor/macaw-ui':
|
||||||
specifier: 0.8.0-pre.127
|
specifier: 0.8.0-pre.127
|
||||||
version: 0.8.0-pre.127(@types/react-dom@18.2.5)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
version: 0.8.0-pre.127(@types/react-dom@18.2.5)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 18.2.5
|
specifier: 18.2.5
|
||||||
version: 18.2.5
|
version: 18.2.5
|
||||||
|
@ -10675,6 +10726,10 @@ packages:
|
||||||
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
|
/@total-typescript/ts-reset@0.5.1:
|
||||||
|
resolution: {integrity: sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@trpc/client@10.34.0(@trpc/server@10.34.0):
|
/@trpc/client@10.34.0(@trpc/server@10.34.0):
|
||||||
resolution: {integrity: sha512-nqtDTIqSY/9syo2EjSy4WWWXPU9GsamEh9Tsg698gLAh1nhgFc5+/YYeb+Ne1pbvWGZ5/3t9Dcz3h4wMyyJ9gQ==}
|
resolution: {integrity: sha512-nqtDTIqSY/9syo2EjSy4WWWXPU9GsamEh9Tsg698gLAh1nhgFc5+/YYeb+Ne1pbvWGZ5/3t9Dcz3h4wMyyJ9gQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
Loading…
Reference in a new issue