Skip to content

Commit eea7761

Browse files
committed
Add support for Metafields to webhooks
1 parent ed55fc7 commit eea7761

File tree

7 files changed

+439
-1
lines changed

7 files changed

+439
-1
lines changed

packages/app/src/cli/models/app/app.test-data.ts

+1
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@ export async function testSingleWebhookSubscriptionExtension({
428428
topic,
429429
api_version: '2024-01',
430430
uri: 'https://my-app.com/webhooks',
431+
metafields: [{namespace: 'custom', key: 'test'}],
431432
},
432433
}: {
433434
emptyConfig?: boolean

packages/app/src/cli/models/extensions/specifications/app_config_webhook.test.ts

+215
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import spec from './app_config_webhook.js'
2+
import {webhookValidator} from './validation/app_config_webhook.js'
3+
import {WebhookSubscriptionSchema} from './app_config_webhook_schemas/webhook_subscription_schema.js'
24
import {placeholderAppConfiguration} from '../../app/app.test-data.js'
35
import {describe, expect, test} from 'vitest'
6+
import {zod} from '@shopify/cli-kit/node/schema'
47

58
describe('webhooks', () => {
69
describe('transform', () => {
@@ -61,4 +64,216 @@ describe('webhooks', () => {
6164
})
6265
})
6366
})
67+
68+
describe('validation', () => {
69+
interface TestWebhookConfig {
70+
api_version: string
71+
subscriptions: unknown[]
72+
}
73+
74+
function validateWebhooks(webhookConfig: TestWebhookConfig) {
75+
const ctx = {
76+
addIssue: (issue: zod.ZodIssue) => {
77+
throw new Error(issue.message)
78+
},
79+
path: [],
80+
} as zod.RefinementCtx
81+
82+
// First validate the schema for each subscription
83+
for (const subscription of webhookConfig.subscriptions) {
84+
const schemaResult = WebhookSubscriptionSchema.safeParse(subscription)
85+
if (!schemaResult.success) {
86+
return {
87+
success: false,
88+
error: new Error(schemaResult.error.issues[0]?.message ?? 'Invalid webhook subscription'),
89+
}
90+
}
91+
}
92+
93+
// Then validate business rules
94+
try {
95+
webhookValidator(webhookConfig, ctx)
96+
return {success: true, error: undefined}
97+
} catch (error) {
98+
if (error instanceof Error) {
99+
return {success: false, error}
100+
}
101+
throw error
102+
}
103+
}
104+
105+
test('allows metafields when API version is 2025-04', () => {
106+
// Given
107+
const webhookConfig: TestWebhookConfig = {
108+
api_version: '2025-04',
109+
subscriptions: [
110+
{
111+
topics: ['orders/create'],
112+
uri: 'https://example.com/webhooks',
113+
metafields: [{namespace: 'custom', key: 'test'}],
114+
},
115+
],
116+
}
117+
118+
// When
119+
const result = validateWebhooks(webhookConfig)
120+
121+
// Then
122+
expect(result.success).toBe(true)
123+
})
124+
125+
test('allows metafields when API version is unstable', () => {
126+
// Given
127+
const webhookConfig: TestWebhookConfig = {
128+
api_version: 'unstable',
129+
subscriptions: [
130+
{
131+
topics: ['orders/create'],
132+
uri: 'https://example.com/webhooks',
133+
metafields: [{namespace: 'custom', key: 'test'}],
134+
},
135+
],
136+
}
137+
138+
// When
139+
const result = validateWebhooks(webhookConfig)
140+
141+
// Then
142+
expect(result.success).toBe(true)
143+
})
144+
145+
test('rejects metafields when API version is earlier than 2025-04', () => {
146+
// Given
147+
const webhookConfig: TestWebhookConfig = {
148+
api_version: '2024-01',
149+
subscriptions: [
150+
{
151+
topics: ['orders/create'],
152+
uri: 'https://example.com/webhooks',
153+
metafields: [{namespace: 'custom', key: 'test'}],
154+
},
155+
],
156+
}
157+
158+
// When
159+
const result = validateWebhooks(webhookConfig)
160+
161+
// Then
162+
expect(result.success).toBe(false)
163+
expect(result.error?.message).toBe(
164+
'Webhook metafields are only supported in API version 2025-04 or later, or with version "unstable"',
165+
)
166+
})
167+
168+
test('validates metafields namespace and key are strings', () => {
169+
// Given
170+
const webhookConfig: TestWebhookConfig = {
171+
api_version: '2025-04',
172+
subscriptions: [
173+
{
174+
topics: ['orders/create'],
175+
uri: 'https://example.com/webhooks',
176+
metafields: [{namespace: 123, key: 'test'}],
177+
},
178+
],
179+
}
180+
181+
// When
182+
const result = validateWebhooks(webhookConfig)
183+
184+
// Then
185+
expect(result.success).toBe(false)
186+
expect(result.error?.message).toBe('Metafield namespace must be a string')
187+
})
188+
189+
test('allows configuration without metafields in older API versions', () => {
190+
// Given
191+
const webhookConfig: TestWebhookConfig = {
192+
api_version: '2024-01',
193+
subscriptions: [
194+
{
195+
topics: ['orders/create'],
196+
uri: 'https://example.com/webhooks',
197+
},
198+
],
199+
}
200+
201+
// When
202+
const result = validateWebhooks(webhookConfig)
203+
204+
// Then
205+
expect(result.success).toBe(true)
206+
})
207+
208+
test('allows empty metafields array in supported API versions', () => {
209+
// Given
210+
const webhookConfig: TestWebhookConfig = {
211+
api_version: '2025-04',
212+
subscriptions: [
213+
{
214+
topics: ['orders/create'],
215+
uri: 'https://example.com/webhooks',
216+
metafields: [],
217+
},
218+
],
219+
}
220+
221+
// When
222+
const result = validateWebhooks(webhookConfig)
223+
224+
// Then
225+
expect(result.success).toBe(true)
226+
})
227+
228+
test('rejects metafields with invalid property types', () => {
229+
// Given
230+
const webhookConfig: TestWebhookConfig = {
231+
api_version: '2025-04',
232+
subscriptions: [
233+
{
234+
topics: ['orders/create'],
235+
uri: 'https://example.com/webhooks',
236+
metafields: [
237+
{
238+
namespace: 123,
239+
key: 'valid',
240+
},
241+
],
242+
},
243+
],
244+
}
245+
246+
// When
247+
const result = validateWebhooks(webhookConfig)
248+
249+
// Then
250+
expect(result.success).toBe(false)
251+
expect(result.error?.message).toBe('Metafield namespace must be a string')
252+
})
253+
254+
test('rejects malformed metafields missing a required property', () => {
255+
// Given
256+
const webhookConfig: TestWebhookConfig = {
257+
api_version: '2025-04',
258+
subscriptions: [
259+
{
260+
topics: ['orders/create'],
261+
uri: 'https://example.com/webhooks',
262+
metafields: [
263+
{
264+
namespace: 'custom',
265+
},
266+
],
267+
},
268+
],
269+
}
270+
271+
// When
272+
const result = validateWebhooks(webhookConfig)
273+
274+
// Then
275+
expect(result.success).toBe(false)
276+
expect(result.error?.message).toBe('Required')
277+
})
278+
})
64279
})

packages/app/src/cli/models/extensions/specifications/app_config_webhook_schemas/webhook_subscription_schema.ts

+9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ export const WebhookSubscriptionSchema = zod.object({
1818
}),
1919
include_fields: zod.array(zod.string({invalid_type_error: 'Value must be a string'})).optional(),
2020
filter: zod.string({invalid_type_error: 'Value must be a string'}).optional(),
21+
metafields: zod
22+
.array(
23+
zod.object({
24+
namespace: zod.string({invalid_type_error: 'Metafield namespace must be a string'}),
25+
key: zod.string({invalid_type_error: 'Metafield key must be a string'}),
26+
}),
27+
{invalid_type_error: 'Metafields must be an array of objects with namespace and key'},
28+
)
29+
.optional(),
2130
compliance_topics: zod
2231
.array(
2332
zod.enum([ComplianceTopic.CustomersRedact, ComplianceTopic.CustomersDataRequest, ComplianceTopic.ShopRedact]),

0 commit comments

Comments
 (0)