@teleporthq/teleport-plugin-next-workflows 0.43.62 → 0.43.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/data-update-item-runtime.test.ts +39 -0
- package/__tests__/page-revalidate-node.test.ts +276 -0
- package/__tests__/payment-refund.test.ts +135 -0
- package/dist/cjs/nodes/analytics/commerce-tracking.d.ts +46 -0
- package/dist/cjs/nodes/analytics/commerce-tracking.d.ts.map +1 -0
- package/dist/cjs/nodes/analytics/commerce-tracking.js +94 -0
- package/dist/cjs/nodes/analytics/commerce-tracking.js.map +1 -0
- package/dist/cjs/nodes/cache/page-revalidate.d.ts +3 -0
- package/dist/cjs/nodes/cache/page-revalidate.d.ts.map +1 -0
- package/dist/cjs/nodes/cache/page-revalidate.js +125 -0
- package/dist/cjs/nodes/cache/page-revalidate.js.map +1 -0
- package/dist/cjs/nodes/cart/cart-add-item.d.ts.map +1 -1
- package/dist/cjs/nodes/cart/cart-add-item.js +22 -1
- package/dist/cjs/nodes/cart/cart-add-item.js.map +1 -1
- package/dist/cjs/nodes/cart/cart-remove-item.d.ts.map +1 -1
- package/dist/cjs/nodes/cart/cart-remove-item.js +23 -1
- package/dist/cjs/nodes/cart/cart-remove-item.js.map +1 -1
- package/dist/cjs/nodes/data/data-create-item.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-create-item.js +42 -1
- package/dist/cjs/nodes/data/data-create-item.js.map +1 -1
- package/dist/cjs/nodes/data/data-update-item.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-update-item.js +38 -3
- package/dist/cjs/nodes/data/data-update-item.js.map +1 -1
- package/dist/cjs/nodes/index.d.ts.map +1 -1
- package/dist/cjs/nodes/index.js +3 -1
- package/dist/cjs/nodes/index.js.map +1 -1
- package/dist/cjs/nodes/payment/payment-refund.d.ts +3 -0
- package/dist/cjs/nodes/payment/payment-refund.d.ts.map +1 -0
- package/dist/cjs/nodes/payment/payment-refund.js +446 -0
- package/dist/cjs/nodes/payment/payment-refund.js.map +1 -0
- package/dist/cjs/secret-collector.d.ts.map +1 -1
- package/dist/cjs/secret-collector.js +3 -0
- package/dist/cjs/secret-collector.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/nodes/analytics/commerce-tracking.d.ts +46 -0
- package/dist/esm/nodes/analytics/commerce-tracking.d.ts.map +1 -0
- package/dist/esm/nodes/analytics/commerce-tracking.js +90 -0
- package/dist/esm/nodes/analytics/commerce-tracking.js.map +1 -0
- package/dist/esm/nodes/cache/page-revalidate.d.ts +3 -0
- package/dist/esm/nodes/cache/page-revalidate.d.ts.map +1 -0
- package/dist/esm/nodes/cache/page-revalidate.js +122 -0
- package/dist/esm/nodes/cache/page-revalidate.js.map +1 -0
- package/dist/esm/nodes/cart/cart-add-item.d.ts.map +1 -1
- package/dist/esm/nodes/cart/cart-add-item.js +22 -1
- package/dist/esm/nodes/cart/cart-add-item.js.map +1 -1
- package/dist/esm/nodes/cart/cart-remove-item.d.ts.map +1 -1
- package/dist/esm/nodes/cart/cart-remove-item.js +23 -1
- package/dist/esm/nodes/cart/cart-remove-item.js.map +1 -1
- package/dist/esm/nodes/data/data-create-item.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-create-item.js +42 -1
- package/dist/esm/nodes/data/data-create-item.js.map +1 -1
- package/dist/esm/nodes/data/data-update-item.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-update-item.js +38 -3
- package/dist/esm/nodes/data/data-update-item.js.map +1 -1
- package/dist/esm/nodes/index.d.ts.map +1 -1
- package/dist/esm/nodes/index.js +3 -1
- package/dist/esm/nodes/index.js.map +1 -1
- package/dist/esm/nodes/payment/payment-refund.d.ts +3 -0
- package/dist/esm/nodes/payment/payment-refund.d.ts.map +1 -0
- package/dist/esm/nodes/payment/payment-refund.js +443 -0
- package/dist/esm/nodes/payment/payment-refund.js.map +1 -0
- package/dist/esm/secret-collector.d.ts.map +1 -1
- package/dist/esm/secret-collector.js +3 -0
- package/dist/esm/secret-collector.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/nodes/analytics/commerce-tracking.ts +93 -0
- package/src/nodes/cache/page-revalidate.ts +140 -0
- package/src/nodes/cart/cart-add-item.ts +35 -1
- package/src/nodes/cart/cart-remove-item.ts +36 -1
- package/src/nodes/data/data-create-item.ts +44 -1
- package/src/nodes/data/data-update-item.ts +39 -3
- package/src/nodes/index.ts +4 -0
- package/src/nodes/payment/payment-refund.ts +512 -0
- package/src/secret-collector.ts +3 -0
|
@@ -99,6 +99,45 @@ describe('data-update-item runtime output + null-safety', () => {
|
|
|
99
99
|
expect(result.affected).toBe(0)
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
+
/*
|
|
103
|
+
A value that could not cross the client→server hand-off arrives as the
|
|
104
|
+
pruner's placeholder. Written through, it REPLACES the stored content with
|
|
105
|
+
`{"__truncated":true}` — which is how a rich-text field carrying an inlined
|
|
106
|
+
image emptied a whole custom page's block tree. The column keeps what it has
|
|
107
|
+
instead, exactly like an undefined mapping.
|
|
108
|
+
*/
|
|
109
|
+
it('keeps the stored value when the new one arrived truncated', async () => {
|
|
110
|
+
const calls = installFetch({ id: 'r1', updatedCount: 1 })
|
|
111
|
+
await handler(
|
|
112
|
+
{
|
|
113
|
+
tableName: 'teleport_pages',
|
|
114
|
+
filters: [{ field: 'id', operator: '=', value: 'p1' }],
|
|
115
|
+
columnMappings: [
|
|
116
|
+
{ column: 'blocks', value: { __truncated: true, type: 'string' } },
|
|
117
|
+
{ column: 'title', value: 'Launch' },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{}
|
|
121
|
+
)
|
|
122
|
+
expect(calls).toHaveLength(1)
|
|
123
|
+
const sentMappings = calls[0].body.columnMappings as Array<{ column: string }>
|
|
124
|
+
expect(sentMappings.map((m) => m.column)).toEqual(['title'])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('keeps the stored value when the new one could not be serialized', async () => {
|
|
128
|
+
const calls = installFetch({ id: 'r1', updatedCount: 1 })
|
|
129
|
+
const result = (await handler(
|
|
130
|
+
{
|
|
131
|
+
tableName: 'teleport_pages',
|
|
132
|
+
filters: [{ field: 'id', operator: '=', value: 'p1' }],
|
|
133
|
+
columnMappings: { blocks: { __serializationError: true } },
|
|
134
|
+
},
|
|
135
|
+
{}
|
|
136
|
+
)) as Record<string, unknown>
|
|
137
|
+
expect(calls).toHaveLength(0)
|
|
138
|
+
expect(result.updatedCount).toBe(0)
|
|
139
|
+
})
|
|
140
|
+
|
|
102
141
|
it('preserves an explicit null columnMapping (intentional clear ≠ undefined)', async () => {
|
|
103
142
|
const calls = installFetch({ id: 'r1', updatedCount: 1 })
|
|
104
143
|
await handler(
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { nodeRegistry } from '../src'
|
|
2
|
+
|
|
3
|
+
interface RevalidateResult {
|
|
4
|
+
revalidated?: string[]
|
|
5
|
+
warning?: string
|
|
6
|
+
success?: boolean
|
|
7
|
+
error?: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FetchCall {
|
|
11
|
+
url: string
|
|
12
|
+
init: {
|
|
13
|
+
method: string
|
|
14
|
+
headers: Record<string, string>
|
|
15
|
+
body: string
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const source = () => nodeRegistry['page-revalidate'].generateHandler()
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Runs the emitted handler the way the generated app does: as runtime source in
|
|
23
|
+
* a scope that provides only `globalThis`, `window` and `fetch`. `process` is
|
|
24
|
+
* reached through the injected `globalThis`, which is exactly how the handler
|
|
25
|
+
* has to read it to survive the browser webpack.
|
|
26
|
+
*/
|
|
27
|
+
const runHandler = async (
|
|
28
|
+
config: Record<string, unknown>,
|
|
29
|
+
options: {
|
|
30
|
+
secret?: string
|
|
31
|
+
context?: Record<string, unknown>
|
|
32
|
+
// tslint:disable-next-line:no-any
|
|
33
|
+
respond?: (call: FetchCall) => any
|
|
34
|
+
calls?: FetchCall[]
|
|
35
|
+
} = {}
|
|
36
|
+
): Promise<RevalidateResult> => {
|
|
37
|
+
const env = options.secret === undefined ? {} : { TQ_CACHE_SECRET: options.secret }
|
|
38
|
+
const calls = options.calls || []
|
|
39
|
+
|
|
40
|
+
// tslint:disable-next-line:no-any
|
|
41
|
+
const fetchImpl = async (url: string, init: any) => {
|
|
42
|
+
const call: FetchCall = { url, init }
|
|
43
|
+
calls.push(call)
|
|
44
|
+
if (!options.respond) {
|
|
45
|
+
throw new Error('offline')
|
|
46
|
+
}
|
|
47
|
+
const payload = options.respond(call)
|
|
48
|
+
return {
|
|
49
|
+
ok: payload.ok !== false,
|
|
50
|
+
json: async () => payload.body,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// tslint:disable-next-line:function-constructor
|
|
55
|
+
const factory = new Function(
|
|
56
|
+
'globalThis',
|
|
57
|
+
'window',
|
|
58
|
+
'fetch',
|
|
59
|
+
`${source()}; return page_revalidate;`
|
|
60
|
+
)
|
|
61
|
+
const fn = factory({ process: { env } }, undefined, fetchImpl)
|
|
62
|
+
|
|
63
|
+
return fn(config, options.context || { __baseUrl: 'https://site.test' })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const expectNotFatal = (result: RevalidateResult) => {
|
|
67
|
+
// Mirrors `isFatalNodeResult` in the emitted runtime: `success: false`, a
|
|
68
|
+
// string `error`, or `error: true` HALTS the workflow.
|
|
69
|
+
expect(result.success).toBeUndefined()
|
|
70
|
+
expect(typeof result.error).not.toBe('string')
|
|
71
|
+
expect(result.error).not.toBe(true)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('page-revalidate node', () => {
|
|
75
|
+
it('is registered and runs on the server', () => {
|
|
76
|
+
expect(nodeRegistry['page-revalidate']).toBeDefined()
|
|
77
|
+
expect(nodeRegistry['page-revalidate'].nodeType).toBe('page-revalidate')
|
|
78
|
+
// `res.revalidate` only exists inside a server route, and server-side puts
|
|
79
|
+
// the rebuild in the same segment as the write it follows.
|
|
80
|
+
expect(nodeRegistry['page-revalidate'].executionEnv).toBe('server')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handlers ship as runtime source and are re-bundled by webpack, which
|
|
85
|
+
* rewrites `require(`.
|
|
86
|
+
*/
|
|
87
|
+
it('never requires a module', () => {
|
|
88
|
+
expect(source()).not.toMatch(/(?<![\w.])require\s*\(/)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('path resolution', () => {
|
|
92
|
+
it('posts the configured paths to the revalidate route', async () => {
|
|
93
|
+
const calls: FetchCall[] = []
|
|
94
|
+
const result = await runHandler(
|
|
95
|
+
{ paths: ['/pages/welcome', '/pages/about'] },
|
|
96
|
+
{
|
|
97
|
+
secret: 'sekret',
|
|
98
|
+
calls,
|
|
99
|
+
respond: () => ({ body: { ok: true, revalidated: ['/pages/welcome'], failed: [] } }),
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
expect(calls).toHaveLength(1)
|
|
104
|
+
expect(calls[0].url).toBe('https://site.test/api/tq-revalidate')
|
|
105
|
+
expect(calls[0].init.method).toBe('POST')
|
|
106
|
+
expect(calls[0].init.headers['x-tq-cache-secret']).toBe('sekret')
|
|
107
|
+
expect(JSON.parse(calls[0].init.body)).toEqual({
|
|
108
|
+
paths: ['/pages/welcome', '/pages/about'],
|
|
109
|
+
})
|
|
110
|
+
expect(result.revalidated).toEqual(['/pages/welcome'])
|
|
111
|
+
expect(result.warning).toBeUndefined()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A path built from a database column routinely arrives as the bare slug,
|
|
116
|
+
* and `res.revalidate` only accepts a site-root-relative path.
|
|
117
|
+
*/
|
|
118
|
+
it('gives a bare slug a leading slash', async () => {
|
|
119
|
+
const calls: FetchCall[] = []
|
|
120
|
+
await runHandler(
|
|
121
|
+
{ paths: ['pages/welcome'] },
|
|
122
|
+
{ secret: 'sekret', calls, respond: () => ({ body: { ok: true } }) }
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
expect(JSON.parse(calls[0].init.body).paths).toEqual(['/pages/welcome'])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* An update revalidates the slug the row HAD and the slug it now has — and
|
|
130
|
+
* when the slug did not change, those are the same string. Rebuilding it
|
|
131
|
+
* twice would double the work for no effect.
|
|
132
|
+
*/
|
|
133
|
+
it('drops blanks, unresolved refs and duplicates', async () => {
|
|
134
|
+
const calls: FetchCall[] = []
|
|
135
|
+
await runHandler(
|
|
136
|
+
{
|
|
137
|
+
paths: [
|
|
138
|
+
'/pages/welcome',
|
|
139
|
+
' ',
|
|
140
|
+
'/pages/welcome',
|
|
141
|
+
null,
|
|
142
|
+
{ type: 'workflowContext', nodeId: 'n1', path: ['value'] },
|
|
143
|
+
'pages/welcome',
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
{ secret: 'sekret', calls, respond: () => ({ body: { ok: true } }) }
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
expect(JSON.parse(calls[0].init.body).paths).toEqual(['/pages/welcome'])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
/*
|
|
153
|
+
* A step that rebuilds several rows at once resolves ONE reference to an
|
|
154
|
+
* array of slugs — a bulk delete cannot know at build time how many rows the
|
|
155
|
+
* admin will select, so it cannot configure a path per row.
|
|
156
|
+
*/
|
|
157
|
+
it('flattens an array of paths resolved from a single reference', async () => {
|
|
158
|
+
const calls: FetchCall[] = []
|
|
159
|
+
await runHandler(
|
|
160
|
+
{ paths: [['/pages/welcome', '/pages/pricing'], '/pages/about'] },
|
|
161
|
+
{ secret: 'sekret', calls, respond: () => ({ body: { ok: true } }) }
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
expect(JSON.parse(calls[0].init.body).paths).toEqual([
|
|
165
|
+
'/pages/welcome',
|
|
166
|
+
'/pages/pricing',
|
|
167
|
+
'/pages/about',
|
|
168
|
+
])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('accepts a single path configured as a bare string', async () => {
|
|
172
|
+
const calls: FetchCall[] = []
|
|
173
|
+
await runHandler(
|
|
174
|
+
{ paths: '/pages/welcome' },
|
|
175
|
+
{ secret: 'sekret', calls, respond: () => ({ body: { ok: true } }) }
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
expect(JSON.parse(calls[0].init.body).paths).toEqual(['/pages/welcome'])
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('forwards the internal request headers the deployment protection needs', async () => {
|
|
182
|
+
const calls: FetchCall[] = []
|
|
183
|
+
await runHandler(
|
|
184
|
+
{ paths: ['/pages/welcome'] },
|
|
185
|
+
{
|
|
186
|
+
secret: 'sekret',
|
|
187
|
+
calls,
|
|
188
|
+
context: {
|
|
189
|
+
__baseUrl: 'https://site.test',
|
|
190
|
+
__internalHeaders: { cookie: '_vercel_jwt=abc' },
|
|
191
|
+
},
|
|
192
|
+
respond: () => ({ body: { ok: true } }),
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
expect(calls[0].init.headers.cookie).toBe('_vercel_jwt=abc')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* THE regression this node is shaped around. The runtime treats
|
|
202
|
+
* `success: false` / a string `error` as FATAL, so a page that could not be
|
|
203
|
+
* rebuilt would halt the workflow AFTER the row was already written — no
|
|
204
|
+
* success toast, no navigation, and an error for an action that worked. Every
|
|
205
|
+
* failure mode below has to report a warning instead.
|
|
206
|
+
*/
|
|
207
|
+
describe('never reports failure in a shape the runtime treats as fatal', () => {
|
|
208
|
+
it('with no paths configured', async () => {
|
|
209
|
+
const calls: FetchCall[] = []
|
|
210
|
+
const result = await runHandler({}, { secret: 'sekret', calls })
|
|
211
|
+
|
|
212
|
+
expect(result.revalidated).toEqual([])
|
|
213
|
+
expect(result.warning).toContain('No page paths')
|
|
214
|
+
expect(calls).toHaveLength(0)
|
|
215
|
+
expectNotFatal(result)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('with every configured path blank', async () => {
|
|
219
|
+
const result = await runHandler({ paths: ['', ' '] }, { secret: 'sekret' })
|
|
220
|
+
|
|
221
|
+
expect(result.revalidated).toEqual([])
|
|
222
|
+
expectNotFatal(result)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('with no secret configured', async () => {
|
|
226
|
+
const calls: FetchCall[] = []
|
|
227
|
+
const result = await runHandler({ paths: ['/pages/welcome'] }, { calls })
|
|
228
|
+
|
|
229
|
+
expect(result.revalidated).toEqual([])
|
|
230
|
+
expect(result.warning).toContain('TQ_CACHE_SECRET')
|
|
231
|
+
// Nothing is sent without the credential the route requires.
|
|
232
|
+
expect(calls).toHaveLength(0)
|
|
233
|
+
expectNotFatal(result)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('when the route answers with an error status', async () => {
|
|
237
|
+
const result = await runHandler(
|
|
238
|
+
{ paths: ['/pages/welcome'] },
|
|
239
|
+
{ secret: 'sekret', respond: () => ({ ok: false, body: { error: 'Unauthorized' } }) }
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
expect(result.revalidated).toEqual([])
|
|
243
|
+
expect(result.warning).toBe('Unauthorized')
|
|
244
|
+
expectNotFatal(result)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('when the route is unreachable', async () => {
|
|
248
|
+
const result = await runHandler({ paths: ['/pages/welcome'] }, { secret: 'sekret' })
|
|
249
|
+
|
|
250
|
+
expect(result.revalidated).toEqual([])
|
|
251
|
+
expect(result.warning).toBe('offline')
|
|
252
|
+
expectNotFatal(result)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* `res.revalidate` throws for a path that was never statically generated —
|
|
257
|
+
* the normal case for a page whose row was just deleted. The paths that DID
|
|
258
|
+
* rebuild are still reported.
|
|
259
|
+
*/
|
|
260
|
+
it('when some paths could not be rebuilt', async () => {
|
|
261
|
+
const result = await runHandler(
|
|
262
|
+
{ paths: ['/pages/welcome', '/pages/gone'] },
|
|
263
|
+
{
|
|
264
|
+
secret: 'sekret',
|
|
265
|
+
respond: () => ({
|
|
266
|
+
body: { ok: true, revalidated: ['/pages/welcome'], failed: ['/pages/gone'] },
|
|
267
|
+
}),
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
expect(result.revalidated).toEqual(['/pages/welcome'])
|
|
272
|
+
expect(result.warning).toContain('/pages/gone')
|
|
273
|
+
expectNotFatal(result)
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { loadHandler, HandlerFn } from './_helpers/load-handler'
|
|
2
|
+
import { paymentRefund } from '../src/nodes/payment/payment-refund'
|
|
3
|
+
import { resolveHandlerEntryName } from '../src/nodes/types'
|
|
4
|
+
import { collectSecrets } from '../src/secret-collector'
|
|
5
|
+
|
|
6
|
+
interface RefundResult {
|
|
7
|
+
success: boolean
|
|
8
|
+
refundId: string
|
|
9
|
+
amount: number
|
|
10
|
+
currency: string
|
|
11
|
+
status: string
|
|
12
|
+
error: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('payment-refund', () => {
|
|
16
|
+
const handler: HandlerFn = loadHandler('payment-refund')
|
|
17
|
+
const source = paymentRefund.generateHandler()
|
|
18
|
+
|
|
19
|
+
it('is a SERVER node that continues the workflow', () => {
|
|
20
|
+
expect(paymentRefund.executionEnv).toBe('server')
|
|
21
|
+
// Unlike a charge, a refund must NOT be terminal: the steps that record it,
|
|
22
|
+
// restock it and email the buyer all run after it.
|
|
23
|
+
expect(paymentRefund.isTerminal).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('has its provider credentials collected into the generated env', () => {
|
|
27
|
+
// Without this the exported project ships with no key for the refund node
|
|
28
|
+
// and every refund fails at runtime with "not configured".
|
|
29
|
+
const secrets = collectSecrets({
|
|
30
|
+
workflows: {
|
|
31
|
+
wf1: {
|
|
32
|
+
nodes: [
|
|
33
|
+
{
|
|
34
|
+
id: 'node-1',
|
|
35
|
+
type: 'payment-refund',
|
|
36
|
+
config: { secretKey: 'sk_test_1', clientId: 'id_1', clientSecret: 'cs_1' },
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
} as never)
|
|
42
|
+
expect(secrets.map((secret) => secret.fieldName).sort()).toEqual([
|
|
43
|
+
'clientId',
|
|
44
|
+
'clientSecret',
|
|
45
|
+
'secretKey',
|
|
46
|
+
])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('resolves its own entry point out of the concatenated source', () => {
|
|
50
|
+
// The entry has to be the FIRST statement-level two-parameter function, or
|
|
51
|
+
// the generated route calls a helper instead.
|
|
52
|
+
expect(resolveHandlerEntryName(source, 'payment-refund')).toBe('payment_refund')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('reads process through globalThis, which webpack leaves alone', () => {
|
|
56
|
+
// A bare `process` is rewritten by the GUI's browser packer into a name
|
|
57
|
+
// that does not exist on the Vercel Node runtime.
|
|
58
|
+
expect(source).toContain('globalThis.process.env')
|
|
59
|
+
// …and nowhere is `process.env` read WITHOUT that prefix.
|
|
60
|
+
expect(source.replace(/globalThis\.process\.env/g, '')).not.toContain('process.env')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('keeps each currency list in the SAME snapshot as the code that reads it', () => {
|
|
64
|
+
// A declaration in one `.toString()`'d function and its reference in
|
|
65
|
+
// another is the "Oo is not defined" bug — a minifier renames the two
|
|
66
|
+
// independently and the generated file references a name it never declares.
|
|
67
|
+
// Slicing the emitted source per function is the only way to check that.
|
|
68
|
+
for (const fnName of ['toProviderMinorUnits', 'fromProviderMinorUnits']) {
|
|
69
|
+
const start = source.indexOf(`function ${fnName}`)
|
|
70
|
+
expect(start).toBeGreaterThanOrEqual(0)
|
|
71
|
+
const next = source.indexOf('\nfunction ', start + 1)
|
|
72
|
+
const body = next === -1 ? source.slice(start) : source.slice(start, next)
|
|
73
|
+
expect(body).toContain('zeroDecimalCurrencies = [')
|
|
74
|
+
expect(body).toContain('threeDecimalCurrencies = [')
|
|
75
|
+
expect(body).toContain('zeroDecimalCurrencies.indexOf')
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('refuses without a payment reference instead of calling a provider', async () => {
|
|
80
|
+
const result = (await handler({ currency: 'usd' }, {})) as RefundResult
|
|
81
|
+
expect(result.success).toBe(false)
|
|
82
|
+
expect(result.error).toMatch(/no payment reference/i)
|
|
83
|
+
expect(result.refundId).toBe('')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('reports missing Stripe credentials as an actionable error', async () => {
|
|
87
|
+
const saved = process.env.STRIPE_SECRET_KEY
|
|
88
|
+
delete process.env.STRIPE_SECRET_KEY
|
|
89
|
+
try {
|
|
90
|
+
const result = (await handler(
|
|
91
|
+
{ providerType: 'stripe', paymentReference: 'pi_1', currency: 'usd' },
|
|
92
|
+
{}
|
|
93
|
+
)) as RefundResult
|
|
94
|
+
expect(result.success).toBe(false)
|
|
95
|
+
expect(result.error).toContain('STRIPE_SECRET_KEY')
|
|
96
|
+
// The shape stays constant on every path, so a downstream node can bind
|
|
97
|
+
// to it without knowing which branch ran.
|
|
98
|
+
expect(result).toMatchObject({ refundId: '', amount: 0, status: '' })
|
|
99
|
+
} finally {
|
|
100
|
+
if (saved !== undefined) {
|
|
101
|
+
process.env.STRIPE_SECRET_KEY = saved
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('reports missing PayPal credentials without attempting a network call', async () => {
|
|
107
|
+
const savedId = process.env.PAYPAL_CLIENT_ID
|
|
108
|
+
const savedSecret = process.env.PAYPAL_CLIENT_SECRET
|
|
109
|
+
delete process.env.PAYPAL_CLIENT_ID
|
|
110
|
+
delete process.env.PAYPAL_CLIENT_SECRET
|
|
111
|
+
try {
|
|
112
|
+
const result = (await handler(
|
|
113
|
+
{ providerType: 'paypal', paymentReference: 'cap_1', currency: 'usd' },
|
|
114
|
+
{}
|
|
115
|
+
)) as RefundResult
|
|
116
|
+
expect(result.success).toBe(false)
|
|
117
|
+
expect(result.error).toMatch(/PayPal credentials are not configured/i)
|
|
118
|
+
} finally {
|
|
119
|
+
if (savedId !== undefined) {
|
|
120
|
+
process.env.PAYPAL_CLIENT_ID = savedId
|
|
121
|
+
}
|
|
122
|
+
if (savedSecret !== undefined) {
|
|
123
|
+
process.env.PAYPAL_CLIENT_SECRET = savedSecret
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('always returns an answer rather than throwing, so bookkeeping can still run', async () => {
|
|
129
|
+
// A declined refund is a branch the workflow takes, not a crash that takes
|
|
130
|
+
// the whole segment — including the steps that record what happened — down.
|
|
131
|
+
await expect(
|
|
132
|
+
handler({ providerType: 'stripe', paymentReference: 'pi_1', currency: 'usd' }, {})
|
|
133
|
+
).resolves.toBeDefined()
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one way a generated workflow handler reports a commerce funnel step.
|
|
3
|
+
*
|
|
4
|
+
* ## Why a global and not an import
|
|
5
|
+
*
|
|
6
|
+
* A node handler ships as a SERIALIZED function body (`fn.toString()`) that is
|
|
7
|
+
* evaluated at runtime with no module scope — it cannot import anything. The
|
|
8
|
+
* analytics tracker publishes `window.tpTrackCommerce` when it boots, and that
|
|
9
|
+
* global is the only channel a handler can reach.
|
|
10
|
+
*
|
|
11
|
+
* ## Why every call is guarded twice
|
|
12
|
+
*
|
|
13
|
+
* The global is absent on any project that did not enable analytics, and absent
|
|
14
|
+
* on every server-side render. These handlers run inside add-to-cart and
|
|
15
|
+
* checkout, where an exception costs a sale — so a missing tracker has to be a
|
|
16
|
+
* silent no-op, never a `ReferenceError`. The `try` covers the second failure
|
|
17
|
+
* mode: a tracker that is present but throws (storage disabled, a browser
|
|
18
|
+
* extension replacing the global) must not take the cart down with it.
|
|
19
|
+
*
|
|
20
|
+
* ## ⛔ Why a calling node must NOT import `trackCommerceStep`
|
|
21
|
+
*
|
|
22
|
+
* TypeScript's CommonJS emit rewrites a cross-module call into
|
|
23
|
+
* `(0, commerce_tracking_1.trackCommerceStep)(...)`. That survives `.toString()`
|
|
24
|
+
* verbatim, and `commerce_tracking_1` does not exist in the generated workflow
|
|
25
|
+
* segment — so the handler throws `ReferenceError` the first time a shopper adds
|
|
26
|
+
* to the cart, in production only, because nothing about it is visible in the
|
|
27
|
+
* TypeScript source.
|
|
28
|
+
*
|
|
29
|
+
* A caller therefore declares the name AMBIENTLY (`declare function
|
|
30
|
+
* trackCommerceStep`), which emits nothing and leaves a bare identifier in the
|
|
31
|
+
* serialized body, and appends `COMMERCE_TRACKING_HELPER_SOURCE` so the runtime
|
|
32
|
+
* definition travels with it. `assertHandlerHasNoModuleRefs` fails the build if
|
|
33
|
+
* a module reference ever creeps back in.
|
|
34
|
+
*/
|
|
35
|
+
/** ES5 source of the helper, for concatenating after a serialized handler. */
|
|
36
|
+
export declare const COMMERCE_TRACKING_HELPER_SOURCE: string;
|
|
37
|
+
/**
|
|
38
|
+
* Fails the BUILD when a serialized handler still references a module namespace.
|
|
39
|
+
*
|
|
40
|
+
* Cheap insurance against a failure that is otherwise invisible: the TypeScript
|
|
41
|
+
* compiles, the tests that only read the source pass, and the handler throws in
|
|
42
|
+
* a shopper's browser. Called by every `generateHandler` that concatenates a
|
|
43
|
+
* helper.
|
|
44
|
+
*/
|
|
45
|
+
export declare function assertHandlerHasNoModuleRefs(source: string, nodeType: string): string;
|
|
46
|
+
//# sourceMappingURL=commerce-tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commerce-tracking.d.ts","sourceRoot":"","sources":["../../../../src/nodes/analytics/commerce-tracking.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AA+BH,8EAA8E;AAC9E,eAAO,MAAM,+BAA+B,QAA+B,CAAA;AAQ3E;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWrF"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The one way a generated workflow handler reports a commerce funnel step.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a global and not an import
|
|
6
|
+
*
|
|
7
|
+
* A node handler ships as a SERIALIZED function body (`fn.toString()`) that is
|
|
8
|
+
* evaluated at runtime with no module scope — it cannot import anything. The
|
|
9
|
+
* analytics tracker publishes `window.tpTrackCommerce` when it boots, and that
|
|
10
|
+
* global is the only channel a handler can reach.
|
|
11
|
+
*
|
|
12
|
+
* ## Why every call is guarded twice
|
|
13
|
+
*
|
|
14
|
+
* The global is absent on any project that did not enable analytics, and absent
|
|
15
|
+
* on every server-side render. These handlers run inside add-to-cart and
|
|
16
|
+
* checkout, where an exception costs a sale — so a missing tracker has to be a
|
|
17
|
+
* silent no-op, never a `ReferenceError`. The `try` covers the second failure
|
|
18
|
+
* mode: a tracker that is present but throws (storage disabled, a browser
|
|
19
|
+
* extension replacing the global) must not take the cart down with it.
|
|
20
|
+
*
|
|
21
|
+
* ## ⛔ Why a calling node must NOT import `trackCommerceStep`
|
|
22
|
+
*
|
|
23
|
+
* TypeScript's CommonJS emit rewrites a cross-module call into
|
|
24
|
+
* `(0, commerce_tracking_1.trackCommerceStep)(...)`. That survives `.toString()`
|
|
25
|
+
* verbatim, and `commerce_tracking_1` does not exist in the generated workflow
|
|
26
|
+
* segment — so the handler throws `ReferenceError` the first time a shopper adds
|
|
27
|
+
* to the cart, in production only, because nothing about it is visible in the
|
|
28
|
+
* TypeScript source.
|
|
29
|
+
*
|
|
30
|
+
* A caller therefore declares the name AMBIENTLY (`declare function
|
|
31
|
+
* trackCommerceStep`), which emits nothing and leaves a bare identifier in the
|
|
32
|
+
* serialized body, and appends `COMMERCE_TRACKING_HELPER_SOURCE` so the runtime
|
|
33
|
+
* definition travels with it. `assertHandlerHasNoModuleRefs` fails the build if
|
|
34
|
+
* a module reference ever creeps back in.
|
|
35
|
+
*/
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.assertHandlerHasNoModuleRefs = exports.COMMERCE_TRACKING_HELPER_SOURCE = void 0;
|
|
38
|
+
/**
|
|
39
|
+
* ⛔ EXACTLY ONE PARAMETER. Not a style choice.
|
|
40
|
+
*
|
|
41
|
+
* When this package is minified by a consumer, the handler's own entry function
|
|
42
|
+
* loses its conventional name, and `resolveHandlerEntryName` then identifies the
|
|
43
|
+
* entry by ARITY: it prefers a 2-param declaration, because every real handler
|
|
44
|
+
* entry is `(config, context)`. A 2-param helper concatenated after a 1-param
|
|
45
|
+
* entry therefore WINS, and the generated workflow calls the tracking helper
|
|
46
|
+
* where it meant to call add-to-cart.
|
|
47
|
+
*
|
|
48
|
+
* One object parameter keeps the helper out of that contest, and the resolver's
|
|
49
|
+
* positional fallback then correctly picks the entry, which is emitted first.
|
|
50
|
+
* `resolve-handler-entry-name.test.ts` checks every registered node type against
|
|
51
|
+
* exactly this, and is what caught it.
|
|
52
|
+
*
|
|
53
|
+
* Not exported either: exporting invites the cross-module call this file exists
|
|
54
|
+
* to prevent. It is here to be read by `.toString()` below, nothing else.
|
|
55
|
+
*/
|
|
56
|
+
function trackCommerceStep(step) {
|
|
57
|
+
try {
|
|
58
|
+
const tracker = globalThis.tpTrackCommerce;
|
|
59
|
+
if (typeof tracker === 'function') {
|
|
60
|
+
;
|
|
61
|
+
tracker(step.name, step.detail);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
/* analytics must never break a cart or a checkout */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** ES5 source of the helper, for concatenating after a serialized handler. */
|
|
69
|
+
exports.COMMERCE_TRACKING_HELPER_SOURCE = trackCommerceStep.toString();
|
|
70
|
+
/**
|
|
71
|
+
* Matches the two shapes TypeScript's CJS emit produces for a cross-module
|
|
72
|
+
* call — `(0, ns_1.fn)(…)` and a bare `ns_1.fn(…)`.
|
|
73
|
+
*/
|
|
74
|
+
const MODULE_REFERENCE = /\b[A-Za-z_$][\w$]*_\d+\s*\./;
|
|
75
|
+
/**
|
|
76
|
+
* Fails the BUILD when a serialized handler still references a module namespace.
|
|
77
|
+
*
|
|
78
|
+
* Cheap insurance against a failure that is otherwise invisible: the TypeScript
|
|
79
|
+
* compiles, the tests that only read the source pass, and the handler throws in
|
|
80
|
+
* a shopper's browser. Called by every `generateHandler` that concatenates a
|
|
81
|
+
* helper.
|
|
82
|
+
*/
|
|
83
|
+
function assertHandlerHasNoModuleRefs(source, nodeType) {
|
|
84
|
+
const match = source.match(MODULE_REFERENCE);
|
|
85
|
+
if (match) {
|
|
86
|
+
throw new Error(`The "${nodeType}" handler references the module namespace "${match[0]}". ` +
|
|
87
|
+
'A serialized handler has no module scope, so this throws ReferenceError at ' +
|
|
88
|
+
'runtime. Declare the helper ambiently (declare function ...) and append its ' +
|
|
89
|
+
'source instead of importing it.');
|
|
90
|
+
}
|
|
91
|
+
return source;
|
|
92
|
+
}
|
|
93
|
+
exports.assertHandlerHasNoModuleRefs = assertHandlerHasNoModuleRefs;
|
|
94
|
+
//# sourceMappingURL=commerce-tracking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commerce-tracking.js","sourceRoot":"","sources":["../../../../src/nodes/analytics/commerce-tracking.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;;;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAS,iBAAiB,CAAC,IAAwD;IACjF,IAAI;QACF,MAAM,OAAO,GAAI,UAAsC,CAAC,eAAe,CAAA;QACvE,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE;YACjC,CAAC;YAAC,OAA4D,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;SACvF;KACF;IAAC,OAAO,CAAC,EAAE;QACV,qDAAqD;KACtD;AACH,CAAC;AAED,8EAA8E;AACjE,QAAA,+BAA+B,GAAG,iBAAiB,CAAC,QAAQ,EAAE,CAAA;AAE3E;;;GAGG;AACH,MAAM,gBAAgB,GAAG,6BAA6B,CAAA;AAEtD;;;;;;;GAOG;AACH,SAAgB,4BAA4B,CAAC,MAAc,EAAE,QAAgB;IAC3E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAA;IAC5C,IAAI,KAAK,EAAE;QACT,MAAM,IAAI,KAAK,CACb,QAAQ,QAAQ,8CAA8C,KAAK,CAAC,CAAC,CAAC,KAAK;YACzE,6EAA6E;YAC7E,8EAA8E;YAC9E,iCAAiC,CACpC,CAAA;KACF;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAXD,oEAWC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-revalidate.d.ts","sourceRoot":"","sources":["../../../../src/nodes/cache/page-revalidate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAmB,MAAM,UAAU,CAAA;AAqIhE,eAAO,MAAM,cAAc,EAAE,oBAM5B,CAAA"}
|