Skip to content

Commit 3caf63e

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

File tree

7 files changed

+345
-1
lines changed

7 files changed

+345
-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

+144
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,148 @@ describe('webhooks', () => {
6161
})
6262
})
6363
})
64+
65+
describe('validation', () => {
66+
test('allows metafields when API version is 2025-04', () => {
67+
// Given
68+
const object = {
69+
webhooks: {
70+
api_version: '2025-04',
71+
subscriptions: [
72+
{
73+
topics: ['orders/create'],
74+
uri: 'https://example.com/webhooks',
75+
metafields: [{namespace: 'custom', key: 'test'}],
76+
},
77+
],
78+
},
79+
}
80+
81+
// When
82+
const result = spec.schema.safeParse(object)
83+
84+
// Then
85+
expect(result.success).toBe(true)
86+
})
87+
88+
test('allows metafields when API version is unstable', () => {
89+
// Given
90+
const object = {
91+
webhooks: {
92+
api_version: 'unstable',
93+
subscriptions: [
94+
{
95+
topics: ['orders/create'],
96+
uri: 'https://example.com/webhooks',
97+
metafields: [{namespace: 'custom', key: 'test'}],
98+
},
99+
],
100+
},
101+
}
102+
103+
// When
104+
const result = spec.schema.safeParse(object)
105+
106+
// Then
107+
expect(result.success).toBe(true)
108+
})
109+
110+
test('rejects metafields when API version is before 2025-04', () => {
111+
// Given
112+
const object = {
113+
webhooks: {
114+
api_version: '2024-01',
115+
subscriptions: [
116+
{
117+
topics: ['orders/create'],
118+
uri: 'https://example.com/webhooks',
119+
metafields: [{namespace: 'custom', key: 'test'}],
120+
},
121+
],
122+
},
123+
}
124+
125+
// When
126+
const result = spec.schema.safeParse(object)
127+
128+
// Then
129+
expect(result.success).toBe(false)
130+
expect(result.error.issues[0].message).toBe(
131+
'Webhook metafields are only supported in API version 2025-04 or later, or with version "unstable"',
132+
)
133+
})
134+
135+
test('allows configuration without metafields in older API versions', () => {
136+
// Given
137+
const object = {
138+
webhooks: {
139+
api_version: '2024-01',
140+
subscriptions: [
141+
{
142+
topics: ['orders/create'],
143+
uri: 'https://example.com/webhooks',
144+
},
145+
],
146+
},
147+
}
148+
149+
// When
150+
const result = spec.schema.safeParse(object)
151+
152+
// Then
153+
expect(result.success).toBe(true)
154+
})
155+
156+
test('allows empty metafields array in supported API versions', () => {
157+
// Given
158+
const object = {
159+
webhooks: {
160+
api_version: '2025-04',
161+
subscriptions: [
162+
{
163+
topics: ['orders/create'],
164+
uri: 'https://example.com/webhooks',
165+
metafields: [],
166+
},
167+
],
168+
},
169+
}
170+
171+
// When
172+
const result = spec.schema.safeParse(object)
173+
174+
// Then
175+
expect(result.success).toBe(true)
176+
})
177+
178+
test('rejects malformed metafields', () => {
179+
// Given
180+
const object = {
181+
webhooks: {
182+
api_version: '2025-04',
183+
subscriptions: [
184+
{
185+
topics: ['orders/create'],
186+
uri: 'https://example.com/webhooks',
187+
metafields: [
188+
{
189+
// Missing required 'key' property
190+
namespace: 'custom',
191+
// Invalid additional property
192+
invalid_property: 'test',
193+
},
194+
],
195+
},
196+
],
197+
},
198+
}
199+
200+
// When
201+
const result = spec.schema.safeParse(object)
202+
203+
// Then
204+
expect(result.success).toBe(false)
205+
expect(result.error.issues[0].message).toMatch(/Required/)
206+
})
207+
})
64208
})

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]),

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

+155
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,159 @@ describe('webhook_subscription', () => {
5353
})
5454
})
5555
})
56+
57+
describe('metafields validation', () => {
58+
test('transforms metafields correctly in local to remote', () => {
59+
// Given
60+
const object = {
61+
topics: ['products/create'],
62+
uri: '/products',
63+
metafields: [
64+
{
65+
namespace: 'custom',
66+
key: 'test',
67+
},
68+
],
69+
}
70+
71+
const webhookSpec = spec
72+
73+
// When
74+
const result = webhookSpec.transformLocalToRemote!(object, {
75+
application_url: 'https://my-app-url.com/',
76+
} as unknown as AppConfigurationWithoutPath)
77+
78+
// Then
79+
expect(result).toEqual({
80+
uri: 'https://my-app-url.com/products',
81+
topics: ['products/create'],
82+
metafields: [
83+
{
84+
namespace: 'custom',
85+
key: 'test',
86+
},
87+
],
88+
})
89+
})
90+
91+
test('preserves metafields in remote to local transform', () => {
92+
// Given
93+
const object = {
94+
topic: 'products/create',
95+
uri: 'https://my-app-url.com/products',
96+
metafields: [
97+
{
98+
namespace: 'custom',
99+
key: 'test',
100+
},
101+
],
102+
}
103+
104+
const webhookSpec = spec
105+
106+
// When
107+
const result = webhookSpec.transformRemoteToLocal!(object)
108+
109+
// Then
110+
expect(result).toMatchObject({
111+
webhooks: {
112+
subscriptions: [
113+
{
114+
topics: ['products/create'],
115+
uri: 'https://my-app-url.com/products',
116+
metafields: [
117+
{
118+
namespace: 'custom',
119+
key: 'test',
120+
},
121+
],
122+
},
123+
],
124+
},
125+
})
126+
})
127+
128+
test('rejects metafields with missing required properties', () => {
129+
// Given
130+
const object = {
131+
topics: ['products/create'],
132+
uri: '/products',
133+
metafields: [
134+
{
135+
// Missing required 'key' property
136+
namespace: 'custom',
137+
},
138+
],
139+
}
140+
141+
const webhookSpec = spec
142+
143+
// When
144+
const result = webhookSpec.schema.safeParse({
145+
webhooks: {
146+
subscriptions: [object],
147+
},
148+
})
149+
150+
// Then
151+
expect(result.success).toBe(false)
152+
expect(result.error.issues[0].message).toMatch(/Required/)
153+
})
154+
155+
test('rejects metafields with invalid property types', () => {
156+
// Given
157+
const object = {
158+
topics: ['products/create'],
159+
uri: '/products',
160+
metafields: [
161+
{
162+
namespace: 123,
163+
key: ['invalid'],
164+
},
165+
],
166+
}
167+
168+
const webhookSpec = spec
169+
170+
// When
171+
const result = webhookSpec.schema.safeParse({
172+
webhooks: {
173+
subscriptions: [object],
174+
},
175+
})
176+
177+
// Then
178+
expect(result.success).toBe(false)
179+
expect(result.error.issues[0].message).toBe('Required')
180+
})
181+
182+
test('rejects metafields with additional invalid properties', () => {
183+
// Given
184+
const object = {
185+
topics: ['products/create'],
186+
uri: '/products',
187+
metafields: [
188+
{
189+
namespace: 'custom',
190+
key: 'test',
191+
// Additional invalid property
192+
invalid_property: 'test',
193+
},
194+
],
195+
}
196+
197+
const webhookSpec = spec
198+
199+
// When
200+
const result = webhookSpec.schema.safeParse({
201+
webhooks: {
202+
subscriptions: [object],
203+
},
204+
})
205+
206+
// Then
207+
expect(result.success).toBe(false)
208+
expect(result.error.issues[0].message).toBe('Required')
209+
})
210+
})
56211
})

packages/app/src/cli/models/extensions/specifications/app_config_webhook_subscription.ts

+13
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ interface TransformedWebhookSubscription {
1313
compliance_topics?: string[]
1414
include_fields?: string[]
1515
filter?: string
16+
metafields?: {
17+
namespace: string
18+
key: string
19+
}[]
1620
}
1721

1822
export const SingleWebhookSubscriptionSchema = zod.object({
@@ -23,6 +27,15 @@ export const SingleWebhookSubscriptionSchema = zod.object({
2327
}),
2428
include_fields: zod.array(zod.string({invalid_type_error: 'Value must be a string'})).optional(),
2529
filter: zod.string({invalid_type_error: 'Value must be a string'}).optional(),
30+
metafields: zod
31+
.array(
32+
zod.object({
33+
namespace: zod.string({invalid_type_error: 'Metafield namespace must be a string'}),
34+
key: zod.string({invalid_type_error: 'Metafield key must be a string'}),
35+
}),
36+
{invalid_type_error: 'Metafields must be an array of objects with namespace and key'},
37+
)
38+
.optional(),
2639
})
2740

2841
/* this transforms webhooks remotely to be accepted by the TOML

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

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ export interface WebhookSubscription {
44
compliance_topics?: string[]
55
include_fields?: string[]
66
filter?: string
7+
metafields?: {
8+
namespace: string
9+
key: string
10+
}[]
711
}
812

913
interface PrivacyComplianceConfig {

0 commit comments

Comments
 (0)