Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/events-config-link-shape-tolerance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/app': patch
---

Support single-subscription events modules when reading remote app configuration
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -115,6 +116,25 @@ describe('transformFromEventsConfig', () => {
expect(result).toEqual(content)
})

test('prepends application_url to a relative URI in a single subscription object', () => {
const content = {
events: {
api_version: '2024-01',
subscription: {topic: 'orders/create', uri: '/webhooks/orders', actions: ['create']},
},
}
const appConfiguration = {application_url: 'https://tunnel.example.com'}

const result = transformFromEventsConfig(content, appConfiguration)

expect(result).toEqual({
events: {
api_version: '2024-01',
subscription: {topic: 'orders/create', uri: 'https://tunnel.example.com/webhooks/orders', actions: ['create']},
},
})
})

test('returns content as-is when events is undefined', () => {
const content = {}
const appConfiguration = {application_url: 'https://tunnel.example.com'}
Expand Down Expand Up @@ -192,4 +212,108 @@ describe('transformToEventsConfig', () => {
},
})
})
test('strips the identifier from a single subscription object and returns it as a one-element array', () => {
const remoteContent = {
events: {
api_version: '2024-01',
subscription: {
topic: 'orders/create',
uri: 'https://example.com/webhook',
actions: ['create'],
handle: 'order-notifier',
identifier: 'id-1',
},
},
}

const result = transformToEventsConfig(remoteContent)

expect(result).toEqual({
events: {
api_version: '2024-01',
subscription: [
{
topic: 'orders/create',
uri: 'https://example.com/webhook',
actions: ['create'],
handle: 'order-notifier',
},
],
},
})
})

test('merging multiple single-subscription modules accumulates one subscription array', () => {
const moduleOne = {
events: {
api_version: '2024-01',
subscription: {
topic: 'orders/create',
uri: 'https://example.com/a',
actions: ['create'],
handle: 'a',
identifier: 'id-a',
},
},
}
const moduleTwo = {
events: {
api_version: '2024-01',
subscription: {
topic: 'products/update',
uri: 'https://example.com/b',
actions: ['update'],
handle: 'b',
identifier: 'id-b',
},
},
}

const merged = deepMergeObjects(transformToEventsConfig(moduleOne), transformToEventsConfig(moduleTwo))

expect(merged).toEqual({
events: {
api_version: '2024-01',
subscription: [
{topic: 'orders/create', uri: 'https://example.com/a', actions: ['create'], handle: 'a'},
{topic: 'products/update', uri: 'https://example.com/b', actions: ['update'], handle: 'b'},
],
},
})
})

test('merging a list-shape module with a single-subscription module accumulates all subscriptions', () => {
const listModule = {
events: {
api_version: '2024-01',
subscription: [
{topic: 'orders/create', uri: 'https://example.com/a', actions: ['create'], handle: 'a', identifier: 'id-a'},
],
},
}
const singleModule = {
events: {
api_version: '2024-01',
subscription: {
topic: 'products/update',
uri: 'https://example.com/b',
actions: ['update'],
handle: 'b',
identifier: 'id-b',
},
},
}

const merged = deepMergeObjects(transformToEventsConfig(listModule), transformToEventsConfig(singleModule))

expect(merged).toEqual({
events: {
api_version: '2024-01',
subscription: [
{topic: 'orders/create', uri: 'https://example.com/a', actions: ['create'], handle: 'a'},
{topic: 'products/update', uri: 'https://example.com/b', actions: ['update'], handle: 'b'},
],
},
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import {prependApplicationUrl} from '../validation/url_prepender.js'
import {CurrentAppConfiguration} from '../../../app/app.js'
import {getPathValue} from '@shopify/cli-kit/common/object'

interface EventSubscription {
uri: string
[key: string]: unknown
}

interface EventsConfig {
events?: {
api_version?: string
subscription?: {uri: string; [key: string]: unknown}[]
subscription?: EventSubscription | EventSubscription[]
}
}

Expand All @@ -27,14 +32,17 @@ export function transformFromEventsConfig(content: object, appConfiguration?: ob
appUrl = (appConfiguration as CurrentAppConfiguration)?.application_url
}

const subscription = eventsConfig.events.subscription
const resolved = wrapSubscriptions(subscription).map((sub) => ({
...sub,
uri: prependApplicationUrl(sub.uri, appUrl),
}))

return {
...eventsConfig,
events: {
...eventsConfig.events,
subscription: eventsConfig.events.subscription.map((sub) => ({
...sub,
uri: prependApplicationUrl(sub.uri, appUrl),
})),
subscription: Array.isArray(subscription) ? resolved : resolved[0],
},
}
}
Expand All @@ -44,18 +52,30 @@ export function transformFromEventsConfig(content: object, appConfiguration?: ob
* Strips the server-managed 'identifier' field from subscriptions.
*/
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: {identifier: string} | {identifier: string}[]
}
const apiVersion = getPathValue(eventsConfig, 'api_version')
const subscription = getPathValue(eventsConfig, 'subscription') as {identifier: string}[]
const subscription = getPathValue<{identifier: string} | {identifier: string}[]>(eventsConfig, 'subscription')

// Server always includes identifier - strip it for local TOML
const cleanedSubscriptions = subscription?.map((sub) => {
const {identifier, ...rest} = sub
return rest
})
// Server always includes identifier - strip it for local TOML.
// Single-subscription modules are normalized to a one-element array so that
// merging multiple modules accumulates a single subscription list.
const cleanedSubscriptions =
subscription === undefined
? undefined
: wrapSubscriptions(subscription).map((sub) => {
const {identifier, ...rest} = sub
return rest
})

const events =
(apiVersion ?? cleanedSubscriptions) ? {api_version: apiVersion, subscription: cleanedSubscriptions} : {}

return {events}
}

function wrapSubscriptions<T>(subscription: T | T[]): T[] {
return Array.isArray(subscription) ? subscription : [subscription]
}
Loading