From 776cb9cc583b74beeaf3a3383e0aeccd8852d85b Mon Sep 17 00:00:00 2001 From: David Peacock Date: Thu, 20 Aug 2026 16:06:55 -0400 Subject: [PATCH] Read single-subscription events modules back into the TOML subscription list Core is moving events modules to a one-module-per-subscription structure: each module's config carries events.subscription as a single object instead of a list. transformToEventsConfig assumed a list and called .map on it, so reading remote configuration (app config link, deploy diffing) would throw once an app's modules are stored in the new shape. Normalize the subscription value to a list before stripping the server-managed identifier. Multiple single-subscription modules then deep-merge back into one TOML subscription array through the existing remoteAppConfigurationExtensionContent union-array merge, exactly like webhook_subscription modules do today. Legacy aggregate modules are unaffected. Assisted-By: devx/8f786480-3b9f-4b46-b2d1-8c99989471b2 --- .../events-single-subscription-readback.md | 5 ++ .../transform/app_config_events.test.ts | 76 +++++++++++++++++++ .../transform/app_config_events.ts | 19 ++++- 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 .changeset/events-single-subscription-readback.md diff --git a/.changeset/events-single-subscription-readback.md b/.changeset/events-single-subscription-readback.md new file mode 100644 index 00000000000..92f4799b615 --- /dev/null +++ b/.changeset/events-single-subscription-readback.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Read events modules stored as one module per subscription back into the TOML subscription list diff --git a/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.test.ts b/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.test.ts index f78346c1a2f..a7d960e0ce9 100644 --- a/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.test.ts +++ b/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.test.ts @@ -1,4 +1,5 @@ import {transformToEventsConfig, transformFromEventsConfig} from './app_config_events.js' +import {deepMergeObjects} from '@shopify/cli-kit/common/object' import {describe, expect, test} from 'vitest' describe('transformFromEventsConfig', () => { @@ -192,4 +193,79 @@ describe('transformToEventsConfig', () => { }, }) }) + + test('wraps a single-subscription object into a one-element subscription list and strips identifier', () => { + const remoteContent = { + events: { + api_version: '2024-01', + subscription: { + topic: 'Product', + actions: ['update'], + uri: 'https://example.com/webhook', + handle: 'product-updates', + identifier: 'id-1', + }, + }, + } + + const result = transformToEventsConfig(remoteContent) + + expect(result).toEqual({ + events: { + api_version: '2024-01', + subscription: [ + { + topic: 'Product', + actions: ['update'], + uri: 'https://example.com/webhook', + handle: 'product-updates', + }, + ], + }, + }) + }) + + test('multiple single-subscription modules deep-merge into one subscription list', () => { + const moduleConfigs = [ + { + events: { + api_version: '2024-01', + subscription: { + topic: 'Product', + actions: ['update'], + uri: 'https://example.com/a', + handle: 'a', + identifier: 'id-a', + }, + }, + }, + { + events: { + api_version: '2024-01', + subscription: { + topic: 'Order', + actions: ['create'], + uri: 'https://example.com/b', + handle: 'b', + identifier: 'id-b', + }, + }, + }, + ] + + const merged = moduleConfigs.reduce( + (accumulator, moduleConfig) => deepMergeObjects(accumulator, transformToEventsConfig(moduleConfig)), + {}, + ) + + expect(merged).toEqual({ + events: { + api_version: '2024-01', + subscription: [ + {topic: 'Product', actions: ['update'], uri: 'https://example.com/a', handle: 'a'}, + {topic: 'Order', actions: ['create'], uri: 'https://example.com/b', handle: 'b'}, + ], + }, + }) + }) }) diff --git a/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.ts b/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.ts index da5883ce1de..ad880a42fcc 100644 --- a/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.ts +++ b/packages/app/src/cli/models/extensions/specifications/transform/app_config_events.ts @@ -42,11 +42,20 @@ export function transformFromEventsConfig(content: object, appConfiguration?: ob /** * Transforms the events config from remote to local format. * Strips the server-managed 'identifier' field from subscriptions. + * + * The server stores events modules in two shapes: a legacy aggregate module + * whose subscription is a list, and one module per subscription whose + * subscription is a single object. Both are normalized to a list here so that + * multiple single-subscription modules deep-merge back into the TOML + * subscription array. */ export function transformToEventsConfig(content: object) { - const eventsConfig = getPathValue(content, 'events') as {api_version: string; subscription: object[]} + const eventsConfig = getPathValue(content, 'events') as { + api_version: string + subscription: object[] | object + } const apiVersion = getPathValue(eventsConfig, 'api_version') - const subscription = getPathValue(eventsConfig, 'subscription') as {identifier: string}[] + const subscription = normalizeSubscriptions(getPathValue(eventsConfig, 'subscription')) // Server always includes identifier - strip it for local TOML const cleanedSubscriptions = subscription?.map((sub) => { @@ -59,3 +68,9 @@ export function transformToEventsConfig(content: object) { return {events} } + +function normalizeSubscriptions(subscription: unknown): {identifier?: string}[] | undefined { + if (subscription === undefined) return undefined + if (Array.isArray(subscription)) return subscription as {identifier?: string}[] + return [subscription as {identifier?: string}] +}