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}] +}