Skip to content

app deleted shared handler#2343

Draft
lkostrowski wants to merge 13 commits intomainfrom
app-deleted-handler
Draft

app deleted shared handler#2343
lkostrowski wants to merge 13 commits intomainfrom
app-deleted-handler

Conversation

@lkostrowski
Copy link
Copy Markdown
Member

Scope of the PR

Related issues

Checklist

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
saleor-app-avatax Ready Ready Preview, Comment Apr 28, 2026 10:07am
saleor-app-cms Ready Ready Preview, Comment Apr 28, 2026 10:07am
saleor-app-klaviyo Ready Ready Preview, Comment Apr 28, 2026 10:07am
saleor-app-payment-np-atobarai Error Error Comment Apr 28, 2026 10:07am
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
saleor-app-payment-stripe Skipped Skipped Apr 28, 2026 10:07am
saleor-app-products-feed Skipped Skipped Comment Apr 28, 2026 10:07am
saleor-app-search Skipped Skipped Comment Apr 28, 2026 10:07am
saleor-app-segment Skipped Skipped Comment Apr 28, 2026 10:07am
saleor-app-smtp Skipped Skipped Comment Apr 28, 2026 10:07am

Request Review

Copilot AI review requested due to automatic review settings April 23, 2026 13:09
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 23, 2026

🦋 Changeset detected

Latest commit: 0418fee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
saleor-app-klaviyo Minor
@saleor/webhook-utils Minor
saleor-app-avatax Patch
saleor-app-cms Patch
saleor-app-products-feed Patch
saleor-app-search Patch
saleor-app-segment Patch
saleor-app-smtp Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 23, 2026

Differences Found

✅ No packages or licenses were added.

Summary

Expand
License Name Package Count Packages
0BSD 1
Packages
  • tslib
CC BY-SA 4.0 1
Packages
  • @cspell/dict-en-common-misspellings
CC0-1.0 1
Packages
  • type-fest
MIT (http://mootools.net/license.txt) 1
Packages
  • slick
MIT/X11 1
Packages
  • nub
Public Domain 1
Packages
  • jsonify
Python-2.0 1
Packages
  • argparse
SEE LICENSE IN LICENSE 1
Packages
  • spawndamnit
SEE LICENSE IN LICENSE.md 1
Packages
  • cookie-lite
Unlicense 1
Packages
  • @sinonjs/text-encoding
WTFPL 1
Packages
  • opener
BlueOak-1.0.0 3
Packages
  • jackspeak
  • package-json-from-dist
  • path-scurry
CC-BY-4.0 3
Packages
  • @saleor/macaw-ui
  • caniuse-lite
  • saleor-apps
LGPL-3.0-or-later 11
Packages
  • @img/sharp-libvips-darwin-arm64
  • @img/sharp-libvips-darwin-x64
  • @img/sharp-libvips-linux-arm
  • @img/sharp-libvips-linux-arm64
  • @img/sharp-libvips-linux-s390x
  • @img/sharp-libvips-linux-x64
  • @img/sharp-libvips-linuxmusl-arm64
  • @img/sharp-libvips-linuxmusl-x64
  • @img/sharp-wasm32
  • @img/sharp-win32-ia32
  • @img/sharp-win32-x64
BSD-2-Clause 22
Packages
  • cheerio-select
  • css-select
  • css-what
  • domelementtype
  • domhandler
  • domutils
  • dotenv
  • entities
  • escodegen
  • eslint-scope
  • espree
  • esprima
  • esrecurse
  • estraverse
  • esutils
  • glob-to-regexp
  • nth-check
  • shimmer
  • terser
  • uglify-js
  • And 2 more...
<<missing>> 27
Packages
  • @saleor/app-problems
  • @saleor/apps-domain
  • @saleor/apps-logger
  • @saleor/apps-otel
  • @saleor/apps-shared
  • @saleor/apps-trpc
  • @saleor/apps-ui
  • @saleor/dynamo-config-repository
  • @saleor/errors
  • @saleor/eslint-config-apps
  • @saleor/handlebars
  • @saleor/react-hook-form-macaw
  • @saleor/sentry-utils
  • @saleor/typescript-config-apps
  • @saleor/webhook-utils
  • busboy
  • json-query
  • saleor-app-avatax
  • saleor-app-cms
  • saleor-app-klaviyo
  • And 7 more...
BSD-3-Clause 48
Packages
  • @protobufjs/aspromise
  • @protobufjs/base64
  • @protobufjs/codegen
  • @protobufjs/eventemitter
  • @protobufjs/fetch
  • @protobufjs/float
  • @protobufjs/inquire
  • @protobufjs/path
  • @protobufjs/pool
  • @protobufjs/utf8
  • @saleor/app-sdk
  • @saleor/eslint-plugin-saleor-app
  • @sentry/cli
  • @sentry/cli-darwin
  • @sentry/cli-linux-arm
  • @sentry/cli-linux-arm64
  • @sentry/cli-linux-i686
  • @sentry/cli-linux-x64
  • @sentry/cli-win32-i686
  • @sentry/cli-win32-x64
  • And 28 more...
ISC 56
Packages
  • @bundled-es-modules/cookie
  • @bundled-es-modules/statuses
  • @bundled-es-modules/tough-cookie
  • @isaacs/cliui
  • abbrev
  • anymatch
  • boolbase
  • cli-width
  • cliui
  • concat-with-sourcemaps
  • electron-to-chromium
  • fastq
  • flatted
  • foreground-child
  • form-data-lite
  • fs.realpath
  • get-caller-file
  • glob
  • glob-parent
  • graceful-fs
  • And 36 more...
Apache-2.0 239
Packages
  • @ampproject/remapping
  • @aws-crypto/crc32
  • @aws-crypto/crc32c
  • @aws-crypto/ie11-detection
  • @aws-crypto/sha1-browser
  • @aws-crypto/sha256-browser
  • @aws-crypto/sha256-js
  • @aws-crypto/supports-web-crypto
  • @aws-crypto/util
  • @aws-sdk/abort-controller
  • @aws-sdk/chunked-blob-reader
  • @aws-sdk/client-dynamodb
  • @aws-sdk/client-s3
  • @aws-sdk/client-sso
  • @aws-sdk/client-sso-oidc
  • @aws-sdk/client-sts
  • @aws-sdk/config-resolver
  • @aws-sdk/core
  • @aws-sdk/credential-provider-env
  • @aws-sdk/credential-provider-http
  • And 219 more...
MIT 1411
Packages
  • @0no-co/graphql.web
  • @adobe/css-tools
  • @algolia/cache-browser-local-storage
  • @algolia/cache-common
  • @algolia/cache-in-memory
  • @algolia/client-account
  • @algolia/client-analytics
  • @algolia/client-common
  • @algolia/client-personalization
  • @algolia/client-search
  • @algolia/logger-common
  • @algolia/logger-console
  • @algolia/recommend
  • @algolia/requester-browser-xhr
  • @algolia/requester-common
  • @algolia/requester-node-http
  • @algolia/transporter
  • @apidevtools/json-schema-ref-parser
  • @ardatan/relay-compiler
  • @ardatan/sync-fetch
  • And 1391 more...

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 2.45399% with 159 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.76%. Comparing base (17031e8) to head (80db897).

Files with missing lines Patch % Lines
.../avatax/src/modules/client-logs/logs-repository.ts 2.38% 82 Missing ⚠️
...app/api/webhooks/app-deleted/webhook-definition.ts 0.00% 33 Missing and 1 partial ⚠️
packages/webhook-utils/src/app-deleted-handler.ts 2.85% 34 Missing ⚠️
apps/avatax/src/app/api/manifest/route.ts 0.00% 5 Missing ⚠️
...s/avatax/src/app/api/webhooks/app-deleted/route.ts 0.00% 1 Missing and 1 partial ⚠️
...vatax/src/modules/client-logs/dynamo-logs-table.ts 33.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2343      +/-   ##
==========================================
- Coverage   37.84%   37.76%   -0.09%     
==========================================
  Files        1037     1040       +3     
  Lines       66937    67099     +162     
  Branches     3533     3536       +3     
==========================================
+ Hits        25335    25339       +4     
- Misses      41221    41377     +156     
- Partials      381      383       +2     
Flag Coverage Δ
avatax 57.10% <2.34%> (-0.48%) ⬇️
cms 20.36% <ø> (ø)
domain 100.00% <ø> (ø)
dynamo-config-repository 79.29% <ø> (ø)
errors 91.66% <ø> (ø)
logger 28.81% <ø> (ø)
np-atobarai 72.58% <ø> (ø)
products-feed 6.00% <ø> (ø)
search 31.88% <ø> (ø)
segment 33.58% <ø> (ø)
shared 56.07% <ø> (ø)
smtp 35.74% <ø> (ø)
stripe 70.83% <ø> (ø)
webhook-utils 10.50% <2.85%> (-0.52%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a shared APP_DELETED webhook handler to @saleor/webhook-utils and wires it into the Klaviyo app so that APL auth data is cleaned up automatically when an app installation is deleted in Saleor.

Changes:

  • Introduce createAppDeletedHandler() in @saleor/webhook-utils backed by a new AppDeleted GraphQL subscription.
  • Add a new Klaviyo webhook API route (/api/webhooks/app-deleted) and include it in the app manifest.
  • Update workspace deps/lockfile and add a changeset for the new webhook-utils functionality.

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pnpm-lock.yaml Locks new workspace linkage for @saleor/webhook-utils and related dependency graph changes.
packages/webhook-utils/src/app-deleted-handler.ts New shared handler that receives APP_DELETED and deletes APL auth data.
packages/webhook-utils/package.json Exposes a new subpath export for ./app-deleted-handler and declares logger peer dependency.
packages/webhook-utils/graphql/subscriptions/app-deleted.graphql Adds the AppDeleted subscription definition used by the webhook.
packages/webhook-utils/generated/graphql.ts Updates generated GraphQL types/documents to include AppDeleted.
apps/klaviyo/src/pages/api/webhooks/app-deleted.ts New webhook route using the shared handler.
apps/klaviyo/src/pages/api/manifest.ts Adds the APP_DELETED webhook manifest to the Klaviyo app manifest response.
apps/klaviyo/package.json Adds @saleor/webhook-utils dependency.
.changeset/petite-days-watch.md Declares a minor bump for @saleor/webhook-utils for the new shared webhook.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +6 to +10
const { handler, getWebhookManifest } = createAppDeletedHandler({
apl: saleorApp.apl,
logger: createLogger("APP_DELETED handler"),
webhookPath: "api/webhooks/app-deleted",

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike the other webhook routes in this app, this handler is not wrapped with wrapWithLoggerContext and withSpanAttributes, so logs/traces for APP_DELETED may miss the usual context and OpenTelemetry attributes. Consider wrapping the exported handler consistently (or have createAppDeletedHandler expose a wrapped handler option).

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +36
// todo something failing here
console.log("asdf");
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the leftover debug console.log("asdf") (and the adjacent TODO). This will pollute logs in production and bypasses the app logger/observability pipeline used elsewhere in the repo.

Suggested change
// todo something failing here
console.log("asdf");

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +11
import { type Logger } from "@saleor/apps-logger";

import { AppDeletedDocument } from "../generated/graphql";

type Params = {
apl: APL;
webhookPath: string;
logger: Logger;
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new API requires Logger from @saleor/apps-logger, but the package already defines its own Logger type in src/types.ts (used by other exports). Having two incompatible logger types in the same package is confusing for consumers; consider accepting a minimal logger interface (info/warn/error/debug) or reusing the existing src/types.ts Logger shape and letting apps pass their logger adapter.

Suggested change
import { type Logger } from "@saleor/apps-logger";
import { AppDeletedDocument } from "../generated/graphql";
type Params = {
apl: APL;
webhookPath: string;
logger: Logger;
import { AppDeletedDocument } from "../generated/graphql";
type MinimalLogger = {
info: (message: string, ...meta: unknown[]) => void;
warn?: (message: string, ...meta: unknown[]) => void;
error: (message: string, ...meta: unknown[]) => void;
debug?: (message: string, ...meta: unknown[]) => void;
};
type Params = {
apl: APL;
webhookPath: string;
logger: MinimalLogger;

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +28
export const createAppDeletedHandler = ({ apl, webhookPath, hooks = {}, logger }: Params) => {
const webhook = new SaleorAsyncWebhook({
apl,
name: "APP_DELETED",
query: AppDeletedDocument,
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces new webhook-handling behavior (creating a SaleorAsyncWebhook and deleting APL entries) but there are no unit tests covering the success/error paths. Since this package already uses Vitest tests, add tests that verify apl.delete is called with the Saleor API URL and that the handler returns 200 on success and 500 on failure.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +14
const { handler, getWebhookManifest } = createAppDeletedHandler({
apl: saleorApp.apl,
logger: createLogger("APP_DELETED handler"),
webhookPath: "api/webhooks/app-deleted",

hooks: {
onEvent: console.log,
onAuthDataDeleted: console.log,
onAuthDataDeleteError: console.log,
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hooks.onEvent: console.log will likely print the entire WebhookContext, which includes authData (e.g. token) in other webhook handlers. This is a sensitive-data leakage risk; avoid logging the full context and log only non-sensitive fields via the app logger (or remove these hooks from the production route).

Suggested change
const { handler, getWebhookManifest } = createAppDeletedHandler({
apl: saleorApp.apl,
logger: createLogger("APP_DELETED handler"),
webhookPath: "api/webhooks/app-deleted",
hooks: {
onEvent: console.log,
onAuthDataDeleted: console.log,
onAuthDataDeleteError: console.log,
const logger = createLogger("APP_DELETED handler");
const { handler, getWebhookManifest } = createAppDeletedHandler({
apl: saleorApp.apl,
logger,
webhookPath: "api/webhooks/app-deleted",
hooks: {
onEvent: () => logger.info("App deleted webhook event received"),
onAuthDataDeleted: () => logger.info("Auth data deleted after app removal"),
onAuthDataDeleteError: () => logger.error("Failed to delete auth data after app removal"),

Copilot uses AI. Check for mistakes.
});

export { getWebhookManifest };

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API route is missing export const config = { api: { bodyParser: false } }, which all other Saleor webhook routes in this app export. Without disabling Next.js body parsing, webhook signature verification can break because the raw request body is no longer available.

Suggested change
export const config = {
api: {
bodyParser: false,
},
};

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 24, 2026 13:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 16 changed files in this pull request and generated 9 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +25 to +31
const webhook = new SaleorAsyncWebhook({
apl,
name: "APP_DELETED",
query: AppDeletedDocument,
event: "APP_DELETED",
isActive: true,
webhookPath,
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webhook name is set to the all-caps event identifier ("APP_DELETED"). In other apps, webhook name is a human-readable label (e.g. "OrderCancelled", "Customer Created") and shows up in Saleor’s webhook list. Consider changing this to a descriptive name like "App deleted" / "AppDeleted" while keeping event: "APP_DELETED".

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +56
return new Response('"Failed to clean up auth data."', { status: 500 });
}
} catch (e) {
logger.error("Failed to execute APP_DELETED event", { error: e });

return new Response('"Failed to clean up auth data."', { status: 500 });
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error responses return a body that includes extra quote characters ('"Failed to clean up auth data."'). This will send the quotes to the client and looks accidental; return a plain string (or JSON) without embedded quotes.

Suggested change
return new Response('"Failed to clean up auth data."', { status: 500 });
}
} catch (e) {
logger.error("Failed to execute APP_DELETED event", { error: e });
return new Response('"Failed to clean up auth data."', { status: 500 });
return new Response("Failed to clean up auth data.", { status: 500 });
}
} catch (e) {
logger.error("Failed to execute APP_DELETED event", { error: e });
return new Response("Failed to clean up auth data.", { status: 500 });

Copilot uses AI. Check for mistakes.
Comment on lines +491 to +493
const [saleorApiUrl] = LogsTable.decomposePrimaryKey(log.id);

return args.saleorApiUrl !== saleorApiUrl;
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LogsRepositoryMemory.pruneAllLogs() calls LogsTable.decomposePrimaryKey(log.id), but ClientLog.id is an ULID (see LogsTransformer mapping id: entity.ulid). Splitting the ULID by # won’t yield saleorApiUrl, so this prune will never remove any logs.

Suggested change
const [saleorApiUrl] = LogsTable.decomposePrimaryKey(log.id);
return args.saleorApiUrl !== saleorApiUrl;
return args.saleorApiUrl !== log.saleorApiUrl;

Copilot uses AI. Check for mistakes.

async pruneAllLogs(args: {
saleorApiUrl: string;
appId: string;
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LogsRepositoryMemory.pruneAllLogs() requires { saleorApiUrl, appId }, but ILogsRepository.pruneAllLogs() is declared as { saleorApiUrl }. Even if this currently compiles, it’s inconsistent and can break callers that use the interface type; align the method signature with the interface (and drop appId if it’s not needed).

Suggested change
appId: string;

Copilot uses AI. Check for mistakes.
Comment on lines +349 to +362
do {
const scanResult = await ResultAsync.fromPromise(
this.logsTable
.build(ScanCommand)
.entities(this.logByDateEntity, this.logsByCheckoutOrOrderId)
.options({
exclusiveStartKey: lastEvaluatedKey,
showEntityAttr: true,
filters: {
LOG_BY_DATE: { attr: "PK", beginsWith: pkPrefix },
LOG_BY_CHECKOUT_OR_ORDER_ID: { attr: "PK", beginsWith: pkPrefix },
},
})
.send(),
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pruneAllLogs() performs a full table Scan and filters by PK prefix. For a logs table that can grow large, this can be slow/expensive and may exceed webhook execution time limits during app deletion. Consider an access pattern that avoids scanning (e.g., a GSI partitioned by saleorApiUrl, or storing logs under a dedicated PK for saleorApiUrl so it can be queried and deleted in batches).

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +38
const repo = new LogsRepositoryDynamodb({
logsTable,
logByDateEntity,
logByCheckoutOrOrderId,
});

await repo.pruneAllLogs({ saleorApiUrl: authData.saleorApiUrl });
},
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pruneAllLogs() call returns a Result, but it’s awaited and ignored. If pruning fails, it will be silently skipped (and because it doesn’t throw, the shared handler will continue). Handle the Result explicitly (log on error or convert to a thrown error if you want the webhook to fail).

Copilot uses AI. Check for mistakes.
"saleor-app-klaviyo": minor
---

App now will listen on APP_DELETED webhook and clean up it's Auth Data once removed
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct grammar in the changeset text: “clean up it's Auth Data” should use the possessive “its”.

Suggested change
App now will listen on APP_DELETED webhook and clean up it's Auth Data once removed
App now will listen on APP_DELETED webhook and clean up its Auth Data once removed

Copilot uses AI. Check for mistakes.
Comment on lines +382 to +392
const requests = chunk.map((item) =>
item.entity === "LOG_BY_DATE"
? this.logByDateEntity
.build(BatchDeleteRequest)
.key({ PK: item.PK, ulid: item.ulid, date: item.date })
: this.logsByCheckoutOrOrderId.build(BatchDeleteRequest).key({
PK: item.PK,
ulid: item.ulid,
date: item.date,
checkoutOrOrderId: item.checkoutOrOrderId,
}),
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pruneAllLogs() relies on item.entity to detect which entity a scanned item belongs to. In this table the entity attribute is _et (see LogsTable generic and existing tests asserting _et: "LOG_BY_DATE"/"LOG_BY_CHECKOUT_OR_ORDER_ID"), so this check will likely always fail and the wrong delete key shape will be built.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +13
import { AppDeletedDocument } from "../generated/graphql";

type Params = {
apl: APL;
webhookPath: string;
logger: Logger;
hooks?: {
onEvent?: (ctx: WebhookContext<unknown>) => Promise<void>;
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hooks.onEvent callback is typed as WebhookContext<unknown>, which loses payload typing for consumers. Since this handler is tied to AppDeletedDocument, it can expose a concrete context type (e.g. WebhookContext<AppDeletedSubscription>) to keep downstream hooks type-safe.

Suggested change
import { AppDeletedDocument } from "../generated/graphql";
type Params = {
apl: APL;
webhookPath: string;
logger: Logger;
hooks?: {
onEvent?: (ctx: WebhookContext<unknown>) => Promise<void>;
import { AppDeletedDocument, type AppDeletedSubscription } from "../generated/graphql";
type Params = {
apl: APL;
webhookPath: string;
logger: Logger;
hooks?: {
onEvent?: (ctx: WebhookContext<AppDeletedSubscription>) => Promise<void>;

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 27, 2026 19:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 19 changed files in this pull request and generated 4 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +24 to +58
export const createAppDeletedHandler = ({ apl, webhookPath, hooks = {}, logger }: Params) => {
const webhook = new SaleorAsyncWebhook({
apl,
name: "APP_DELETED",
query: AppDeletedDocument,
event: "APP_DELETED",
isActive: true,
webhookPath,
});

const handler = webhook.createHandler(async (_req, ctx) => {
try {
logger.info("APP_DELETED event received. Auth Data will be removed");

await hooks.onEvent?.(ctx);

try {
await apl.delete(ctx.authData.saleorApiUrl);

await hooks.onAuthDataDeleted?.();

return new Response("ok", { status: 200 });
} catch (e) {
logger.error("Error deleting auth data on APP_DELETED", { error: e });

await hooks.onAuthDataDeleteError?.(e as Error);

return new Response('"Failed to clean up auth data."', { status: 500 });
}
} catch (e) {
logger.error("Failed to execute APP_DELETED event", { error: e });

return new Response('"Failed to clean up auth data."', { status: 500 });
}
});
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new shared handler introduces non-trivial behavior (invoking hooks, deleting APL auth data, and mapping failures to HTTP responses) but there are no unit tests covering success/failure paths. Add tests for: (1) calling apl.delete with ctx.authData.saleorApiUrl, (2) hook invocation order, and (3) 500 responses + onAuthDataDeleteError on deletion failure.

Copilot uses AI. Check for mistakes.
Comment on lines +485 to +494
async pruneAllLogs(args: {
saleorApiUrl: string;
appId: string;
}): Promise<Result<undefined, unknown>> {
this.logs = this.logs.filter((l) => {
const log = l.getValue();
const [saleorApiUrl] = LogsTable.decomposePrimaryKey(log.id);

return args.saleorApiUrl !== saleorApiUrl;
});
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LogsRepositoryMemory.pruneAllLogs has a signature that doesn't match ILogsRepository (it requires appId, while the interface does not), which will break TypeScript compilation. Also, the pruning logic splits log.id using LogsTable.decomposePrimaryKey, but ClientLog.id is just a ULID (see LogsTransformer mapping), so this filter will never match and logs won't be pruned. Align the method signature with the interface and store enough context in the memory repo (e.g. persist PK / saleorApiUrl alongside the log) to make pruning work.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +37
const repo = new LogsRepositoryDynamodb({
logsTable,
logByDateEntity,
logByCheckoutOrOrderId,
});

await repo.pruneAllLogs({ saleorApiUrl: authData.saleorApiUrl });
},
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repo.pruneAllLogs returns a Result, but the returned value is ignored here. That means pruning failures will be silently swallowed and the webhook will continue (and still delete APL auth data), making cleanup unreliable and hard to diagnose. Handle the Result explicitly (log and/or throw on Err) so failures are observable and can fail the webhook when appropriate.

Copilot uses AI. Check for mistakes.
Comment on lines +330 to +349
async pruneAllLogs({
saleorApiUrl,
}: {
saleorApiUrl: string;
}): Promise<
Result<
undefined,
| InstanceType<typeof LogsRepositoryDynamodb.LogsFetchError>
| InstanceType<typeof LogsRepositoryDynamodb.WriteLogError>
| InstanceType<typeof LogsRepositoryDynamodb.UnprocessedItemsError>
>
> {
this.logger.debug("Starting pruning logs for saleorApiUrl", { saleorApiUrl });

const pkPrefix = `${saleorApiUrl}#`;

let lastEvaluatedKey: LastEvaluatedKey;
let deletedCount = 0;

do {
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pruneAllLogs is a new public method on ILogsRepository / LogsRepositoryDynamodb, and this module already has unit tests (logs-repository.test.ts), but there are no tests validating pruning behavior (e.g. scan pagination, chunking into 25-item batch deletes, and handling of UnprocessedItems). Add unit tests (with DynamoDB toolbox client mocked) to cover at least the happy path and the UnprocessedItems error path.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants