Webhook helpers (#949)
* WIP * Added script and making implementation more roboust * Added rollback on issues with the migration * Cleanup the code * Use allSettled instead of all * Do not check spelling in schema files. Schema is pulled from the API and is not controlled by our team * Update the pkg json * Fix typo on log message * Add dedupe to the ignored words. Its used by codegen * Add changesets
This commit is contained in:
parent
a32fe7caf4
commit
7e0755ec9e
49 changed files with 73253 additions and 917 deletions
5
.changeset/orange-actors-eat.md
Normal file
5
.changeset/orange-actors-eat.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-search": patch
|
||||
---
|
||||
|
||||
Webhook migration scripts has been moved to the shared package.
|
5
.changeset/silver-windows-accept.md
Normal file
5
.changeset/silver-windows-accept.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@saleor/webhook-utils": patch
|
||||
---
|
||||
|
||||
Introduced a new shared package with helpers used for webhook management.
|
|
@ -6,7 +6,12 @@ const isSentryPropertiesInEnvironment =
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
|
||||
transpilePackages: [
|
||||
"@saleor/apps-shared",
|
||||
"@saleor/apps-ui",
|
||||
"@saleor/webhook-utils",
|
||||
"@saleor/react-hook-form-macaw",
|
||||
],
|
||||
};
|
||||
|
||||
const configWithSentry = withSentryConfig(
|
||||
|
@ -22,7 +27,7 @@ const configWithSentry = withSentryConfig(
|
|||
tunnelRoute: "/monitoring",
|
||||
hideSourceMaps: true,
|
||||
disableLogger: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = isSentryPropertiesInEnvironment ? configWithSentry : nextConfig;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"@saleor/apps-ui": "workspace:*",
|
||||
"@saleor/macaw-ui": "0.8.0-pre.127",
|
||||
"@saleor/react-hook-form-macaw": "workspace:*",
|
||||
"@saleor/webhook-utils": "workspace:*",
|
||||
"@sentry/nextjs": "7.67.0",
|
||||
"@tanstack/react-query": "4.29.19",
|
||||
"@trpc/client": "10.38.1",
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
|
||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||
import { WebhookActivityTogglerService } from "../../src/domain/WebhookActivityToggler.service";
|
||||
import { FetchOwnWebhooksDocument } from "../../generated/graphql";
|
||||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
|
||||
export const recreateWebhooks = async ({
|
||||
authData,
|
||||
dryRun,
|
||||
}: {
|
||||
authData: AuthData;
|
||||
dryRun: boolean;
|
||||
}) => {
|
||||
console.log("Working on env: ", authData.saleorApiUrl);
|
||||
|
||||
const client = createGraphQLClient({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
token: authData.token,
|
||||
});
|
||||
const webhooks = await client
|
||||
.query(FetchOwnWebhooksDocument, {
|
||||
id: authData.appId,
|
||||
})
|
||||
.toPromise()
|
||||
.then((r) => r.data?.app?.webhooks);
|
||||
|
||||
if (!webhooks?.length) {
|
||||
console.error("The environment does not have any webhooks, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use currently existing webhook data to determine a proper baseUrl and enabled state
|
||||
const targetUrl = webhooks[0].targetUrl;
|
||||
const enabled = webhooks[0].isActive;
|
||||
|
||||
const baseUrl = new URL(targetUrl).origin;
|
||||
|
||||
if (dryRun) {
|
||||
console.log("Necessary data gathered, skipping recreation of webhooks due to dry run mode");
|
||||
return;
|
||||
}
|
||||
|
||||
const webhookService = new WebhookActivityTogglerService(authData.appId, client);
|
||||
|
||||
try {
|
||||
await webhookService.recreateOwnWebhooks({ baseUrl, enableWebhooks: enabled });
|
||||
console.log("✅ Webhooks recreated successfully");
|
||||
} catch (e) {
|
||||
console.error("🛑 Failed to recreate webhooks: ", e);
|
||||
}
|
||||
};
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import * as dotenv from "dotenv";
|
||||
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
|
||||
import { recreateWebhooks } from "./recreate-webhooks";
|
||||
import { updateWebhooksScript } from "./update-webhooks";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
|
@ -21,7 +21,7 @@ const runMigration = async () => {
|
|||
});
|
||||
|
||||
for (const env of allEnvs) {
|
||||
await recreateWebhooks({ authData: env, dryRun: true });
|
||||
await updateWebhooksScript({ authData: env, dryRun: true });
|
||||
}
|
||||
|
||||
console.log("Migration dry run complete");
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import * as dotenv from "dotenv";
|
||||
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
|
||||
import { recreateWebhooks } from "./recreate-webhooks";
|
||||
import { updateWebhooksScript } from "./update-webhooks";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
|
@ -21,7 +21,7 @@ const runMigration = async () => {
|
|||
});
|
||||
|
||||
for (const env of allEnvs) {
|
||||
await recreateWebhooks({ authData: env, dryRun: false });
|
||||
await updateWebhooksScript({ authData: env, dryRun: false });
|
||||
}
|
||||
|
||||
console.log("Migration complete");
|
||||
|
|
47
apps/search/scripts/migrations/update-webhooks.ts
Normal file
47
apps/search/scripts/migrations/update-webhooks.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
|
||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
import { webhookMigrationRunner } from "@saleor/webhook-utils";
|
||||
import { appWebhooks } from "../../webhooks";
|
||||
|
||||
export const updateWebhooksScript = async ({
|
||||
authData,
|
||||
dryRun,
|
||||
}: {
|
||||
authData: AuthData;
|
||||
dryRun: boolean;
|
||||
}) => {
|
||||
console.log("Working on env: ", authData.saleorApiUrl);
|
||||
|
||||
const client = createGraphQLClient({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
token: authData.token,
|
||||
});
|
||||
|
||||
await webhookMigrationRunner({
|
||||
client,
|
||||
dryRun,
|
||||
getManifests: async ({ appDetails }) => {
|
||||
const webhooks = appDetails.webhooks;
|
||||
|
||||
if (!webhooks?.length) {
|
||||
console.info("The environment does not have any webhooks, skipping");
|
||||
return [];
|
||||
}
|
||||
|
||||
// All webhooks in this application are turned on or off. If any of them is enabled, we enable all of them.
|
||||
const enabled = webhooks.some((w) => w.isActive);
|
||||
|
||||
const targetUrl = appDetails.appUrl;
|
||||
|
||||
if (!targetUrl?.length) {
|
||||
throw new Error("App has no defined appUrl, skipping");
|
||||
}
|
||||
|
||||
const baseUrl = new URL(targetUrl).origin;
|
||||
|
||||
return appWebhooks.map((w) => ({ ...w.getWebhookManifest(baseUrl), enabled }));
|
||||
},
|
||||
});
|
||||
};
|
|
@ -68,7 +68,8 @@
|
|||
"urql",
|
||||
"Protos",
|
||||
"pino",
|
||||
"IFRAME"
|
||||
"IFRAME",
|
||||
"dedupe"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"node_modules",
|
||||
|
@ -80,6 +81,7 @@
|
|||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/graphql.ts",
|
||||
"**/CHANGELOG.md"
|
||||
"**/CHANGELOG.md",
|
||||
"**/schema.graphql"
|
||||
]
|
||||
}
|
||||
|
|
12
package.json
12
package.json
|
@ -39,15 +39,15 @@
|
|||
"*.{ts,tsx,md,js,jsx}": "cspell --no-must-find-files"
|
||||
},
|
||||
"packageManager": "pnpm@8.7.4",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@saleor/app-sdk": "0.43.1"
|
||||
}
|
||||
},
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*",
|
||||
"templates/*"
|
||||
],
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@saleor/app-sdk": "0.43.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
4
packages/webhook-utils/.eslintrc
Normal file
4
packages/webhook-utils/.eslintrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": ["saleor"]
|
||||
}
|
17
packages/webhook-utils/.graphqlrc.yml
Normal file
17
packages/webhook-utils/.graphqlrc.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
schema: graphql/schema.graphql
|
||||
documents: [graphql/**/*.graphql, src/**/*.ts, src/**/*.tsx]
|
||||
extensions:
|
||||
codegen:
|
||||
overwrite: true
|
||||
generates:
|
||||
generated/graphql.ts:
|
||||
config:
|
||||
dedupeFragments: true
|
||||
dedupeOperationSuffix: true
|
||||
plugins:
|
||||
- typescript
|
||||
- typescript-operations
|
||||
- typed-document-node
|
||||
generated/schema.graphql:
|
||||
plugins:
|
||||
- schema-ast
|
22742
packages/webhook-utils/generated/graphql.ts
Normal file
22742
packages/webhook-utils/generated/graphql.ts
Normal file
File diff suppressed because it is too large
Load diff
24215
packages/webhook-utils/generated/schema.graphql
Normal file
24215
packages/webhook-utils/generated/schema.graphql
Normal file
File diff suppressed because it is too large
Load diff
16
packages/webhook-utils/graphql/fragments/AppDetails.graphql
Normal file
16
packages/webhook-utils/graphql/fragments/AppDetails.graphql
Normal file
|
@ -0,0 +1,16 @@
|
|||
fragment AppDetailsFragment on App {
|
||||
id
|
||||
appUrl
|
||||
name
|
||||
webhooks{
|
||||
...WebhookDetailsFragment
|
||||
}
|
||||
metadata{
|
||||
key
|
||||
value
|
||||
}
|
||||
privateMetadata{
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
fragment ChannelDetailsFragment on Channel {
|
||||
id
|
||||
slug
|
||||
name
|
||||
currencyCode
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
fragment ShopDetailsFragment on Shop {
|
||||
version
|
||||
domain {
|
||||
url
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
fragment WebhookDetailsFragment on Webhook {
|
||||
id
|
||||
isActive
|
||||
name
|
||||
targetUrl
|
||||
subscriptionQuery
|
||||
syncEvents{
|
||||
name
|
||||
eventType
|
||||
}
|
||||
asyncEvents {
|
||||
name
|
||||
eventType
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
mutation CreateAppWebhook($input: WebhookCreateInput!) {
|
||||
webhookCreate(input:$input){
|
||||
webhook{
|
||||
...WebhookDetailsFragment
|
||||
}
|
||||
errors{
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
mutation ModifyAppWebhook($id: ID!, $input: WebhookUpdateInput!) {
|
||||
webhookUpdate(id: $id, input: $input) {
|
||||
errors {
|
||||
message
|
||||
}
|
||||
webhook {
|
||||
...WebhookDetailsFragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
mutation RemoveAppWebhook($id: ID!) {
|
||||
webhookDelete(id: $id){
|
||||
errors{
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
query GetAppDetailsAndWebhooksData {
|
||||
app{
|
||||
...AppDetailsFragment
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
query GetSaleorInstanceData {
|
||||
shop{
|
||||
...ShopDetailsFragment
|
||||
}
|
||||
channels{
|
||||
...ChannelDetailsFragment
|
||||
}
|
||||
}
|
24129
packages/webhook-utils/graphql/schema.graphql
Normal file
24129
packages/webhook-utils/graphql/schema.graphql
Normal file
File diff suppressed because it is too large
Load diff
5
packages/webhook-utils/index.ts
Normal file
5
packages/webhook-utils/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export * from "./src/operations/create-app-webhook";
|
||||
export * from "./src/operations/modify-app-webhook";
|
||||
export * from "./src/operations/remove-app-webhook";
|
||||
export * from "./src/operations/get-app-details-and-webhooks-data";
|
||||
export * from "./src/webhook-migration-runner";
|
46
packages/webhook-utils/package.json
Normal file
46
packages/webhook-utils/package.json
Normal file
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "@saleor/webhook-utils",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"fetch-schema": "curl https://raw.githubusercontent.com/saleor/saleor/${npm_package_saleor_schemaVersion}/saleor/graphql/schema.graphql > graphql/schema.graphql",
|
||||
"generate": "graphql-codegen",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@saleor/app-sdk": "0.41.1",
|
||||
"@saleor/apps-shared": "workspace:*",
|
||||
"@urql/exchange-auth": "^2.1.4",
|
||||
"eslint": "8.46.0",
|
||||
"graphql": "16.7.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"pino": "^8.14.1",
|
||||
"pino-pretty": "^10.0.0",
|
||||
"semver": "^7.5.1",
|
||||
"urql": "^4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "4.0.1",
|
||||
"@graphql-codegen/schema-ast": "4.0.0",
|
||||
"@graphql-codegen/typed-document-node": "5.0.1",
|
||||
"@graphql-codegen/typescript": "4.0.1",
|
||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||
"@graphql-typed-document-node/core": "3.2.0",
|
||||
"@saleor/app-sdk": "0.41.1",
|
||||
"@types/semver": "^7.5.0",
|
||||
"eslint-config-saleor": "workspace:*",
|
||||
"typescript": "5.1.6",
|
||||
"urql": "^4.0.4",
|
||||
"vite": "4.4.8",
|
||||
"vitest": "0.34.1"
|
||||
},
|
||||
"main": "index.ts",
|
||||
"peerDependencies": {
|
||||
"next": "13.3.0",
|
||||
"pino": "^8.14.1",
|
||||
"pino-pretty": "^10.0.0"
|
||||
},
|
||||
"saleor": {
|
||||
"schemaVersion": "3.7"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { Client } from "urql";
|
||||
import { WebhookEventTypeAsyncEnum, WebhookEventTypeSyncEnum } from "../generated/graphql";
|
||||
import { WebhookManifest } from "@saleor/app-sdk/types";
|
||||
import { createAppWebhook } from "./operations/create-app-webhook";
|
||||
|
||||
interface CreateAppWebhookFromManifestArgs {
|
||||
client: Client;
|
||||
webhookManifest: WebhookManifest;
|
||||
}
|
||||
|
||||
export const createAppWebhookFromManifest = async ({
|
||||
client,
|
||||
webhookManifest,
|
||||
}: CreateAppWebhookFromManifestArgs) => {
|
||||
return createAppWebhook({
|
||||
client,
|
||||
input: {
|
||||
asyncEvents: webhookManifest.asyncEvents as WebhookEventTypeAsyncEnum[],
|
||||
syncEvents: webhookManifest.syncEvents as WebhookEventTypeSyncEnum[],
|
||||
isActive: webhookManifest.isActive,
|
||||
name: webhookManifest.name,
|
||||
targetUrl: webhookManifest.targetUrl,
|
||||
query: webhookManifest.query,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import { Client } from "urql";
|
||||
import { WebhookDetailsFragment } from "../generated/graphql";
|
||||
import { createAppWebhook } from "./operations/create-app-webhook";
|
||||
|
||||
interface CreateAppWebhookFromWebhookDetailsFragmentArgs {
|
||||
client: Client;
|
||||
webhookDetails: WebhookDetailsFragment;
|
||||
}
|
||||
|
||||
export const createAppWebhookFromWebhookDetailsFragment = async ({
|
||||
client,
|
||||
webhookDetails,
|
||||
}: CreateAppWebhookFromWebhookDetailsFragmentArgs) => {
|
||||
return createAppWebhook({
|
||||
client,
|
||||
input: {
|
||||
asyncEvents: webhookDetails.asyncEvents.map((event) => event.eventType),
|
||||
syncEvents: webhookDetails.syncEvents.map((event) => event.eventType),
|
||||
isActive: webhookDetails.isActive,
|
||||
name: webhookDetails.name,
|
||||
targetUrl: webhookDetails.targetUrl,
|
||||
query: webhookDetails.subscriptionQuery,
|
||||
},
|
||||
});
|
||||
};
|
17
packages/webhook-utils/src/disable-webhook.ts
Normal file
17
packages/webhook-utils/src/disable-webhook.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Client } from "urql";
|
||||
import { modifyAppWebhook } from "./operations/modify-app-webhook";
|
||||
|
||||
interface DisableWebhookArgs {
|
||||
client: Client;
|
||||
webhookId: string;
|
||||
}
|
||||
|
||||
export const disableWebhook = async ({ client, webhookId }: DisableWebhookArgs) => {
|
||||
return modifyAppWebhook({
|
||||
client,
|
||||
webhookId,
|
||||
input: {
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
};
|
17
packages/webhook-utils/src/enable-webhook.ts
Normal file
17
packages/webhook-utils/src/enable-webhook.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Client } from "urql";
|
||||
import { modifyAppWebhook } from "./operations/modify-app-webhook";
|
||||
|
||||
interface enableWebhookArgs {
|
||||
client: Client;
|
||||
webhookId: string;
|
||||
}
|
||||
|
||||
export const enableWebhook = async ({ client, webhookId }: enableWebhookArgs) => {
|
||||
await modifyAppWebhook({
|
||||
client,
|
||||
webhookId,
|
||||
input: {
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,101 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { getWebhookIdsAndManifestsToUpdate } from "./get-webhook-ids-and-manifests-to-update";
|
||||
|
||||
describe("getWebhookIdsAndQueriesToUpdate", () => {
|
||||
it("Returns an empty list, when no data is passed", () => {
|
||||
expect(
|
||||
getWebhookIdsAndManifestsToUpdate({
|
||||
existingWebhooksPartial: [],
|
||||
newWebhookManifests: [],
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
it("Returns all of the entries, when new webhook manifests contain the same webhooks as existing list", () => {
|
||||
expect(
|
||||
getWebhookIdsAndManifestsToUpdate({
|
||||
existingWebhooksPartial: [
|
||||
{ id: "1", name: "webhook1" },
|
||||
{ id: "2", name: "webhook2" },
|
||||
],
|
||||
newWebhookManifests: [
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook1",
|
||||
query: "newQuery1",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook2",
|
||||
query: "newQuery2",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toStrictEqual([
|
||||
{
|
||||
webhookId: "1",
|
||||
webhookManifest: {
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook1",
|
||||
query: "newQuery1",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
webhookId: "2",
|
||||
webhookManifest: {
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook2",
|
||||
query: "newQuery2",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("Returns subset of entries, when existing webhook list contain some of them", () => {
|
||||
expect(
|
||||
getWebhookIdsAndManifestsToUpdate({
|
||||
existingWebhooksPartial: [{ id: "1", name: "webhook1" }],
|
||||
newWebhookManifests: [
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook1",
|
||||
query: "newQuery1",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook2",
|
||||
query: "newQuery2",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toStrictEqual([
|
||||
{
|
||||
webhookId: "1",
|
||||
webhookManifest: {
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook1",
|
||||
query: "newQuery1",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import { WebhookManifest } from "@saleor/app-sdk/types";
|
||||
|
||||
type WebhookPartial = { id: string; name: string };
|
||||
|
||||
interface GetWebhookIdsAndQueriesToUpdateArgs<T extends WebhookPartial> {
|
||||
newWebhookManifests: Array<WebhookManifest>;
|
||||
existingWebhooksPartial: Array<T>;
|
||||
}
|
||||
|
||||
type ReturnType = { webhookId: string; webhookManifest: WebhookManifest };
|
||||
|
||||
// Couples the webhook id with the manifest to update
|
||||
export const getWebhookIdsAndManifestsToUpdate = <T extends WebhookPartial>({
|
||||
newWebhookManifests,
|
||||
existingWebhooksPartial,
|
||||
}: GetWebhookIdsAndQueriesToUpdateArgs<T>): Array<ReturnType> => {
|
||||
return newWebhookManifests
|
||||
.map((webhookManifest) => {
|
||||
const existingWebhook = existingWebhooksPartial.find(
|
||||
(webhook) => webhook.name === webhookManifest.name,
|
||||
);
|
||||
|
||||
if (!existingWebhook) {
|
||||
// Theres no webhook with this name, so we cant start an update
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!webhookManifest.query) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
webhookId: existingWebhook.id,
|
||||
webhookManifest,
|
||||
};
|
||||
})
|
||||
.filter((data): data is ReturnType => data !== undefined); // Filter out undefined values and narrow down the type
|
||||
};
|
117
packages/webhook-utils/src/filters/webhooks-to-add.test.ts
Normal file
117
packages/webhook-utils/src/filters/webhooks-to-add.test.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { webhooksToAdd } from "./webhooks-to-add";
|
||||
|
||||
describe("webhooksToAdd", () => {
|
||||
it("Returns an empty list, when no data is passed", () => {
|
||||
expect(
|
||||
webhooksToAdd({
|
||||
existingWebhooksPartial: [],
|
||||
newWebhookManifests: [],
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
it("Returns empty list, when new webhook manifests contain the same webhooks as existing list", () => {
|
||||
expect(
|
||||
webhooksToAdd({
|
||||
existingWebhooksPartial: [
|
||||
{ id: "1", name: "webhook1" },
|
||||
{ id: "1", name: "webhook2" },
|
||||
],
|
||||
newWebhookManifests: [
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook1",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook2",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
it("Returns all of the manifests, when existing webhook list is empty", () => {
|
||||
expect(
|
||||
webhooksToAdd({
|
||||
existingWebhooksPartial: [],
|
||||
newWebhookManifests: [
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook1",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook2",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toStrictEqual([
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook1",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook2",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("Returns list with the new webhook to add, when it was not specified in the existing manifests", () => {
|
||||
expect(
|
||||
webhooksToAdd({
|
||||
existingWebhooksPartial: [{ id: "1", name: "webhookOld" }],
|
||||
newWebhookManifests: [
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhookOld",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhookNew",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toStrictEqual([
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhookNew",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
18
packages/webhook-utils/src/filters/webhooks-to-add.ts
Normal file
18
packages/webhook-utils/src/filters/webhooks-to-add.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { WebhookManifest } from "@saleor/app-sdk/types";
|
||||
|
||||
interface WebhooksToAddArgs {
|
||||
newWebhookManifests: Array<WebhookManifest>;
|
||||
existingWebhooksPartial: Array<{ id: string; name: string }>;
|
||||
}
|
||||
|
||||
export const webhooksToAdd = ({
|
||||
newWebhookManifests,
|
||||
existingWebhooksPartial,
|
||||
}: WebhooksToAddArgs) => {
|
||||
return newWebhookManifests.filter(
|
||||
(newWebhookManifest) =>
|
||||
!existingWebhooksPartial.find(
|
||||
(existingWebhook) => newWebhookManifest.name === existingWebhook.name,
|
||||
),
|
||||
);
|
||||
};
|
|
@ -0,0 +1,75 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { webhooksToRemove } from "./webhooks-to-remove";
|
||||
|
||||
describe("webhooksToRemove", () => {
|
||||
it("Returns an empty list, when no data is passed", () => {
|
||||
expect(
|
||||
webhooksToRemove({
|
||||
existingWebhooksPartial: [],
|
||||
newWebhookManifests: [],
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
it("Returns empty list, when new webhook manifests contain the same webhooks as existing list", () => {
|
||||
expect(
|
||||
webhooksToRemove({
|
||||
existingWebhooksPartial: [
|
||||
{ id: "1", name: "webhook1" },
|
||||
{ id: "1", name: "webhook2" },
|
||||
],
|
||||
newWebhookManifests: [
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook1",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook2",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
it("Returns all of the existing webhooks, when new webhook manifests list is empty", () => {
|
||||
expect(
|
||||
webhooksToRemove({
|
||||
existingWebhooksPartial: [
|
||||
{ id: "1", name: "webhook1" },
|
||||
{ id: "2", name: "webhook2" },
|
||||
],
|
||||
newWebhookManifests: [],
|
||||
}),
|
||||
).toStrictEqual([
|
||||
{ id: "1", name: "webhook1" },
|
||||
{ id: "2", name: "webhook2" },
|
||||
]);
|
||||
});
|
||||
it("Returns list with the webhook one webhook to remove, when it was not specified in the new manifests", () => {
|
||||
expect(
|
||||
webhooksToRemove({
|
||||
existingWebhooksPartial: [
|
||||
{ id: "1", name: "webhook1" },
|
||||
{ id: "2", name: "webhook2" },
|
||||
],
|
||||
newWebhookManifests: [
|
||||
{
|
||||
asyncEvents: [],
|
||||
isActive: true,
|
||||
name: "webhook1",
|
||||
query: "",
|
||||
syncEvents: [],
|
||||
targetUrl: "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toStrictEqual([{ id: "2", name: "webhook2" }]);
|
||||
});
|
||||
});
|
22
packages/webhook-utils/src/filters/webhooks-to-remove.ts
Normal file
22
packages/webhook-utils/src/filters/webhooks-to-remove.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { WebhookManifest } from "@saleor/app-sdk/types";
|
||||
|
||||
type WebhookPartial = { id: string; name: string };
|
||||
|
||||
interface WebhooksToRemoveArgs<T extends WebhookPartial> {
|
||||
newWebhookManifests: Array<WebhookManifest>;
|
||||
existingWebhooksPartial: Array<T>;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns partials of the existing webhooks which are not specified in the manifests.
|
||||
* The comparison is based on webhook names.
|
||||
*/
|
||||
export const webhooksToRemove = <T extends WebhookPartial>({
|
||||
newWebhookManifests,
|
||||
existingWebhooksPartial,
|
||||
}: WebhooksToRemoveArgs<T>) => {
|
||||
return existingWebhooksPartial.filter(
|
||||
(existingWebhook) =>
|
||||
!newWebhookManifests.find((webhookManifest) => webhookManifest.name === existingWebhook.name),
|
||||
);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import { Client } from "urql";
|
||||
import { WebhookEventTypeAsyncEnum, WebhookEventTypeSyncEnum } from "../generated/graphql";
|
||||
import { WebhookManifest } from "@saleor/app-sdk/types";
|
||||
import { modifyAppWebhook } from "./operations/modify-app-webhook";
|
||||
|
||||
interface ModifyAppWebhookFromManifestArgs {
|
||||
client: Client;
|
||||
webhookId: string;
|
||||
webhookManifest: WebhookManifest;
|
||||
}
|
||||
|
||||
export const modifyAppWebhookFromManifest = async ({
|
||||
client,
|
||||
webhookId,
|
||||
webhookManifest,
|
||||
}: ModifyAppWebhookFromManifestArgs) => {
|
||||
return modifyAppWebhook({
|
||||
client,
|
||||
webhookId,
|
||||
input: {
|
||||
asyncEvents: webhookManifest.asyncEvents as WebhookEventTypeAsyncEnum[],
|
||||
syncEvents: webhookManifest.syncEvents as WebhookEventTypeSyncEnum[],
|
||||
isActive: webhookManifest.isActive,
|
||||
name: webhookManifest.name,
|
||||
targetUrl: webhookManifest.targetUrl,
|
||||
query: webhookManifest.query,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import { Client } from "urql";
|
||||
import { WebhookDetailsFragment } from "../generated/graphql";
|
||||
import { modifyAppWebhook } from "./operations/modify-app-webhook";
|
||||
|
||||
interface ModifyAppWebhookFromWebhookDetailsArgs {
|
||||
client: Client;
|
||||
webhookDetails: WebhookDetailsFragment;
|
||||
}
|
||||
|
||||
export const modifyAppWebhookFromWebhookDetails = async ({
|
||||
client,
|
||||
webhookDetails,
|
||||
}: ModifyAppWebhookFromWebhookDetailsArgs) => {
|
||||
return modifyAppWebhook({
|
||||
client,
|
||||
webhookId: webhookDetails.id,
|
||||
input: {
|
||||
asyncEvents: webhookDetails.asyncEvents.map((event) => event.eventType),
|
||||
syncEvents: webhookDetails.syncEvents.map((event) => event.eventType),
|
||||
isActive: webhookDetails.isActive,
|
||||
name: webhookDetails.name,
|
||||
targetUrl: webhookDetails.targetUrl,
|
||||
query: webhookDetails.subscriptionQuery,
|
||||
},
|
||||
});
|
||||
};
|
28
packages/webhook-utils/src/operations/create-app-webhook.ts
Normal file
28
packages/webhook-utils/src/operations/create-app-webhook.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Client } from "urql";
|
||||
import { CreateAppWebhookDocument, WebhookCreateInput } from "../../generated/graphql";
|
||||
|
||||
interface CreateAppWebhookArgs {
|
||||
client: Client;
|
||||
input: WebhookCreateInput;
|
||||
}
|
||||
|
||||
export const createAppWebhook = async ({ client, input }: CreateAppWebhookArgs) => {
|
||||
return client
|
||||
.mutation(CreateAppWebhookDocument, {
|
||||
input,
|
||||
})
|
||||
.toPromise()
|
||||
.then((r) => {
|
||||
if (r.error) {
|
||||
throw new Error(`Webhook creation failed. The API returned an error: ${r.error.message}`);
|
||||
}
|
||||
const webhook = r.data?.webhookCreate?.webhook;
|
||||
|
||||
if (!webhook) {
|
||||
throw new Error(
|
||||
"Webhook creation response is empty. The API returned no additional error.",
|
||||
);
|
||||
}
|
||||
return webhook;
|
||||
});
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { Client } from "urql";
|
||||
import { GetAppDetailsAndWebhooksDataDocument } from "../../generated/graphql";
|
||||
|
||||
interface GetAppWebhooksArgs {
|
||||
client: Client;
|
||||
}
|
||||
|
||||
export const getAppDetailsAndWebhooksData = async ({ client }: GetAppWebhooksArgs) => {
|
||||
return client
|
||||
.query(GetAppDetailsAndWebhooksDataDocument, {})
|
||||
.toPromise()
|
||||
.then((r) => {
|
||||
const app = r.data?.app;
|
||||
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
"Could not get the app details. Access token could be invalid or app is disabled.",
|
||||
);
|
||||
}
|
||||
return app;
|
||||
});
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
import { Client } from "urql";
|
||||
import {
|
||||
GetAppDetailsAndWebhooksDataDocument,
|
||||
GetSaleorInstanceDataDocument,
|
||||
} from "../../generated/graphql";
|
||||
|
||||
interface GetSaleorInstanceDetailsArgs {
|
||||
client: Client;
|
||||
}
|
||||
|
||||
export const getSaleorInstanceDetails = async ({ client }: GetSaleorInstanceDetailsArgs) => {
|
||||
return client
|
||||
.query(GetSaleorInstanceDataDocument, {})
|
||||
.toPromise()
|
||||
.then((r) => {
|
||||
if (r.error) {
|
||||
throw new Error(
|
||||
`Getting Saleor Instance details failed. The API returned an error: ${r.error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
shop: r.data?.shop,
|
||||
channels: r.data?.channels || [],
|
||||
};
|
||||
});
|
||||
};
|
29
packages/webhook-utils/src/operations/modify-app-webhook.ts
Normal file
29
packages/webhook-utils/src/operations/modify-app-webhook.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { Client } from "urql";
|
||||
import { ModifyAppWebhookDocument, WebhookUpdateInput } from "../../generated/graphql";
|
||||
|
||||
interface ModifyAppWebhookArgs {
|
||||
client: Client;
|
||||
webhookId: string;
|
||||
input: WebhookUpdateInput;
|
||||
}
|
||||
|
||||
export const modifyAppWebhook = async ({ client, webhookId, input }: ModifyAppWebhookArgs) => {
|
||||
return client
|
||||
.mutation(ModifyAppWebhookDocument, {
|
||||
id: webhookId,
|
||||
input,
|
||||
})
|
||||
.toPromise()
|
||||
.then((r) => {
|
||||
if (r.error) {
|
||||
throw new Error(`Webhook creation failed. The API returned an error: ${r.error.message}`);
|
||||
}
|
||||
|
||||
const webhook = r.data?.webhookUpdate?.webhook;
|
||||
|
||||
if (!webhook) {
|
||||
throw new Error("Webhook modify response is empty. The API returned no additional error.");
|
||||
}
|
||||
return webhook;
|
||||
});
|
||||
};
|
22
packages/webhook-utils/src/operations/remove-app-webhook.ts
Normal file
22
packages/webhook-utils/src/operations/remove-app-webhook.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Client } from "urql";
|
||||
import { RemoveAppWebhookDocument } from "../../generated/graphql";
|
||||
|
||||
interface RemoveAppWebhookArgs {
|
||||
client: Client;
|
||||
webhookId: string;
|
||||
}
|
||||
|
||||
export const removeAppWebhook = async ({ client, webhookId }: RemoveAppWebhookArgs) => {
|
||||
return client
|
||||
.mutation(RemoveAppWebhookDocument, {
|
||||
id: webhookId,
|
||||
})
|
||||
.toPromise()
|
||||
.then((r) => {
|
||||
if (r.error) {
|
||||
throw new Error(`Webhook creation failed. The API returned an error: ${r.error.message}`);
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
};
|
6
packages/webhook-utils/src/setup-tests.ts
Normal file
6
packages/webhook-utils/src/setup-tests.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Add test setup logic here
|
||||
*
|
||||
* https://vitest.dev/config/#setupfiles
|
||||
*/
|
||||
export {};
|
166
packages/webhook-utils/src/update-webhooks.ts
Normal file
166
packages/webhook-utils/src/update-webhooks.ts
Normal file
|
@ -0,0 +1,166 @@
|
|||
import { WebhookManifest } from "@saleor/app-sdk/types";
|
||||
import { Client } from "urql";
|
||||
import { webhooksToRemove } from "./filters/webhooks-to-remove";
|
||||
import { getWebhookIdsAndManifestsToUpdate } from "./filters/get-webhook-ids-and-manifests-to-update";
|
||||
import { webhooksToAdd } from "./filters/webhooks-to-add";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
import { removeAppWebhook } from "./operations/remove-app-webhook";
|
||||
import { WebhookDetailsFragment } from "../generated/graphql";
|
||||
import { createAppWebhookFromManifest } from "./create-app-webhook-from-manifest";
|
||||
import { modifyAppWebhookFromManifest } from "./modify-app-webhook-from-manifest";
|
||||
import { createAppWebhookFromWebhookDetailsFragment } from "./create-app-webhook-from-webhook-details-fragment";
|
||||
import { modifyAppWebhookFromWebhookDetails } from "./modify-app-webhook-from-webhook-details";
|
||||
|
||||
const logger = createLogger({ name: "updateWebhooks" });
|
||||
|
||||
interface RollbackArgs {
|
||||
client: Client;
|
||||
webhookManifests: Array<WebhookManifest>;
|
||||
existingWebhooksData: Array<WebhookDetailsFragment>;
|
||||
addedWebhooks: Array<WebhookDetailsFragment>;
|
||||
modifiedWebhooks: Array<WebhookDetailsFragment>;
|
||||
removedWebhooks: Array<WebhookDetailsFragment>;
|
||||
}
|
||||
|
||||
const rollback = async ({
|
||||
client,
|
||||
addedWebhooks,
|
||||
modifiedWebhooks,
|
||||
existingWebhooksData,
|
||||
removedWebhooks,
|
||||
}: RollbackArgs) => {
|
||||
if (addedWebhooks.length) {
|
||||
logger.info("Removing added webhooks");
|
||||
await Promise.allSettled(
|
||||
addedWebhooks.map((webhook) => removeAppWebhook({ client, webhookId: webhook.id })),
|
||||
);
|
||||
}
|
||||
|
||||
if (modifiedWebhooks.length) {
|
||||
logger.info("Rollback modified webhooks");
|
||||
await Promise.allSettled(
|
||||
modifiedWebhooks.map((modifiedWebhook) => {
|
||||
const webhookDetails = existingWebhooksData.find(
|
||||
(existingWebhook) => existingWebhook.id === modifiedWebhook.id,
|
||||
);
|
||||
|
||||
if (!webhookDetails) {
|
||||
logger.error("This should not happen");
|
||||
throw new Error("This should not happen");
|
||||
}
|
||||
|
||||
return modifyAppWebhookFromWebhookDetails({ client, webhookDetails });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (removedWebhooks.length) {
|
||||
logger.debug("Rollback removed webhooks");
|
||||
|
||||
await Promise.allSettled(
|
||||
modifiedWebhooks.map((webhookDetails) => {
|
||||
return createAppWebhookFromWebhookDetailsFragment({
|
||||
client,
|
||||
webhookDetails,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface UpdateWebhooksArgs {
|
||||
client: Client;
|
||||
webhookManifests: Array<WebhookManifest>;
|
||||
existingWebhooksData: Array<WebhookDetailsFragment>;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
* Based on given list of existing and new webhooks:
|
||||
* - remove the ones which are not in the new list
|
||||
* - create the ones which are not in the existing list
|
||||
* - update queries the ones which are in both lists
|
||||
*
|
||||
* If any of the operations fails, rollback all changes to the initial state
|
||||
*/
|
||||
export const updateWebhooks = async ({
|
||||
client,
|
||||
webhookManifests,
|
||||
existingWebhooksData,
|
||||
dryRun,
|
||||
}: UpdateWebhooksArgs) => {
|
||||
const addedWebhooks = [];
|
||||
const modifiedWebhooks = [];
|
||||
const removedWebhooks = [];
|
||||
|
||||
try {
|
||||
logger.debug("Preparing list of changes to be executed");
|
||||
|
||||
// Based on names, find the ones which should be added
|
||||
const webhookManifestsToBeAdded = webhooksToAdd({
|
||||
existingWebhooksPartial: existingWebhooksData,
|
||||
newWebhookManifests: webhookManifests,
|
||||
});
|
||||
|
||||
// Based on names, find the ones which should be updated
|
||||
const webhookIdsAndManifestsToBeUpdated = getWebhookIdsAndManifestsToUpdate({
|
||||
existingWebhooksPartial: existingWebhooksData,
|
||||
newWebhookManifests: webhookManifests,
|
||||
});
|
||||
|
||||
// Based on names, find the ones which should be removed
|
||||
const webhookToBeRemoved = webhooksToRemove({
|
||||
existingWebhooksPartial: existingWebhooksData,
|
||||
newWebhookManifests: webhookManifests,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Scheduled changes: ${webhookIdsAndManifestsToBeUpdated.length} to be updated, ${webhookManifestsToBeAdded.length} to be added, ${webhookToBeRemoved.length} to be removed`,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
logger.info("Dry run mode, changes will not be executed. Exiting.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const webhookManifest of webhookManifestsToBeAdded) {
|
||||
logger.debug(`Adding webhook ${webhookManifest.name}`);
|
||||
const createdWebhook = await createAppWebhookFromManifest({ client, webhookManifest });
|
||||
|
||||
logger.debug("Webhook added");
|
||||
addedWebhooks.push(createdWebhook);
|
||||
}
|
||||
|
||||
for (const updateData of webhookIdsAndManifestsToBeUpdated) {
|
||||
const { webhookId, webhookManifest } = updateData;
|
||||
|
||||
logger.debug(`Updating webhook ${webhookManifest.name}`);
|
||||
const response = await modifyAppWebhookFromManifest({ client, webhookId, webhookManifest });
|
||||
|
||||
logger.debug("Webhook updated");
|
||||
modifiedWebhooks.push(response);
|
||||
}
|
||||
|
||||
for (const webhookDetails of webhookToBeRemoved) {
|
||||
logger.debug(`Removing webhook ${webhookDetails.name}`);
|
||||
|
||||
await removeAppWebhook({ client, webhookId: webhookDetails.id });
|
||||
|
||||
logger.debug("Webhook removed");
|
||||
removedWebhooks.push(webhookDetails);
|
||||
}
|
||||
|
||||
logger.info("Migration finished successfully");
|
||||
} catch (e) {
|
||||
logger.error(e, "Error during update procedure, rolling back changes");
|
||||
await rollback({
|
||||
client,
|
||||
addedWebhooks,
|
||||
existingWebhooksData,
|
||||
modifiedWebhooks,
|
||||
webhookManifests,
|
||||
removedWebhooks,
|
||||
});
|
||||
logger.info("Changes rolled back");
|
||||
}
|
||||
};
|
59
packages/webhook-utils/src/webhook-migration-runner.ts
Normal file
59
packages/webhook-utils/src/webhook-migration-runner.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { Client } from "urql";
|
||||
import { AppDetailsFragment } from "../generated/graphql";
|
||||
import { WebhookManifest } from "@saleor/app-sdk/types";
|
||||
import { getAppDetailsAndWebhooksData } from "./operations/get-app-details-and-webhooks-data";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
import { updateWebhooks } from "./update-webhooks";
|
||||
|
||||
const logger = createLogger({ name: "updateScript" });
|
||||
|
||||
interface WebhookMigrationRunnerArgs {
|
||||
client: Client;
|
||||
getManifests: ({
|
||||
appDetails,
|
||||
}: {
|
||||
appDetails: AppDetailsFragment;
|
||||
}) => Promise<Array<WebhookManifest>>;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
export const webhookMigrationRunner = async ({
|
||||
client,
|
||||
getManifests,
|
||||
dryRun,
|
||||
}: WebhookMigrationRunnerArgs) => {
|
||||
logger.info("Getting app details and webhooks data");
|
||||
|
||||
let appDetails: AppDetailsFragment | undefined;
|
||||
|
||||
try {
|
||||
appDetails = await getAppDetailsAndWebhooksData({ client });
|
||||
} catch (e) {
|
||||
logger.error(e, "Couldn't fetch the app details.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appDetails) {
|
||||
logger.error("No app details.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Got app details and webhooks data. Generate list of webhook manifests");
|
||||
let newWebhookManifests: Array<WebhookManifest> = [];
|
||||
|
||||
try {
|
||||
newWebhookManifests = await getManifests({ appDetails });
|
||||
} catch (e) {
|
||||
logger.error(e, "Couldn't prepare list of manifests.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Got list of webhook manifests. Updating webhooks");
|
||||
await updateWebhooks({
|
||||
client,
|
||||
webhookManifests: newWebhookManifests,
|
||||
existingWebhooksData: appDetails.webhooks || [],
|
||||
dryRun,
|
||||
});
|
||||
logger.info("Migration finished.");
|
||||
};
|
20
packages/webhook-utils/tsconfig.json
Normal file
20
packages/webhook-utils/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
12
packages/webhook-utils/vitest.config.ts
Normal file
12
packages/webhook-utils/vitest.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
test: {
|
||||
passWithNoTests: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: "./src/setup-tests.ts",
|
||||
css: false,
|
||||
},
|
||||
});
|
1879
pnpm-lock.yaml
1879
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue