Compare commits

..

4 commits

Author SHA1 Message Date
4b12982597 add changeset 2023-09-30 22:22:32 +02:00
292a5bdb0c emails-and-messages redis apl support 2023-09-30 22:03:02 +02:00
Lukasz Ostrowski
653b98df86
Release apps (#1041)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-28 16:02:35 +02:00
Krzysztof Wolski
ae6dbb125b
Remove cache update on product webhooks (#1036)
* Product feed - change max function exec time to 5 minutes

* Remove cache update on product webhooks

* Add webhooks migration

---------

Co-authored-by: Lukasz Ostrowski <lukasz.ostrowski@saleor.io>
2023-09-27 12:56:12 +02:00
57 changed files with 336 additions and 772 deletions

View file

@ -0,0 +1,8 @@
#changelog
---
"apps": minor
---
### Added
- `apps/emails-and-messages/.env.template`: Described the new environment variable and how it works
- `apps/emails-and-messages/src/saleor-app.ts`: Added case "redis" for switch(AplType), which takes advantage of the [RedisAPL PR](https://github.com/saleor/app-sdk/pull/287) I submitted

View file

@ -7,6 +7,12 @@ APL=
REST_APL_ENDPOINT= REST_APL_ENDPOINT=
REST_APL_TOKEN= REST_APL_TOKEN=
# To use Redis as an APL store, set APP_API_BASE_URL and REDIS_URL.
# URL is in format redis[s]://[[username][:password]@][host][:port][/db-number],
# so for example redis://alice:foobared@awesome.redis.server:6380
# For saleor-platform, thats: `redis://redis:6379/1`
REDIS_URL=
APP_LOG_LEVEL=info APP_LOG_LEVEL=info
# Local development variables. When developped locally with Saleor inside docker, these can be set to: # Local development variables. When developped locally with Saleor inside docker, these can be set to:
@ -16,3 +22,4 @@ APP_LOG_LEVEL=info
# https://docs.saleor.io/docs/3.x/developer/extending/apps/local-app-development # https://docs.saleor.io/docs/3.x/developer/extending/apps/local-app-development
APP_IFRAME_BASE_URL= APP_IFRAME_BASE_URL=
APP_API_BASE_URL= APP_API_BASE_URL=

View file

@ -1,4 +1,4 @@
import { APL, FileAPL, SaleorCloudAPL, UpstashAPL } from "@saleor/app-sdk/APL"; import { APL, FileAPL, RedisAPL, SaleorCloudAPL, UpstashAPL } from "@saleor/app-sdk/APL";
import { SaleorApp } from "@saleor/app-sdk/saleor-app"; import { SaleorApp } from "@saleor/app-sdk/saleor-app";
const aplType = process.env.APL ?? "file"; const aplType = process.env.APL ?? "file";
@ -6,6 +6,12 @@ const aplType = process.env.APL ?? "file";
export let apl: APL; export let apl: APL;
switch (aplType) { switch (aplType) {
case "redis": {
if (!process.env.REDIS_URL) throw new Error("Missing redis url");
if (!process.env.APP_API_BASE_URL)
throw new Error("Redis relies on APP_API_BASE_URL to store keys, please set env variable");
apl = new RedisAPL(new URL(process.env.REDIS_URL), process.env.APP_API_BASE_URL);
}
case "upstash": case "upstash":
apl = new UpstashAPL(); apl = new UpstashAPL();

View file

@ -1,5 +1,7 @@
{ {
"extends": ["//"], "extends": [
"//"
],
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"pipeline": { "pipeline": {
"build": { "build": {
@ -21,7 +23,8 @@
"NEXT_PUBLIC_SENTRY_DSN", "NEXT_PUBLIC_SENTRY_DSN",
"SENTRY_ENVIRONMENT", "SENTRY_ENVIRONMENT",
"APP_IFRAME_BASE_URL", "APP_IFRAME_BASE_URL",
"APP_API_BASE_URL" "APP_API_BASE_URL",
"REDIS_URL"
] ]
} }
} }

View file

@ -1,5 +1,12 @@
# saleor-app-products-feed # saleor-app-products-feed
## 1.12.2
### Patch Changes
- ae6dbb1: Removed webhooks on product changes used for feed cache due to changed max execution time.
- ae6dbb1: Changed Vercel's maximum execution time to be 5 minutes for feed generation. This should help with the previous limits of 60s, that was not enough for feed to be generated.
## 1.12.1 ## 1.12.1
### Patch Changes ### Patch Changes

View file

@ -1,8 +0,0 @@
fragment ProductVariantWebhookPayload on ProductVariant {
channel
channelListings {
channel {
slug
}
}
}

View file

@ -1,8 +0,0 @@
fragment ProductWebhookPayload on Product {
channel
channelListings {
channel {
slug
}
}
}

View file

@ -1,9 +0,0 @@
subscription ProductCreated {
event {
... on ProductCreated {
product {
...ProductWebhookPayload
}
}
}
}

View file

@ -1,9 +0,0 @@
subscription ProductDeleted {
event {
... on ProductDeleted {
product {
...ProductWebhookPayload
}
}
}
}

View file

@ -1,9 +0,0 @@
subscription ProductUpdated {
event {
... on ProductUpdated {
product {
...ProductWebhookPayload
}
}
}
}

View file

@ -1,9 +0,0 @@
subscription ProductVariantCreated {
event {
... on ProductVariantCreated {
productVariant {
...ProductVariantWebhookPayload
}
}
}
}

View file

@ -1,10 +0,0 @@
subscription ProductVariantDeleted {
event {
... on ProductVariantDeleted {
productVariant {
...ProductVariantWebhookPayload
}
}
}
}

View file

@ -1,9 +0,0 @@
subscription ProductVariantUpdated {
event {
... on ProductVariantUpdated {
productVariant {
...ProductVariantWebhookPayload
}
}
}
}

View file

@ -6,7 +6,12 @@ const isSentryPropertiesInEnvironment =
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"], transpilePackages: [
"@saleor/apps-shared",
"@saleor/apps-ui",
"@saleor/react-hook-form-macaw",
"@saleor/webhook-utils",
],
}; };
const configWithSentry = withSentryConfig( const configWithSentry = withSentryConfig(
@ -22,7 +27,7 @@ const configWithSentry = withSentryConfig(
tunnelRoute: "/monitoring", tunnelRoute: "/monitoring",
hideSourceMaps: true, hideSourceMaps: true,
disableLogger: true, disableLogger: true,
} },
); );
module.exports = isSentryPropertiesInEnvironment ? configWithSentry : nextConfig; module.exports = isSentryPropertiesInEnvironment ? configWithSentry : nextConfig;

View file

@ -1,6 +1,6 @@
{ {
"name": "saleor-app-products-feed", "name": "saleor-app-products-feed",
"version": "1.12.1", "version": "1.12.2",
"scripts": { "scripts": {
"build": "pnpm generate && next build", "build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev", "dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -19,6 +19,7 @@
"@saleor/apps-ui": "workspace:*", "@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.127", "@saleor/macaw-ui": "0.8.0-pre.127",
"@saleor/react-hook-form-macaw": "workspace:*", "@saleor/react-hook-form-macaw": "workspace:*",
"@saleor/webhook-utils": "workspace:*",
"@sentry/nextjs": "7.67.0", "@sentry/nextjs": "7.67.0",
"@tanstack/react-query": "4.29.19", "@tanstack/react-query": "4.29.19",
"@trpc/client": "10.38.1", "@trpc/client": "10.38.1",
@ -27,6 +28,7 @@
"@trpc/server": "10.38.1", "@trpc/server": "10.38.1",
"@urql/exchange-auth": "^2.1.4", "@urql/exchange-auth": "^2.1.4",
"@vitejs/plugin-react": "4.0.4", "@vitejs/plugin-react": "4.0.4",
"dotenv": "^16.3.1",
"fast-xml-parser": "^4.0.15", "fast-xml-parser": "^4.0.15",
"graphql": "16.7.1", "graphql": "16.7.1",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",

View file

@ -0,0 +1,7 @@
# Webhook migration scripts
Test migration with dry run, operation will not modify any data:
`npx tsx scripts/migrations/run-webhooks-migration-dry-run.ts`
To start the migration run command:
`npx tsx scripts/migrations/run-webhooks-migration.ts`

View file

@ -0,0 +1,20 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { SaleorCloudAPL } from "@saleor/app-sdk/APL";
export const verifyRequiredEnvs = () => {
const requiredEnvs = ["SALEOR_CLOUD_TOKEN", "SALEOR_CLOUD_RESOURCE_URL"];
if (!requiredEnvs.every((env) => process.env[env])) {
throw new Error(`Missing envs: ${requiredEnvs.join(" | ")}`);
}
};
export const fetchCloudAplEnvs = () => {
const saleorAPL = new SaleorCloudAPL({
token: process.env.SALEOR_CLOUD_TOKEN!,
resourceUrl: process.env.SALEOR_CLOUD_RESOURCE_URL!,
});
return saleorAPL.getAll();
};

View file

@ -0,0 +1,30 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
import { updateWebhooksScript } from "./update-webhooks";
dotenv.config();
const runMigration = async () => {
console.log("Starting webhooks migration (dry run)");
verifyRequiredEnvs();
console.log("Envs verified, fetching envs");
const allEnvs = await fetchCloudAplEnvs().catch((r) => {
console.error("Could not fetch instances from the APL");
console.error(r);
process.exit(1);
});
for (const env of allEnvs) {
await updateWebhooksScript({ authData: env, dryRun: true });
}
console.log("Migration dry run complete");
};
runMigration();

View file

@ -0,0 +1,30 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
import { updateWebhooksScript } from "./update-webhooks";
dotenv.config();
const runMigration = async () => {
console.log("Starting running migration");
verifyRequiredEnvs();
console.log("Envs verified, fetching envs");
const allEnvs = await fetchCloudAplEnvs().catch((r) => {
console.error("Could not fetch instances from the APL");
console.error(r);
process.exit(1);
});
for (const env of allEnvs) {
await updateWebhooksScript({ authData: env, dryRun: false });
}
console.log("Migration complete");
};
runMigration();

View file

@ -0,0 +1,29 @@
/* 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";
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 }) => {
// Products feed application has currently no webhooks, so we return empty array
return [];
},
});
};

View file

@ -2,7 +2,6 @@ import { router } from "../trpc/trpc-server";
import { protectedClientProcedure } from "../trpc/protected-client-procedure"; import { protectedClientProcedure } from "../trpc/protected-client-procedure";
import { createLogger } from "@saleor/apps-shared"; import { createLogger } from "@saleor/apps-shared";
import { updateCacheForConfigurations } from "../metadata-cache/update-cache-for-configurations";
import { AppConfigSchema, imageSizeInputSchema, titleTemplateInputSchema } from "./app-config"; import { AppConfigSchema, imageSizeInputSchema, titleTemplateInputSchema } from "./app-config";
import { z } from "zod"; import { z } from "zod";
import { createS3ClientFromConfiguration } from "../file-storage/s3/create-s3-client-from-configuration"; import { createS3ClientFromConfiguration } from "../file-storage/s3/create-s3-client-from-configuration";
@ -106,17 +105,6 @@ export const appConfigurationRouter = router({
}) => { }) => {
const config = await getConfig(); const config = await getConfig();
/**
* TODO Check if this has to run, once its cached, it should be invalidated by webhooks only.
*
* But this operation isn't expensive and users will not continuously save this form
*/
await updateCacheForConfigurations({
client: apiClient,
channelsSlugs: [input.channelSlug],
saleorApiUrl: saleorApiUrl,
});
logger.debug({ channel: input.channelSlug }, "Updated cache for channel"); logger.debug({ channel: input.channelSlug }, "Updated cache for channel");
config.setChannelUrls(input.channelSlug, input.urls); config.setChannelUrls(input.channelSlug, input.urls);

View file

@ -1,33 +0,0 @@
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
export class CacheConfigurator {
private metadataKeyPrefix = "cursor-cache-";
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
private constructKey(channel: string) {
return this.metadataKeyPrefix + channel;
}
get({ channel }: { channel: string }): Promise<string[] | undefined> {
return this.metadataManager.get(this.constructKey(channel), this.saleorApiUrl).then((data) => {
if (!data) {
return undefined;
}
try {
return JSON.parse(data);
} catch (e) {
throw new Error("Invalid metadata value, can't be parsed");
}
});
}
set({ channel, value }: { channel: string; value: string[] }): Promise<void> {
return this.metadataManager.set({
key: this.constructKey(channel),
value: JSON.stringify(value),
domain: this.saleorApiUrl,
});
}
}

View file

@ -1,33 +0,0 @@
import { createLogger } from "@saleor/apps-shared";
import { CacheConfigurator } from "./cache-configurator";
import { createSettingsManager } from "../../lib/metadata-manager";
import { getCursors } from "../google-feed/fetch-product-data";
import { Client } from "urql";
interface UpdateCacheForConfigurationsArgs {
client: Client;
saleorApiUrl: string;
channelsSlugs: string[];
}
export const updateCacheForConfigurations = async ({
client,
channelsSlugs,
saleorApiUrl,
}: UpdateCacheForConfigurationsArgs) => {
const logger = createLogger({ saleorApiUrl: saleorApiUrl });
logger.debug("Updating the cursor cache");
const cache = new CacheConfigurator(createSettingsManager(client), saleorApiUrl);
const cacheUpdatePromises = channelsSlugs.map(async (channel) => {
const cursors = await getCursors({ client, channel });
await cache.set({ channel, value: cursors });
});
await Promise.all(cacheUpdatePromises);
logger.debug("Cursor cache updated");
};

View file

@ -1,41 +0,0 @@
import { GraphqlClientFactory } from "../../lib/create-graphql-client";
import { updateCacheForConfigurations } from "./update-cache-for-configurations";
import { AuthData } from "@saleor/app-sdk/APL";
import {
ProductVariantWebhookPayloadFragment,
ProductWebhookPayloadFragment,
} from "../../../generated/graphql";
import { NextApiResponse } from "next";
type ChannelFragment =
| Pick<ProductWebhookPayloadFragment, "channel" | "channelListings">
| Pick<ProductVariantWebhookPayloadFragment, "channel" | "channelListings">;
export const updateCacheOnWebhook = async ({
channels,
authData,
res,
}: {
authData: AuthData;
channels: ChannelFragment;
res: NextApiResponse;
}) => {
const client = GraphqlClientFactory.fromAuthData(authData);
const channelsSlugs = [
channels.channel,
...(channels.channelListings?.map((cl) => cl.channel.slug) ?? []),
].filter((c) => c) as string[];
if (channelsSlugs.length === 0) {
return res.status(200).end();
}
await updateCacheForConfigurations({
channelsSlugs,
client,
saleorApiUrl: authData.saleorApiUrl,
});
return res.status(200).end();
};

View file

@ -6,9 +6,6 @@ import { fetchProductData } from "../../../../../modules/google-feed/fetch-produ
import { GoogleFeedSettingsFetcher } from "../../../../../modules/google-feed/get-google-feed-settings"; import { GoogleFeedSettingsFetcher } from "../../../../../modules/google-feed/get-google-feed-settings";
import { generateGoogleXmlFeed } from "../../../../../modules/google-feed/generate-google-xml-feed"; import { generateGoogleXmlFeed } from "../../../../../modules/google-feed/generate-google-xml-feed";
import { fetchShopData } from "../../../../../modules/google-feed/fetch-shop-data"; import { fetchShopData } from "../../../../../modules/google-feed/fetch-shop-data";
import { CacheConfigurator } from "../../../../../modules/metadata-cache/cache-configurator";
import { createSettingsManager } from "../../../../../lib/metadata-manager";
import { GraphqlClientFactory } from "../../../../../lib/create-graphql-client";
import { uploadFile } from "../../../../../modules/file-storage/s3/upload-file"; import { uploadFile } from "../../../../../modules/file-storage/s3/upload-file";
import { createS3ClientFromConfiguration } from "../../../../../modules/file-storage/s3/create-s3-client-from-configuration"; import { createS3ClientFromConfiguration } from "../../../../../modules/file-storage/s3/create-s3-client-from-configuration";
import { getFileDetails } from "../../../../../modules/file-storage/s3/get-file-details"; import { getFileDetails } from "../../../../../modules/file-storage/s3/get-file-details";
@ -16,6 +13,10 @@ import { getDownloadUrl, getFileName } from "../../../../../modules/file-storage
import { RootConfig } from "../../../../../modules/app-configuration/app-config"; import { RootConfig } from "../../../../../modules/app-configuration/app-config";
import { z, ZodError } from "zod"; import { z, ZodError } from "zod";
export const config = {
maxDuration: 5 * 60, // 5 minutes
};
// By default we cache the feed for 5 minutes. This can be changed by setting the FEED_CACHE_MAX_AGE // By default we cache the feed for 5 minutes. This can be changed by setting the FEED_CACHE_MAX_AGE
const FEED_CACHE_MAX_AGE = process.env.FEED_CACHE_MAX_AGE const FEED_CACHE_MAX_AGE = process.env.FEED_CACHE_MAX_AGE
? parseInt(process.env.FEED_CACHE_MAX_AGE, 10) ? parseInt(process.env.FEED_CACHE_MAX_AGE, 10)
@ -157,23 +158,10 @@ export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
logger.debug("Generating a new feed"); logger.debug("Generating a new feed");
const cacheClient = GraphqlClientFactory.fromAuthData(authData);
if (!cacheClient) {
logger.error("Can't create the gql client");
return res.status(500).end();
}
// get cached cursors
const cache = new CacheConfigurator(createSettingsManager(cacheClient), authData.saleorApiUrl);
const cursors = await cache.get({ channel });
// TODO: instead of separate variants, use group id https://support.google.com/merchants/answer/6324507?hl=en
let productVariants: GoogleFeedProductVariantFragment[] = []; let productVariants: GoogleFeedProductVariantFragment[] = [];
try { try {
productVariants = await fetchProductData({ client, channel, cursors, imageSize }); productVariants = await fetchProductData({ client, channel, imageSize });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return res.status(400).end(); return res.status(400).end();

View file

@ -2,11 +2,6 @@ import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
import { AppManifest } from "@saleor/app-sdk/types"; import { AppManifest } from "@saleor/app-sdk/types";
import packageJson from "../../../package.json"; import packageJson from "../../../package.json";
import { webhookProductCreated } from "./webhooks/product_created";
import { webhookProductDeleted } from "./webhooks/product_deleted";
import { webhookProductVariantCreated } from "./webhooks/product_variant_created";
import { webhookProductVariantDeleted } from "./webhooks/product_variant_deleted";
import { webhookProductVariantUpdated } from "./webhooks/product_variant_updated";
export default createManifestHandler({ export default createManifestHandler({
async manifestFactory({ appBaseUrl }) { async manifestFactory({ appBaseUrl }) {
@ -31,13 +26,7 @@ export default createManifestHandler({
supportUrl: "https://github.com/saleor/apps/discussions", supportUrl: "https://github.com/saleor/apps/discussions",
tokenTargetUrl: `${apiBaseURL}/api/register`, tokenTargetUrl: `${apiBaseURL}/api/register`,
version: packageJson.version, version: packageJson.version,
webhooks: [ webhooks: [],
webhookProductCreated.getWebhookManifest(apiBaseURL),
webhookProductDeleted.getWebhookManifest(apiBaseURL),
webhookProductVariantCreated.getWebhookManifest(apiBaseURL),
webhookProductVariantDeleted.getWebhookManifest(apiBaseURL),
webhookProductVariantUpdated.getWebhookManifest(apiBaseURL),
],
}; };
return manifest; return manifest;

View file

@ -1,41 +0,0 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { createLogger } from "@saleor/apps-shared";
import { saleorApp } from "../../../saleor-app";
import {
ProductCreatedDocument,
ProductWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductCreated = new SaleorAsyncWebhook<ProductWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_created",
event: "PRODUCT_CREATED",
apl: saleorApp.apl,
query: ProductCreatedDocument,
// todo make it disabled by default, enable when app is configured
isActive: true,
});
const logger = createLogger({
service: "webhook-product_created",
});
export const handler: NextWebhookApiHandler<ProductWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductCreated.createHandler(handler);

View file

@ -1,40 +0,0 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import {
ProductDeletedDocument,
ProductWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
import { createLogger } from "@saleor/apps-shared";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductDeleted = new SaleorAsyncWebhook<ProductWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_deleted",
event: "PRODUCT_DELETED",
apl: saleorApp.apl,
query: ProductDeletedDocument,
isActive: true,
});
const logger = createLogger({
service: "webhook_product_deleted",
});
export const handler: NextWebhookApiHandler<ProductWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductDeleted.createHandler(handler);

View file

@ -1,40 +0,0 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import {
ProductUpdatedDocument,
ProductWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
import { createLogger } from "@saleor/apps-shared";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductUpdated = new SaleorAsyncWebhook<ProductWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_updated",
event: "PRODUCT_UPDATED",
apl: saleorApp.apl,
query: ProductUpdatedDocument,
isActive: true,
});
const logger = createLogger({
service: "webhookProductUpdatedWebhookHandler",
});
export const handler: NextWebhookApiHandler<ProductWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductUpdated.createHandler(handler);

View file

@ -1,41 +0,0 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import {
ProductVariantCreatedDocument,
ProductVariantWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
import { createLogger } from "@saleor/apps-shared";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductVariantCreated =
new SaleorAsyncWebhook<ProductVariantWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_variant_created",
event: "PRODUCT_VARIANT_CREATED",
apl: saleorApp.apl,
query: ProductVariantCreatedDocument,
isActive: true,
});
const logger = createLogger({
service: "PRODUCT_VARIANT_CREATED webhook",
});
export const handler: NextWebhookApiHandler<ProductVariantWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductVariantCreated.createHandler(handler);

View file

@ -1,41 +0,0 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { createLogger } from "@saleor/apps-shared";
import {
ProductVariantDeletedDocument,
ProductVariantWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductVariantDeleted =
new SaleorAsyncWebhook<ProductVariantWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_variant_deleted",
event: "PRODUCT_VARIANT_DELETED",
apl: saleorApp.apl,
query: ProductVariantDeletedDocument,
isActive: true,
});
const logger = createLogger({
service: "PRODUCT_VARIANT_DELETED",
});
export const handler: NextWebhookApiHandler<ProductVariantWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductVariantDeleted.createHandler(handler);

View file

@ -1,41 +0,0 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { createLogger } from "@saleor/apps-shared";
import {
ProductVariantUpdatedDocument,
ProductVariantWebhookPayloadFragment,
} from "../../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
export const config = {
api: {
bodyParser: false,
},
};
export const webhookProductVariantUpdated =
new SaleorAsyncWebhook<ProductVariantWebhookPayloadFragment>({
webhookPath: "api/webhooks/product_variant_updated",
event: "PRODUCT_VARIANT_UPDATED",
apl: saleorApp.apl,
query: ProductVariantUpdatedDocument,
isActive: true,
});
const logger = createLogger({
service: "webhookProductVariantUpdatedWebhookHandler",
});
export const handler: NextWebhookApiHandler<ProductVariantWebhookPayloadFragment> = async (
req,
res,
context
) => {
await updateCacheOnWebhook({
authData: context.authData,
channels: context.payload,
res,
});
};
export default webhookProductVariantUpdated.createHandler(handler);

View file

@ -31,8 +31,6 @@
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"jotai": "^2.4.2", "jotai": "^2.4.2",
"jsdom": "^20.0.3", "jsdom": "^20.0.3",
"modern-errors": "^6.0.0",
"modern-errors-serialize": "^5.0.0",
"next": "13.4.8", "next": "13.4.8",
"pino": "^8.14.1", "pino": "^8.14.1",
"pino-pretty": "^10.0.0", "pino-pretty": "^10.0.0",

View file

@ -1,6 +0,0 @@
import ModernError from "modern-errors";
import modernErrorsSerialize from "modern-errors-serialize";
export const BaseError = ModernError.subclass("BaseError", {
plugins: [modernErrorsSerialize],
});

View file

@ -1,7 +1,41 @@
import { NextApiResponse } from "next"; import { NextApiResponse } from "next";
import { AvalaraError } from "avatax/lib/AvaTaxClient";
import { ZodError } from "zod";
import { createLogger, Logger } from "../../lib/logger"; import { createLogger, Logger } from "../../lib/logger";
import { TaxBadWebhookPayloadError, TaxCriticalError } from "../taxes/tax-error";
class WebhookErrorResolver {
private logger: Logger;
constructor() {
this.logger = createLogger({ event: "WebhookErrorResolver" });
}
private resolveErrorMessage(error: unknown) {
if (error instanceof ZodError) {
this.logger.error(error.message, "Unexpected Zod error caught:");
this.logger.debug(error.stack, "Error details:");
return error.message;
}
if (error instanceof AvalaraError) {
this.logger.error(error.message, "Unexpected Avalara error caught:");
this.logger.debug(error.stack, "Error stack:");
this.logger.debug(error.target, "Error target:");
return error.message;
}
if (error instanceof Error) {
this.logger.error(error.stack, "Unexpected error caught:");
return error.message;
}
return "Internal server error";
}
resolve(error: unknown) {
return this.resolveErrorMessage(error);
}
}
export class WebhookResponse { export class WebhookResponse {
private logger: Logger; private logger: Logger;
@ -9,28 +43,13 @@ export class WebhookResponse {
this.logger = createLogger({ event: "WebhookResponse" }); this.logger = createLogger({ event: "WebhookResponse" });
} }
private respondWithBadRequest(errorMessage: string) {
// Are we sure its 400?
return this.res.status(400).json({ error: errorMessage });
}
private respondWithInternalServerError(errorMessage: string) {
return this.res.status(500).json({ error: errorMessage });
}
error(error: unknown) { error(error: unknown) {
if (error instanceof TaxBadWebhookPayloadError) { const errorResolver = new WebhookErrorResolver();
this.logger.warn({ error }, "TaxBadWebhookPayloadError occurred"); const errorMessage = errorResolver.resolve(error);
return this.respondWithBadRequest(error.message);
}
if (error instanceof TaxCriticalError) { this.logger.debug({ errorMessage }, "Responding to Saleor with error:");
this.logger.error({ error }, "TaxCriticalError occurred");
return this.respondWithInternalServerError(error.message);
}
this.logger.error({ error }, "Unexpected error occurred"); return this.res.status(500).json({ error: errorMessage });
return this.respondWithInternalServerError("Unexpected error occurred");
} }
success(data?: unknown) { success(data?: unknown) {

View file

@ -1,17 +0,0 @@
import { createLogger } from "../../lib/logger";
import { AvalaraError } from "avatax/lib/AvaTaxClient";
import { TaxExternalError, TaxUnexpectedError } from "../taxes/tax-error";
export class AvataxErrorNormalizer {
private logger = createLogger({ name: "AvataxErrorNormalizer" });
normalize(error: unknown) {
if (error instanceof AvalaraError) {
this.logger.debug(error.stack, "AvalaraError occurred");
return new TaxExternalError(error.message);
}
return TaxUnexpectedError.normalize(error);
}
}

View file

@ -8,7 +8,6 @@ import { AvataxConfig } from "../avatax-connection-schema";
import { ClientLogger } from "../../logs/client-logger"; import { ClientLogger } from "../../logs/client-logger";
import { AvataxCalculateTaxesPayloadService } from "./avatax-calculate-taxes-payload.service"; import { AvataxCalculateTaxesPayloadService } from "./avatax-calculate-taxes-payload.service";
import { AvataxCalculateTaxesResponseTransformer } from "./avatax-calculate-taxes-response-transformer"; import { AvataxCalculateTaxesResponseTransformer } from "./avatax-calculate-taxes-response-transformer";
import { AvataxErrorNormalizer } from "../avatax-error-normalizer";
export const SHIPPING_ITEM_CODE = "Shipping"; export const SHIPPING_ITEM_CODE = "Shipping";
@ -68,8 +67,6 @@ export class AvataxCalculateTaxesAdapter
return transformedResponse; return transformedResponse;
} catch (error) { } catch (error) {
const errorNormalizer = new AvataxErrorNormalizer();
this.clientLogger.push({ this.clientLogger.push({
event: "[CalculateTaxes] createTransaction", event: "[CalculateTaxes] createTransaction",
status: "error", status: "error",
@ -78,7 +75,7 @@ export class AvataxCalculateTaxesAdapter
output: error, output: error,
}, },
}); });
throw errorNormalizer.normalize(error); throw error;
} }
} }
} }

View file

@ -1,14 +1,14 @@
import { DocumentType } from "avatax/lib/enums/DocumentType"; import { DocumentType } from "avatax/lib/enums/DocumentType";
import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes"; import { TaxBaseFragment } from "../../../../generated/graphql";
import { discountUtils } from "../../taxes/discount-utils"; import { discountUtils } from "../../taxes/discount-utils";
import { TaxUnexpectedError } from "../../taxes/tax-error";
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
import { avataxAddressFactory } from "../address-factory"; import { avataxAddressFactory } from "../address-factory";
import { AvataxClient, CreateTransactionArgs } from "../avatax-client"; import { AvataxClient, CreateTransactionArgs } from "../avatax-client";
import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema"; import { AvataxConfig, defaultAvataxConfig } from "../avatax-connection-schema";
import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository"; import { AvataxTaxCodeMatches } from "../tax-code/avatax-tax-code-match-repository";
import { AvataxCalculateTaxesPayloadLinesTransformer } from "./avatax-calculate-taxes-payload-lines-transformer"; import { AvataxCalculateTaxesPayloadLinesTransformer } from "./avatax-calculate-taxes-payload-lines-transformer";
import { AvataxEntityTypeMatcher } from "../avatax-entity-type-matcher";
import { taxProviderUtils } from "../../taxes/tax-provider-utils";
import { CalculateTaxesPayload } from "../../../pages/api/webhooks/checkout-calculate-taxes";
export class AvataxCalculateTaxesPayloadTransformer { export class AvataxCalculateTaxesPayloadTransformer {
private matchDocumentType(config: AvataxConfig): DocumentType { private matchDocumentType(config: AvataxConfig): DocumentType {
@ -31,7 +31,7 @@ export class AvataxCalculateTaxesPayloadTransformer {
return taxProviderUtils.resolveStringOrThrow(payload.taxBase.sourceObject.userEmail); return taxProviderUtils.resolveStringOrThrow(payload.taxBase.sourceObject.userEmail);
} }
throw new TaxUnexpectedError("Cannot resolve customer code"); throw new Error("Cannot resolve customer code");
} }
async transform( async transform(

View file

@ -14,11 +14,11 @@ export class AvataxCalculateTaxesResponseLinesTransformer {
return { return {
total_gross_amount: taxProviderUtils.resolveOptionalOrThrow( total_gross_amount: taxProviderUtils.resolveOptionalOrThrow(
line.lineAmount, line.lineAmount,
"line.lineAmount is undefined", new Error("line.lineAmount is undefined")
), ),
total_net_amount: taxProviderUtils.resolveOptionalOrThrow( total_net_amount: taxProviderUtils.resolveOptionalOrThrow(
line.lineAmount, line.lineAmount,
"line.lineAmount is undefined", new Error("line.lineAmount is undefined")
), ),
tax_rate: 0, tax_rate: 0,
}; };
@ -26,21 +26,21 @@ export class AvataxCalculateTaxesResponseLinesTransformer {
const lineTaxCalculated = taxProviderUtils.resolveOptionalOrThrow( const lineTaxCalculated = taxProviderUtils.resolveOptionalOrThrow(
line.taxCalculated, line.taxCalculated,
"line.taxCalculated is undefined", new Error("line.taxCalculated is undefined")
); );
const lineTotalNetAmount = taxProviderUtils.resolveOptionalOrThrow( const lineTotalNetAmount = taxProviderUtils.resolveOptionalOrThrow(
line.taxableAmount, line.taxableAmount,
"line.taxableAmount is undefined", new Error("line.taxableAmount is undefined")
); );
const lineTotalGrossAmount = numbers.roundFloatToTwoDecimals( const lineTotalGrossAmount = numbers.roundFloatToTwoDecimals(
lineTotalNetAmount + lineTaxCalculated, lineTotalNetAmount + lineTaxCalculated
); );
return { return {
total_gross_amount: lineTotalGrossAmount, total_gross_amount: lineTotalGrossAmount,
total_net_amount: lineTotalNetAmount, total_net_amount: lineTotalNetAmount,
/* /*
* avatax doesn't return combined tax rate * avatax doesnt return combined tax rate
* // todo: calculate percentage tax rate * // todo: calculate percentage tax rate
*/ tax_rate: 0, */ tax_rate: 0,
}; };

View file

@ -6,7 +6,7 @@ import { SHIPPING_ITEM_CODE } from "./avatax-calculate-taxes-adapter";
export class AvataxCalculateTaxesResponseShippingTransformer { export class AvataxCalculateTaxesResponseShippingTransformer {
transform( transform(
transaction: TransactionModel, transaction: TransactionModel
): Pick< ): Pick<
CalculateTaxesResponse, CalculateTaxesResponse,
"shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate" "shipping_price_gross_amount" | "shipping_price_net_amount" | "shipping_tax_rate"
@ -25,14 +25,14 @@ export class AvataxCalculateTaxesResponseShippingTransformer {
return { return {
shipping_price_gross_amount: taxProviderUtils.resolveOptionalOrThrow( shipping_price_gross_amount: taxProviderUtils.resolveOptionalOrThrow(
shippingLine.lineAmount, shippingLine.lineAmount,
"shippingLine.lineAmount is undefined", new Error("shippingLine.lineAmount is undefined")
), ),
shipping_price_net_amount: taxProviderUtils.resolveOptionalOrThrow( shipping_price_net_amount: taxProviderUtils.resolveOptionalOrThrow(
shippingLine.lineAmount, shippingLine.lineAmount,
"shippingLine.lineAmount is undefined", new Error("shippingLine.lineAmount is undefined")
), ),
/* /*
* avatax doesn't return combined tax rate * avatax doesnt return combined tax rate
* // todo: calculate percentage tax rate * // todo: calculate percentage tax rate
*/ */
shipping_tax_rate: 0, shipping_tax_rate: 0,
@ -41,14 +41,14 @@ export class AvataxCalculateTaxesResponseShippingTransformer {
const shippingTaxCalculated = taxProviderUtils.resolveOptionalOrThrow( const shippingTaxCalculated = taxProviderUtils.resolveOptionalOrThrow(
shippingLine.taxCalculated, shippingLine.taxCalculated,
"shippingLine.taxCalculated is undefined", new Error("shippingLine.taxCalculated is undefined")
); );
const shippingTaxableAmount = taxProviderUtils.resolveOptionalOrThrow( const shippingTaxableAmount = taxProviderUtils.resolveOptionalOrThrow(
shippingLine.taxableAmount, shippingLine.taxableAmount,
"shippingLine.taxableAmount is undefined", new Error("shippingLine.taxableAmount is undefined")
); );
const shippingGrossAmount = numbers.roundFloatToTwoDecimals( const shippingGrossAmount = numbers.roundFloatToTwoDecimals(
shippingTaxableAmount + shippingTaxCalculated, shippingTaxableAmount + shippingTaxCalculated
); );
return { return {

View file

@ -0,0 +1,59 @@
import { AddressResolutionModel } from "avatax/lib/models/AddressResolutionModel";
import { describe, expect, it } from "vitest";
import { AvataxValidationResponseResolver } from "./avatax-validation-response-resolver";
import { ResolutionQuality } from "avatax/lib/enums/ResolutionQuality";
import { JurisdictionType } from "avatax/lib/enums/JurisdictionType";
const mockFailedValidationResponse: AddressResolutionModel = {
address: {
line1: "2000 Main Street",
city: "Irvine",
region: "CA",
country: "US",
postalCode: "92614",
},
coordinates: {
latitude: 33.684884,
longitude: -117.851321,
},
resolutionQuality: ResolutionQuality.Intersection,
taxAuthorities: [
{
avalaraId: "AGAM",
jurisdictionName: "CALIFORNIA",
jurisdictionType: JurisdictionType.State,
signatureCode: "AGAM",
},
],
messages: [
{
summary: "The address is not deliverable.",
details:
"The physical location exists but there are no homes on this street. One reason might be railroad tracks or rivers running alongside this street, as they would prevent construction of homes in this location.",
refersTo: "address",
severity: "Error",
source: "Avalara.AvaTax.Services.Address",
},
],
};
const mockSuccessfulValidationResponse: AddressResolutionModel = {
...mockFailedValidationResponse,
messages: [],
};
describe("AvataxValidationResponseResolver", () => {
const responseResolver = new AvataxValidationResponseResolver();
it("should throw error when messages", () => {
expect(() => responseResolver.resolve(mockFailedValidationResponse)).toThrow();
});
it("should not throw error when no messages", () => {
expect(() => responseResolver.resolve(mockSuccessfulValidationResponse)).not.toThrow();
});
it("should not return anything when no messages", () => {
expect(responseResolver.resolve(mockSuccessfulValidationResponse)).toBeUndefined();
});
});

View file

@ -0,0 +1,11 @@
import { AddressResolutionModel } from "avatax/lib/models/AddressResolutionModel";
export class AvataxValidationResponseResolver {
resolve(response: AddressResolutionModel) {
if (response.messages && response.messages.length > 0) {
throw new Error(
"The provided address is invalid. Please visit https://developer.avalara.com/avatax/address-validation/ to learn about address formatting."
);
}
}
}

View file

@ -5,7 +5,6 @@ import { AvataxClient, VoidTransactionArgs } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema"; import { AvataxConfig } from "../avatax-connection-schema";
import { ClientLogger } from "../../logs/client-logger"; import { ClientLogger } from "../../logs/client-logger";
import { AvataxOrderCancelledPayloadTransformer } from "./avatax-order-cancelled-payload-transformer"; import { AvataxOrderCancelledPayloadTransformer } from "./avatax-order-cancelled-payload-transformer";
import { AvataxErrorNormalizer } from "../avatax-error-normalizer";
export type AvataxOrderCancelledTarget = VoidTransactionArgs; export type AvataxOrderCancelledTarget = VoidTransactionArgs;
@ -44,8 +43,6 @@ export class AvataxOrderCancelledAdapter implements WebhookAdapter<OrderCancelle
this.logger.debug(`Successfully voided the transaction of id: ${target.transactionCode}`); this.logger.debug(`Successfully voided the transaction of id: ${target.transactionCode}`);
} catch (error) { } catch (error) {
const errorNormalizer = new AvataxErrorNormalizer();
this.clientLogger.push({ this.clientLogger.push({
event: "[OrderCancelled] voidTransaction", event: "[OrderCancelled] voidTransaction",
status: "error", status: "error",
@ -54,8 +51,6 @@ export class AvataxOrderCancelledAdapter implements WebhookAdapter<OrderCancelle
output: error, output: error,
}, },
}); });
throw errorNormalizer.normalize(error);
} }
} }
} }

View file

@ -1,14 +1,13 @@
import { AuthData } from "@saleor/app-sdk/APL"; import { AuthData } from "@saleor/app-sdk/APL";
import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql"; import { OrderConfirmedSubscriptionFragment } from "../../../../generated/graphql";
import { Logger, createLogger } from "../../../lib/logger"; import { Logger, createLogger } from "../../../lib/logger";
import { ClientLogger } from "../../logs/client-logger";
import { CreateOrderResponse } from "../../taxes/tax-provider-webhook"; import { CreateOrderResponse } from "../../taxes/tax-provider-webhook";
import { WebhookAdapter } from "../../taxes/tax-webhook-adapter"; import { WebhookAdapter } from "../../taxes/tax-webhook-adapter";
import { AvataxClient } from "../avatax-client"; import { AvataxClient } from "../avatax-client";
import { AvataxConfig } from "../avatax-connection-schema"; import { AvataxConfig } from "../avatax-connection-schema";
import { ClientLogger } from "../../logs/client-logger";
import { AvataxOrderConfirmedPayloadService } from "./avatax-order-confirmed-payload.service"; import { AvataxOrderConfirmedPayloadService } from "./avatax-order-confirmed-payload.service";
import { AvataxOrderConfirmedResponseTransformer } from "./avatax-order-confirmed-response-transformer"; import { AvataxOrderConfirmedResponseTransformer } from "./avatax-order-confirmed-response-transformer";
import { AvataxErrorNormalizer } from "../avatax-error-normalizer";
type AvataxOrderConfirmedPayload = { type AvataxOrderConfirmedPayload = {
order: OrderConfirmedSubscriptionFragment; order: OrderConfirmedSubscriptionFragment;
@ -69,8 +68,6 @@ export class AvataxOrderConfirmedAdapter
return transformedResponse; return transformedResponse;
} catch (error) { } catch (error) {
const errorNormalizer = new AvataxErrorNormalizer();
this.clientLogger.push({ this.clientLogger.push({
event: "[OrderConfirmed] createTransaction", event: "[OrderConfirmed] createTransaction",
status: "error", status: "error",
@ -79,8 +76,7 @@ export class AvataxOrderConfirmedAdapter
output: error, output: error,
}, },
}); });
throw error;
throw errorNormalizer.normalize(error);
} }
} }
} }

View file

@ -7,7 +7,9 @@ export class AvataxOrderConfirmedResponseTransformer {
return { return {
id: taxProviderUtils.resolveOptionalOrThrow( id: taxProviderUtils.resolveOptionalOrThrow(
response.code, response.code,
"Could not update the order metadata with AvaTax transaction code because it was not returned from the createTransaction mutation.", new Error(
"Could not update the order metadata with AvaTax transaction code because it was not returned from the createTransaction mutation."
)
), ),
}; };
} }

View file

@ -1,18 +0,0 @@
import { BaseError } from "../../error";
const TaxError = BaseError.subclass("TaxError");
/*
* Errors that happen if there is not enough data in webhook payload to proceed with the process. Is not reported.
* Better name: BadRequestError?
*/
export const TaxBadWebhookPayloadError = TaxError.subclass("TaxBadWebhookPayloadError");
// Breaks the process. Is reported.
export const TaxCriticalError = TaxError.subclass("TaxCriticalError");
// Error that shouldn't happen. Should provide extra insights for debugging.
export const TaxUnexpectedError = TaxCriticalError.subclass("TaxUnexpectedError");
// Error that happens when external service returns an error
export const TaxExternalError = TaxCriticalError.subclass("TaxExternalError");

View file

@ -9,7 +9,9 @@ describe("taxProviderUtils", () => {
expect(() => taxProviderUtils.resolveOptionalOrThrow(undefined)).toThrowError(); expect(() => taxProviderUtils.resolveOptionalOrThrow(undefined)).toThrowError();
}); });
it("throws a custom error if value is undefined", () => { it("throws a custom error if value is undefined", () => {
expect(() => taxProviderUtils.resolveOptionalOrThrow(undefined, "test")).toThrowError("test"); expect(() =>
taxProviderUtils.resolveOptionalOrThrow(undefined, new Error("test"))
).toThrowError("test");
}), }),
it("returns value if value is not undefined", () => { it("returns value if value is not undefined", () => {
expect(taxProviderUtils.resolveOptionalOrThrow("test")).toBe("test"); expect(taxProviderUtils.resolveOptionalOrThrow("test")).toBe("test");

View file

@ -1,34 +1,25 @@
import { z } from "zod"; import { z } from "zod";
import { TaxUnexpectedError } from "./tax-error";
/* /*
* The providers sdk types claim to sometimes return undefined. * The providers sdk types claim to sometimes return undefined.
* If it ever happens, we have nothing to fall back to, so we throw an error. * If it ever happens, we have nothing to fall back to, so we throw an error.
* Should only be used for values that are required for further calculation. * Should only be used for values that are required for further calculation.
*/ */
function resolveOptionalOrThrow<T>(value: T | undefined | null, errorMessage?: string): T { function resolveOptionalOrThrow<T>(value: T | undefined | null, error?: Error): T {
if (value === undefined || value === null) { if (value === undefined || value === null) {
throw new TaxUnexpectedError( throw error
errorMessage ? error
? errorMessage : new Error("Could not resolve data. Value needed for further calculation is undefined.");
: "Could not resolve data. Value needed for further calculation is undefined.",
);
} }
return value; return value;
} }
function resolveStringOrThrow(value: string | undefined | null): string { function resolveStringOrThrow(value: string | undefined | null): string {
const parseResult = z return z
.string({ required_error: "This field must be defined." }) .string({ required_error: "This field must be defined." })
.min(1, { message: "This field can not be empty." }) .min(1, { message: "This field can not be empty." })
.safeParse(value); .parse(value);
if (!parseResult.success) {
throw new TaxUnexpectedError(parseResult.error.message);
}
return parseResult.data;
} }
export const taxProviderUtils = { export const taxProviderUtils = {

View file

@ -8,7 +8,6 @@ import { TaxJarConfig } from "../taxjar-connection-schema";
import { TaxJarCalculateTaxesPayloadService } from "./taxjar-calculate-taxes-payload-service"; import { TaxJarCalculateTaxesPayloadService } from "./taxjar-calculate-taxes-payload-service";
import { TaxJarCalculateTaxesResponseTransformer } from "./taxjar-calculate-taxes-response-transformer"; import { TaxJarCalculateTaxesResponseTransformer } from "./taxjar-calculate-taxes-response-transformer";
import { ClientLogger } from "../../logs/client-logger"; import { ClientLogger } from "../../logs/client-logger";
import { TaxJarErrorNormalizer } from "../taxjar-error-normalizer";
export type TaxJarCalculateTaxesPayload = { export type TaxJarCalculateTaxesPayload = {
taxBase: TaxBaseFragment; taxBase: TaxBaseFragment;
@ -71,8 +70,6 @@ export class TaxJarCalculateTaxesAdapter
return transformedResponse; return transformedResponse;
} catch (error) { } catch (error) {
const errorNormalizer = new TaxJarErrorNormalizer();
this.clientLogger.push({ this.clientLogger.push({
event: "[CalculateTaxes] fetchTaxForOrder", event: "[CalculateTaxes] fetchTaxForOrder",
status: "error", status: "error",
@ -81,8 +78,7 @@ export class TaxJarCalculateTaxesAdapter
output: error, output: error,
}, },
}); });
throw error;
throw errorNormalizer.normalize(error);
} }
} }
} }

View file

@ -13,14 +13,14 @@ import {
*/ */
export function matchPayloadLinesToResponseLines( export function matchPayloadLinesToResponseLines(
payloadLines: TaxBaseFragment["lines"], payloadLines: TaxBaseFragment["lines"],
responseLines: NonNullable<Breakdown["line_items"]>, responseLines: NonNullable<Breakdown["line_items"]>
) { ) {
return payloadLines.map((payloadLine) => { return payloadLines.map((payloadLine) => {
const responseLine = responseLines.find((line) => line.id === payloadLine.sourceLine.id); const responseLine = responseLines.find((line) => line.id === payloadLine.sourceLine.id);
if (!responseLine) { if (!responseLine) {
throw new Error( throw new Error(
`Saleor product line with id ${payloadLine.sourceLine.id} not found in TaxJar response.`, `Saleor product line with id ${payloadLine.sourceLine.id} not found in TaxJar response.`
); );
} }
@ -31,7 +31,7 @@ export function matchPayloadLinesToResponseLines(
export class TaxJarCalculateTaxesResponseLinesTransformer { export class TaxJarCalculateTaxesResponseLinesTransformer {
transform( transform(
payload: TaxJarCalculateTaxesPayload, payload: TaxJarCalculateTaxesPayload,
response: TaxForOrderRes, response: TaxForOrderRes
): TaxJarCalculateTaxesResponse["lines"] { ): TaxJarCalculateTaxesResponse["lines"] {
const responseLines = response.tax.breakdown?.line_items ?? []; const responseLines = response.tax.breakdown?.line_items ?? [];
@ -40,15 +40,15 @@ export class TaxJarCalculateTaxesResponseLinesTransformer {
return lines.map((line) => { return lines.map((line) => {
const taxableAmount = taxProviderUtils.resolveOptionalOrThrow( const taxableAmount = taxProviderUtils.resolveOptionalOrThrow(
line?.taxable_amount, line?.taxable_amount,
"Line taxable amount is required to calculate net amount", new Error("Line taxable amount is required to calculate net amount")
); );
const taxCollectable = taxProviderUtils.resolveOptionalOrThrow( const taxCollectable = taxProviderUtils.resolveOptionalOrThrow(
line?.tax_collectable, line?.tax_collectable,
"Line tax collectable is required to calculate net amount", new Error("Line tax collectable is required to calculate net amount")
); );
const taxRate = taxProviderUtils.resolveOptionalOrThrow( const taxRate = taxProviderUtils.resolveOptionalOrThrow(
line?.combined_tax_rate, line?.combined_tax_rate,
"Line combined tax rate is required to calculate net amount", new Error("Line combined tax rate is required to calculate net amount")
); );
return { return {

View file

@ -8,7 +8,6 @@ import { TaxJarConfig } from "../taxjar-connection-schema";
import { TaxJarOrderConfirmedPayloadService } from "./taxjar-order-confirmed-payload.service"; import { TaxJarOrderConfirmedPayloadService } from "./taxjar-order-confirmed-payload.service";
import { TaxJarOrderConfirmedResponseTransformer } from "./taxjar-order-confirmed-response-transformer"; import { TaxJarOrderConfirmedResponseTransformer } from "./taxjar-order-confirmed-response-transformer";
import { ClientLogger } from "../../logs/client-logger"; import { ClientLogger } from "../../logs/client-logger";
import { TaxJarErrorNormalizer } from "../taxjar-error-normalizer";
export type TaxJarOrderConfirmedPayload = { export type TaxJarOrderConfirmedPayload = {
order: OrderConfirmedSubscriptionFragment; order: OrderConfirmedSubscriptionFragment;
@ -68,8 +67,6 @@ export class TaxJarOrderConfirmedAdapter
return transformedResponse; return transformedResponse;
} catch (error) { } catch (error) {
const errorNormalizer = new TaxJarErrorNormalizer();
this.clientLogger.push({ this.clientLogger.push({
event: "[OrderConfirmed] createOrder", event: "[OrderConfirmed] createOrder",
status: "error", status: "error",
@ -78,7 +75,7 @@ export class TaxJarOrderConfirmedAdapter
output: error, output: error,
}, },
}); });
throw errorNormalizer.normalize(error); throw error;
} }
} }
} }

View file

@ -15,14 +15,14 @@ export function sumPayloadLines(lines: LineItem[]): number {
prev + prev +
taxProviderUtils.resolveOptionalOrThrow( taxProviderUtils.resolveOptionalOrThrow(
line.unit_price, line.unit_price,
"Line unit_price is required to calculate order taxes", new Error("Line unit_price is required to calculate order taxes")
) * ) *
taxProviderUtils.resolveOptionalOrThrow( taxProviderUtils.resolveOptionalOrThrow(
line.quantity, line.quantity,
"Line quantity is required to calculate order taxes", new Error("Line quantity is required to calculate order taxes")
),
0,
), ),
0
)
); );
} }
@ -30,7 +30,7 @@ export class TaxJarOrderConfirmedPayloadTransformer {
transform( transform(
order: OrderConfirmedSubscriptionFragment, order: OrderConfirmedSubscriptionFragment,
taxJarConfig: TaxJarConfig, taxJarConfig: TaxJarConfig,
matches: TaxJarTaxCodeMatches, matches: TaxJarTaxCodeMatches
): TaxJarOrderConfirmedTarget { ): TaxJarOrderConfirmedTarget {
const linesTransformer = new TaxJarOrderConfirmedPayloadLinesTransformer(); const linesTransformer = new TaxJarOrderConfirmedPayloadLinesTransformer();
const lineItems = linesTransformer.transform(order.lines, matches); const lineItems = linesTransformer.transform(order.lines, matches);

View file

@ -1,17 +0,0 @@
import { TaxjarError } from "taxjar/dist/util/types";
import { createLogger } from "../../lib/logger";
import { TaxExternalError, TaxUnexpectedError } from "../taxes/tax-error";
export class TaxJarErrorNormalizer {
private logger = createLogger({ name: "TaxJarErrorNormalizer" });
normalize(error: unknown) {
if (error instanceof TaxjarError) {
this.logger.debug(error.stack, "TaxjarError occurred");
return new TaxExternalError(error.message);
}
return TaxUnexpectedError.normalize(error);
}
}

View file

@ -7,7 +7,6 @@ import { saleorApp } from "../../../../saleor-app";
import { createLogger } from "../../../lib/logger"; import { createLogger } from "../../../lib/logger";
import { WebhookResponse } from "../../../modules/app/webhook-response"; import { WebhookResponse } from "../../../modules/app/webhook-response";
import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service"; import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service";
import { TaxBadWebhookPayloadError } from "../../../modules/taxes/tax-error";
export const config = { export const config = {
api: { api: {
@ -22,11 +21,11 @@ export type CalculateTaxesPayload = Extract<
function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) { function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) {
if (!payload.taxBase.lines.length) { if (!payload.taxBase.lines.length) {
throw new TaxBadWebhookPayloadError("No lines found in taxBase"); throw new Error("No lines found in taxBase");
} }
if (!payload.taxBase.address) { if (!payload.taxBase.address) {
throw new TaxBadWebhookPayloadError("No address found in taxBase"); throw new Error("No address found in taxBase");
} }
return payload; return payload;

View file

@ -7,7 +7,6 @@ import { saleorApp } from "../../../../saleor-app";
import { createLogger } from "../../../lib/logger"; import { createLogger } from "../../../lib/logger";
import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service"; import { getActiveConnectionService } from "../../../modules/taxes/get-active-connection-service";
import { WebhookResponse } from "../../../modules/app/webhook-response"; import { WebhookResponse } from "../../../modules/app/webhook-response";
import { TaxBadWebhookPayloadError } from "../../../modules/taxes/tax-error";
export const config = { export const config = {
api: { api: {
@ -19,11 +18,11 @@ type CalculateTaxesPayload = Extract<CalculateTaxesEventFragment, { __typename:
function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) { function verifyCalculateTaxesPayload(payload: CalculateTaxesPayload) {
if (!payload.taxBase.lines.length) { if (!payload.taxBase.lines.length) {
throw new TaxBadWebhookPayloadError("No lines found in taxBase"); throw new Error("No lines found in taxBase");
} }
if (!payload.taxBase.address) { if (!payload.taxBase.address) {
throw new TaxBadWebhookPayloadError("No address found in taxBase"); throw new Error("No address found in taxBase");
} }
return payload; return payload;

View file

@ -1,6 +1,5 @@
{ {
"words": [ "words": [
"Autocommit",
"Adyen", "Adyen",
"Afterpay", "Afterpay",
"Algolia", "Algolia",

View file

@ -1,5 +1,9 @@
lockfileVersion: '6.0' lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides: overrides:
'@saleor/app-sdk': 0.43.1 '@saleor/app-sdk': 0.43.1
@ -919,6 +923,9 @@ importers:
'@saleor/react-hook-form-macaw': '@saleor/react-hook-form-macaw':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/react-hook-form-macaw version: link:../../packages/react-hook-form-macaw
'@saleor/webhook-utils':
specifier: workspace:*
version: link:../../packages/webhook-utils
'@sentry/nextjs': '@sentry/nextjs':
specifier: 7.67.0 specifier: 7.67.0
version: 7.67.0(next@13.4.8)(react@18.2.0) version: 7.67.0(next@13.4.8)(react@18.2.0)
@ -943,6 +950,9 @@ importers:
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: 4.0.4 specifier: 4.0.4
version: 4.0.4(vite@4.4.8) version: 4.0.4(vite@4.4.8)
dotenv:
specifier: ^16.3.1
version: 16.3.1
fast-xml-parser: fast-xml-parser:
specifier: ^4.0.15 specifier: ^4.0.15
version: 4.0.15 version: 4.0.15
@ -1517,12 +1527,6 @@ importers:
jsdom: jsdom:
specifier: ^20.0.3 specifier: ^20.0.3
version: 20.0.3 version: 20.0.3
modern-errors:
specifier: ^6.0.0
version: 6.0.0
modern-errors-serialize:
specifier: ^5.0.0
version: 5.0.0(modern-errors@6.0.0)
next: next:
specifier: 13.4.8 specifier: 13.4.8
version: 13.4.8(@babel/core@7.22.17)(react-dom@18.2.0)(react@18.2.0) version: 13.4.8(@babel/core@7.22.17)(react-dom@18.2.0)(react@18.2.0)
@ -14212,34 +14216,11 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/error-class-utils@3.0.0:
resolution: {integrity: sha512-L26cyYkaV6nzbUbmDRNSXAZfcuQy4cvEDvD+WoRF6c6nIEEydfgn7grd+idf2xLVYaTHnn7yYQjaz+Dnx+N1lQ==}
engines: {node: '>=16.17.0'}
dev: false
/error-custom-class@9.0.0:
resolution: {integrity: sha512-cfXOxbwRQpXLUSecZctO/GPtKm9auTd2v1eY4CsclMgRkse/h5w59V1u1p7LdStVnw/SCbROcsd5zLenauvlRw==}
engines: {node: '>=16.17.0'}
dependencies:
error-class-utils: 3.0.0
dev: false
/error-ex@1.3.2: /error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
dependencies: dependencies:
is-arrayish: 0.2.1 is-arrayish: 0.2.1
/error-serializer@6.0.1:
resolution: {integrity: sha512-SDEXcpWyys6yd6zLcC+s5bGnfe+xWxBJoC7p+o72c5F+hDdgdWc8LB8EOvcdqs7U+rzInYldFpiqSwmC3VZUeg==}
engines: {node: '>=16.17.0'}
dependencies:
is-error-instance: 2.0.0
is-plain-obj: 4.1.0
normalize-exception: 3.0.0
safe-json-value: 2.0.1
set-error-class: 2.0.0
dev: false
/error-stack-parser@2.1.4: /error-stack-parser@2.1.4:
resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
dependencies: dependencies:
@ -15231,11 +15212,6 @@ packages:
dependencies: dependencies:
to-regex-range: 5.0.1 to-regex-range: 5.0.1
/filter-obj@5.1.0:
resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
engines: {node: '>=14.16'}
dev: false
/final-form-arrays@3.1.0(final-form@4.20.9): /final-form-arrays@3.1.0(final-form@4.20.9):
resolution: {integrity: sha512-TWBvun+AopgBLw9zfTFHBllnKMVNEwCEyDawphPuBGGqNsuhGzhT7yewHys64KFFwzIs6KEteGLpKOwvTQEscQ==} resolution: {integrity: sha512-TWBvun+AopgBLw9zfTFHBllnKMVNEwCEyDawphPuBGGqNsuhGzhT7yewHys64KFFwzIs6KEteGLpKOwvTQEscQ==}
peerDependencies: peerDependencies:
@ -16392,11 +16368,6 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/is-error-instance@2.0.0:
resolution: {integrity: sha512-5RuM+oFY0P5MRa1nXJo6IcTx9m2VyXYhRtb4h0olsi2GHci4bqZ6akHk+GmCYvDrAR9yInbiYdr2pnoqiOMw/Q==}
engines: {node: '>=16.17.0'}
dev: false
/is-extglob@2.1.1: /is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -16500,11 +16471,6 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
dev: false
/is-plain-object@2.0.4: /is-plain-object@2.0.4:
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -17664,16 +17630,6 @@ packages:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
dev: true dev: true
/merge-error-cause@4.0.1:
resolution: {integrity: sha512-fTPQshSNjhq6BGvoe5F6xezzcWTn98rog8Ra0gJ0jqgwZXizPNRyg/pjhWX5+pXYanecSPUXa17uEM/RwZfKXw==}
engines: {node: '>=16.17.0'}
dependencies:
normalize-exception: 3.0.0
set-error-class: 2.0.0
set-error-props: 5.0.0
wrap-error-message: 2.0.1
dev: false
/merge-stream@2.0.0: /merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true dev: true
@ -18200,32 +18156,6 @@ packages:
pkg-types: 1.0.3 pkg-types: 1.0.3
ufo: 1.2.0 ufo: 1.2.0
/modern-errors-serialize@5.0.0(modern-errors@6.0.0):
resolution: {integrity: sha512-vKtplFTL+nooOabeOsX2ur9tSgXCzvKOY15WAx1bwq9Cz6yETipqKilZX+bGXFrTRptUOotuACRWhQ+46wWguQ==}
engines: {node: '>=16.17.0'}
peerDependencies:
modern-errors: ^6.0.0
dependencies:
error-serializer: 6.0.1
is-plain-obj: 4.1.0
modern-errors: 6.0.0
dev: false
/modern-errors@6.0.0:
resolution: {integrity: sha512-IgtbY9ITQfbtZUdoIiqOwReV+Z2iL82OtwWTNV9cusKT/SvNivIAXKyGjEGcoCpLc+32UZp0DuqXViIDAG44Zg==}
engines: {node: '>=16.17.0'}
dependencies:
error-class-utils: 3.0.0
error-custom-class: 9.0.0
filter-obj: 5.1.0
is-plain-obj: 4.1.0
merge-error-cause: 4.0.1
normalize-exception: 3.0.0
set-error-message: 2.0.1
set-error-props: 5.0.0
set-error-stack: 2.0.0
dev: false
/moment@2.29.4: /moment@2.29.4:
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
dev: false dev: false
@ -18488,14 +18418,6 @@ packages:
abbrev: 1.1.1 abbrev: 1.1.1
dev: false dev: false
/normalize-exception@3.0.0:
resolution: {integrity: sha512-SMZtWSLjls45KBgwvS2jWyXLtOI9j90JyQ6tJstl91Gti4W7QwZyF/nWwlFRz/Cx4Gy70DAtLT0EzXYXcPJJUw==}
engines: {node: '>=16.17.0'}
dependencies:
is-error-instance: 2.0.0
is-plain-obj: 4.1.0
dev: false
/normalize-package-data@2.5.0: /normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies: dependencies:
@ -20046,13 +19968,6 @@ packages:
resolve: 1.22.2 resolve: 1.22.2
dev: true dev: true
/redefine-property@2.0.0:
resolution: {integrity: sha512-7UfHFiHkePd9mb/vYMPYuAPjAa/77xGQ1S6laaWNQkz5gVJAtYpoWYQ5iFL/ZcDxXZVqnD7N4aFFnIn4T36Sbw==}
engines: {node: '>=16.17.0'}
dependencies:
is-plain-obj: 4.1.0
dev: false
/redent@3.0.0: /redent@3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -20395,14 +20310,6 @@ packages:
/safe-buffer@5.2.1: /safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
/safe-json-value@2.0.1:
resolution: {integrity: sha512-vvoBxKVyksxwqzNDoD2vLVkcvbjYBFXS/CghUrFDsrP0wgTaw+/gIyOADNYa1vyPmICLUWH7RNh0FtwmFsEQCQ==}
engines: {node: '>=16.17.0'}
dependencies:
is-plain-obj: 4.1.0
normalize-exception: 3.0.0
dev: false
/safe-regex-test@1.0.0: /safe-regex-test@1.0.0:
resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==}
dependencies: dependencies:
@ -20560,36 +20467,6 @@ packages:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
dev: true dev: true
/set-error-class@2.0.0:
resolution: {integrity: sha512-ZBXDmoj+bWd+vJbA8VZE/aVQ6NL5iu2AVMtUyVIVXVMEi4oozCGPZAPjaJJZ4k8koLYb0OAFcyIRb0T6XiCuXg==}
engines: {node: '>=16.17.0'}
dependencies:
normalize-exception: 3.0.0
dev: false
/set-error-message@2.0.1:
resolution: {integrity: sha512-s/eeP0f4ed1S3fl0KbxZoy5Pbeg5D6Nbple9nut4VPwHTvEIk5r7vKq0FwjNjszdUPdlTrs4GJCOkWUqWeTeWg==}
engines: {node: '>=16.17.0'}
dependencies:
normalize-exception: 3.0.0
dev: false
/set-error-props@5.0.0:
resolution: {integrity: sha512-AKeNtJ7f9HUzB9Vw9KWiKKe6NR5b8hJoVVnXGN+ZkEj0jTfM0ggL+I2O/14zfJn9lgUqGgMgyjjRhldp7eTpeA==}
engines: {node: '>=16.17.0'}
dependencies:
is-error-instance: 2.0.0
is-plain-obj: 4.1.0
redefine-property: 2.0.0
dev: false
/set-error-stack@2.0.0:
resolution: {integrity: sha512-mABWr7mmaY1EVBMXWo32t6byRkKclJ3gipglE2+XGBZxDEk0+zVumRfWyAK3s/EB/TbbUm1Gp0H8VvqlFkMa+g==}
engines: {node: '>=16.17.0'}
dependencies:
normalize-exception: 3.0.0
dev: false
/set-harmonic-interval@1.0.1: /set-harmonic-interval@1.0.1:
resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==}
engines: {node: '>=6.9'} engines: {node: '>=6.9'}
@ -22589,14 +22466,6 @@ packages:
string-width: 4.2.3 string-width: 4.2.3
strip-ansi: 6.0.1 strip-ansi: 6.0.1
/wrap-error-message@2.0.1:
resolution: {integrity: sha512-LrBMsWJ85HKjLs5ABjhZeW7mWpwsAoV16iqmhEXUf4Y2GvdLwrqK4FPGNNoAi7a20wy4wHU2ci61wQfcOgz/Kw==}
engines: {node: '>=16.17.0'}
dependencies:
normalize-exception: 3.0.0
set-error-message: 2.0.1
dev: false
/wrappy@1.0.2: /wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
@ -22810,7 +22679,3 @@ packages:
/zod@3.21.4: /zod@3.21.4:
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false