@teleporthq/teleport-plugin-next-workflows 0.44.8 → 0.44.9

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.
Files changed (117) hide show
  1. package/__tests__/_helpers/load-server-runtime.ts +6 -2
  2. package/__tests__/_helpers/run-account-delete-route.ts +140 -0
  3. package/__tests__/account-delete-current-password.test.ts +73 -0
  4. package/__tests__/account-delete-password-check.test.ts +113 -0
  5. package/__tests__/account-delete-visitor-details.test.ts +206 -0
  6. package/__tests__/auth-password-change-ends-sessions.test.ts +178 -0
  7. package/__tests__/data-api-invalid-body.test.ts +110 -0
  8. package/__tests__/data-node-not-awaited.test.ts +7 -6
  9. package/__tests__/invoice-payment-line.test.ts +68 -0
  10. package/__tests__/invoice-tender.test.ts +116 -0
  11. package/__tests__/pending-work-after-reply.test.ts +294 -0
  12. package/__tests__/runtime-condition-operands.test.ts +86 -0
  13. package/__tests__/webhook-delivery-lease.test.ts +68 -6
  14. package/dist/cjs/account-delete-route-generator.d.ts +7 -2
  15. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
  16. package/dist/cjs/account-delete-route-generator.js +90 -43
  17. package/dist/cjs/account-delete-route-generator.js.map +1 -1
  18. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  19. package/dist/cjs/api-route-generator.js +12 -13
  20. package/dist/cjs/api-route-generator.js.map +1 -1
  21. package/dist/cjs/auth-generator.d.ts.map +1 -1
  22. package/dist/cjs/auth-generator.js +34 -1
  23. package/dist/cjs/auth-generator.js.map +1 -1
  24. package/dist/cjs/await-result.d.ts +13 -5
  25. package/dist/cjs/await-result.d.ts.map +1 -1
  26. package/dist/cjs/await-result.js +19 -8
  27. package/dist/cjs/await-result.js.map +1 -1
  28. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  29. package/dist/cjs/data-api-route-generator.js +9 -1
  30. package/dist/cjs/data-api-route-generator.js.map +1 -1
  31. package/dist/cjs/executor-generator.d.ts.map +1 -1
  32. package/dist/cjs/executor-generator.js +18 -6
  33. package/dist/cjs/executor-generator.js.map +1 -1
  34. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  35. package/dist/cjs/invoice/invoice-html-code.js +96 -5
  36. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  37. package/dist/cjs/nodes/account/account-delete-current.d.ts.map +1 -1
  38. package/dist/cjs/nodes/account/account-delete-current.js +4 -1
  39. package/dist/cjs/nodes/account/account-delete-current.js.map +1 -1
  40. package/dist/cjs/server-runtime-code.d.ts.map +1 -1
  41. package/dist/cjs/server-runtime-code.js +21 -0
  42. package/dist/cjs/server-runtime-code.js.map +1 -1
  43. package/dist/cjs/transactional-email-code.d.ts +7 -0
  44. package/dist/cjs/transactional-email-code.d.ts.map +1 -1
  45. package/dist/cjs/transactional-email-code.js +5 -5
  46. package/dist/cjs/transactional-email-code.js.map +1 -1
  47. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  48. package/dist/cjs/vercel-functions.d.ts +34 -0
  49. package/dist/cjs/vercel-functions.d.ts.map +1 -0
  50. package/dist/cjs/vercel-functions.js +62 -0
  51. package/dist/cjs/vercel-functions.js.map +1 -0
  52. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.d.ts +6 -0
  53. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.d.ts.map +1 -1
  54. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.js +26 -11
  55. package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.js.map +1 -1
  56. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  57. package/dist/cjs/workflow-project-plugin.js +3 -0
  58. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  59. package/dist/esm/account-delete-route-generator.d.ts +7 -2
  60. package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
  61. package/dist/esm/account-delete-route-generator.js +90 -43
  62. package/dist/esm/account-delete-route-generator.js.map +1 -1
  63. package/dist/esm/api-route-generator.d.ts.map +1 -1
  64. package/dist/esm/api-route-generator.js +12 -13
  65. package/dist/esm/api-route-generator.js.map +1 -1
  66. package/dist/esm/auth-generator.d.ts.map +1 -1
  67. package/dist/esm/auth-generator.js +34 -1
  68. package/dist/esm/auth-generator.js.map +1 -1
  69. package/dist/esm/await-result.d.ts +13 -5
  70. package/dist/esm/await-result.d.ts.map +1 -1
  71. package/dist/esm/await-result.js +18 -7
  72. package/dist/esm/await-result.js.map +1 -1
  73. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  74. package/dist/esm/data-api-route-generator.js +9 -1
  75. package/dist/esm/data-api-route-generator.js.map +1 -1
  76. package/dist/esm/executor-generator.d.ts.map +1 -1
  77. package/dist/esm/executor-generator.js +18 -6
  78. package/dist/esm/executor-generator.js.map +1 -1
  79. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  80. package/dist/esm/invoice/invoice-html-code.js +96 -5
  81. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  82. package/dist/esm/nodes/account/account-delete-current.d.ts.map +1 -1
  83. package/dist/esm/nodes/account/account-delete-current.js +4 -1
  84. package/dist/esm/nodes/account/account-delete-current.js.map +1 -1
  85. package/dist/esm/server-runtime-code.d.ts.map +1 -1
  86. package/dist/esm/server-runtime-code.js +21 -0
  87. package/dist/esm/server-runtime-code.js.map +1 -1
  88. package/dist/esm/transactional-email-code.d.ts +7 -0
  89. package/dist/esm/transactional-email-code.d.ts.map +1 -1
  90. package/dist/esm/transactional-email-code.js +5 -5
  91. package/dist/esm/transactional-email-code.js.map +1 -1
  92. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  93. package/dist/esm/vercel-functions.d.ts +34 -0
  94. package/dist/esm/vercel-functions.d.ts.map +1 -0
  95. package/dist/esm/vercel-functions.js +57 -0
  96. package/dist/esm/vercel-functions.js.map +1 -0
  97. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.d.ts +6 -0
  98. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.d.ts.map +1 -1
  99. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.js +26 -11
  100. package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.js.map +1 -1
  101. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  102. package/dist/esm/workflow-project-plugin.js +3 -0
  103. package/dist/esm/workflow-project-plugin.js.map +1 -1
  104. package/package.json +2 -2
  105. package/src/account-delete-route-generator.ts +97 -45
  106. package/src/api-route-generator.ts +12 -13
  107. package/src/auth-generator.ts +34 -1
  108. package/src/await-result.ts +19 -7
  109. package/src/data-api-route-generator.ts +9 -1
  110. package/src/executor-generator.ts +18 -6
  111. package/src/invoice/invoice-html-code.ts +96 -5
  112. package/src/nodes/account/account-delete-current.ts +4 -1
  113. package/src/server-runtime-code.ts +22 -0
  114. package/src/transactional-email-code.ts +12 -5
  115. package/src/vercel-functions.ts +67 -0
  116. package/src/webhook-delivery-lease/webhook-delivery-lease-code.ts +26 -11
  117. package/src/workflow-project-plugin.ts +3 -0
@@ -0,0 +1,294 @@
1
+ import { generateServerSegmentAPIRoute } from '../src/api-route-generator'
2
+ import { generateSharedRuntimeUtilsCode } from '../src/executor-generator'
3
+ import { isFireAndForgetNode } from '../src/await-result'
4
+ import { NextWorkflowProjectPlugin } from '../src/workflow-project-plugin'
5
+ import { VERCEL_FUNCTIONS_PACKAGE, VERCEL_FUNCTIONS_VERSION } from '../src/vercel-functions'
6
+ import type { WorkflowSegment } from '../src/types'
7
+ import { loadServerRuntime } from './_helpers/load-server-runtime'
8
+
9
+ /**
10
+ * A step the author chose not to wait for still has to finish on the server,
11
+ * but the visitor no longer waits for it: a cash order's invoice (rendered,
12
+ * stored and mailed, 8–14 s) runs on after the segment has replied. On Vercel
13
+ * the route hands that work to waitUntil; anywhere else it awaits it before
14
+ * replying, as before.
15
+ */
16
+
17
+ const REQUEST_CONTEXT = Symbol.for('@vercel/request-context')
18
+
19
+ const invoiceNode = (awaitResult?: boolean) => ({
20
+ id: 'invoice',
21
+ type: 'ecommerce-generate-invoice',
22
+ label: 'Generate Invoice',
23
+ stepNumber: 1,
24
+ config: { orderId: 'order-1', ...(awaitResult === undefined ? {} : { awaitResult }) },
25
+ })
26
+
27
+ const segmentOf = (node: ReturnType<typeof invoiceNode>): WorkflowSegment =>
28
+ ({
29
+ id: 'seg-cash',
30
+ env: 'server',
31
+ nodeIds: [node.id],
32
+ nodes: [node],
33
+ edges: [],
34
+ } as unknown as WorkflowSegment)
35
+
36
+ const deferred = () => {
37
+ let resolve: () => void = () => undefined
38
+ const promise = new Promise<void>((done) => {
39
+ resolve = done
40
+ })
41
+ return { promise, resolve }
42
+ }
43
+
44
+ const settleMicrotasks = () => new Promise((done) => setTimeout(done, 20))
45
+
46
+ interface Reply {
47
+ status: number
48
+ body: any
49
+ }
50
+
51
+ const originalFetch = globalThis.fetch
52
+
53
+ beforeEach(() => {
54
+ jest.spyOn(console, 'info').mockImplementation(() => undefined)
55
+ })
56
+
57
+ afterEach(() => {
58
+ jest.restoreAllMocks()
59
+ globalThis.fetch = originalFetch
60
+ delete (globalThis as Record<symbol, unknown>)[REQUEST_CONTEXT]
61
+ })
62
+
63
+ /**
64
+ * Boots the generated segment route with `modules` installed, and an invoice
65
+ * route that answers only once `invoiceGate` opens.
66
+ */
67
+ const bootRoute = (node: ReturnType<typeof invoiceNode>, modules: Record<string, unknown>) => {
68
+ const invoiceGate = deferred()
69
+ const progress = { invoiceFinished: false }
70
+ globalThis.fetch = (async () => {
71
+ await invoiceGate.promise
72
+ progress.invoiceFinished = true
73
+ return { ok: true, status: 200, json: async () => ({ invoiceId: 'inv-1' }) }
74
+ }) as unknown as typeof fetch
75
+
76
+ const runtime = loadServerRuntime(modules)
77
+ const routeModule: { exports: unknown } = { exports: {} }
78
+ // tslint:disable-next-line:function-constructor
79
+ new Function('module', 'exports', 'require', generateServerSegmentAPIRoute(segmentOf(node)))(
80
+ routeModule,
81
+ routeModule.exports,
82
+ (id: string) => {
83
+ if (id.endsWith('server-runtime')) {
84
+ return runtime
85
+ }
86
+ throw new Error('Unexpected require: ' + id)
87
+ }
88
+ )
89
+ const handler = routeModule.exports as (req: unknown, res: unknown) => Promise<void>
90
+ const reply: Reply = { status: 0, body: undefined }
91
+ const res = {
92
+ status(code: number) {
93
+ reply.status = code
94
+ return this
95
+ },
96
+ json(payload: unknown) {
97
+ reply.body = payload
98
+ return this
99
+ },
100
+ setHeader: (): void => undefined,
101
+ }
102
+ const handled = handler(
103
+ { method: 'POST', body: { context: {} }, headers: { host: 'shop.example' } },
104
+ res
105
+ )
106
+ return { handled, reply, progress, openInvoiceGate: invoiceGate.resolve }
107
+ }
108
+
109
+ const onVercel = () => {
110
+ ;(globalThis as Record<symbol, unknown>)[REQUEST_CONTEXT] = {
111
+ get: () => ({ waitUntil: (): void => undefined }),
112
+ }
113
+ }
114
+
115
+ const fakeVercelFunctions = () => {
116
+ const handedOff: Array<Promise<unknown>> = []
117
+ return {
118
+ handedOff,
119
+ module: {
120
+ waitUntil: (promise: Promise<unknown>) => {
121
+ handedOff.push(promise)
122
+ },
123
+ },
124
+ }
125
+ }
126
+
127
+ describe('the invoice step may be left not awaited', () => {
128
+ it('is fire-and-forget only on an explicit awaitResult: false', () => {
129
+ expect(isFireAndForgetNode(invoiceNode(false) as never)).toBe(true)
130
+ expect(isFireAndForgetNode(invoiceNode(true) as never)).toBe(false)
131
+ expect(isFireAndForgetNode(invoiceNode() as never)).toBe(false)
132
+ })
133
+
134
+ it('is mirrored inside the generated runtime', () => {
135
+ const shared: { exports: Record<string, any> } = { exports: {} }
136
+ // tslint:disable-next-line:function-constructor
137
+ new Function('module', 'exports', generateSharedRuntimeUtilsCode())(shared, shared.exports)
138
+ expect(shared.exports.isFireAndForgetNode(invoiceNode(false))).toBe(true)
139
+ expect(shared.exports.isFireAndForgetNode(invoiceNode())).toBe(false)
140
+ })
141
+ })
142
+
143
+ describe('a segment route with a not-awaited step', () => {
144
+ it('replies on Vercel before the step finishes, and hands the step to waitUntil', async () => {
145
+ onVercel()
146
+ const vercel = fakeVercelFunctions()
147
+ const run = bootRoute(invoiceNode(false), { [VERCEL_FUNCTIONS_PACKAGE]: vercel.module })
148
+
149
+ const outcome = await Promise.race([
150
+ run.handled.then(() => 'replied'),
151
+ settleMicrotasks().then(() => 'still waiting'),
152
+ ])
153
+
154
+ expect(outcome).toBe('replied')
155
+ expect(run.reply.status).toBe(200)
156
+ expect(run.reply.body.success).toBe(true)
157
+ expect(run.progress.invoiceFinished).toBe(false)
158
+ expect(vercel.handedOff).toHaveLength(1)
159
+
160
+ run.openInvoiceGate()
161
+ await vercel.handedOff[0]
162
+ expect(run.progress.invoiceFinished).toBe(true)
163
+ })
164
+
165
+ it('waits for the step before replying outside a Vercel request', async () => {
166
+ const vercel = fakeVercelFunctions()
167
+ const run = bootRoute(invoiceNode(false), { [VERCEL_FUNCTIONS_PACKAGE]: vercel.module })
168
+
169
+ await settleMicrotasks()
170
+ expect(run.reply.status).toBe(0)
171
+
172
+ run.openInvoiceGate()
173
+ await run.handled
174
+ expect(run.progress.invoiceFinished).toBe(true)
175
+ expect(run.reply.status).toBe(200)
176
+ expect(vercel.handedOff).toHaveLength(0)
177
+ })
178
+
179
+ it('waits for the step before replying when @vercel/functions is not installed', async () => {
180
+ onVercel()
181
+ const run = bootRoute(invoiceNode(false), {})
182
+
183
+ await settleMicrotasks()
184
+ expect(run.reply.status).toBe(0)
185
+
186
+ run.openInvoiceGate()
187
+ await run.handled
188
+ expect(run.progress.invoiceFinished).toBe(true)
189
+ expect(run.reply.status).toBe(200)
190
+ })
191
+
192
+ it('still runs an awaited invoice step before replying, on Vercel too', async () => {
193
+ onVercel()
194
+ const vercel = fakeVercelFunctions()
195
+ const run = bootRoute(invoiceNode(), { [VERCEL_FUNCTIONS_PACKAGE]: vercel.module })
196
+
197
+ await settleMicrotasks()
198
+ expect(run.reply.status).toBe(0)
199
+
200
+ run.openInvoiceGate()
201
+ await run.handled
202
+ expect(run.reply.body.results.invoice.invoiceId).toBe('inv-1')
203
+ expect(vercel.handedOff).toHaveLength(0)
204
+ })
205
+ })
206
+
207
+ describe('the generated project declares @vercel/functions', () => {
208
+ const generate = async (
209
+ workflows: Record<string, unknown>,
210
+ dependencies: Record<string, string> = {}
211
+ ) => {
212
+ const structure: any = {
213
+ uidl: {
214
+ name: 'Store',
215
+ globals: { settings: { title: 'Store', language: 'en' }, env: {} },
216
+ root: {
217
+ name: 'Root',
218
+ stateDefinitions: {
219
+ route: { type: 'string', defaultValue: 'Home', values: [{ value: 'Home' }] },
220
+ },
221
+ node: { type: 'element', content: { elementType: 'container', children: [] } },
222
+ },
223
+ components: {},
224
+ workflows: { workflows },
225
+ },
226
+ strategy: { pages: { options: {} } },
227
+ files: new Map(),
228
+ dependencies: { ...dependencies },
229
+ devDependencies: {},
230
+ }
231
+ await new NextWorkflowProjectPlugin().runAfter(structure)
232
+ return structure
233
+ }
234
+
235
+ const cronWorkflow = {
236
+ id: 'wf-cron',
237
+ name: 'Nightly count',
238
+ trigger: {
239
+ type: 'event-cron-triggered',
240
+ nodeId: 't',
241
+ scope: 'global',
242
+ config: { schedule: '0 * * * *', urlPath: 'cron_nightly' },
243
+ },
244
+ nodes: [
245
+ {
246
+ id: 'count',
247
+ type: 'data-count',
248
+ config: { dataSourceId: 'ds-1', tableName: 'orders', filters: [] },
249
+ stepNumber: 1,
250
+ label: 'Count orders',
251
+ },
252
+ ],
253
+ edges: [{ id: 'e1', source: 't', target: 'count' }],
254
+ }
255
+
256
+ const filesRequiringIt = (structure: any): string[] => {
257
+ const names: string[] = []
258
+ for (const record of structure.files.values()) {
259
+ for (const file of record.files || []) {
260
+ if (
261
+ typeof file.content === 'string' &&
262
+ file.content.includes(`require('${VERCEL_FUNCTIONS_PACKAGE}')`)
263
+ ) {
264
+ names.push([...record.path, file.name].join('/'))
265
+ }
266
+ }
267
+ }
268
+ return names
269
+ }
270
+
271
+ it('when an emitted file requires it', async () => {
272
+ const structure = await generate({ [cronWorkflow.id]: cronWorkflow })
273
+
274
+ expect(filesRequiringIt(structure)).toContain('utils/workflows/server-runtime')
275
+ expect(structure.dependencies[VERCEL_FUNCTIONS_PACKAGE]).toBe(VERCEL_FUNCTIONS_VERSION)
276
+ })
277
+
278
+ it('keeps a version the project already declares', async () => {
279
+ const structure = await generate(
280
+ { [cronWorkflow.id]: cronWorkflow },
281
+ { [VERCEL_FUNCTIONS_PACKAGE]: '^2.2.10' }
282
+ )
283
+
284
+ expect(filesRequiringIt(structure)).toContain('utils/workflows/server-runtime')
285
+ expect(structure.dependencies[VERCEL_FUNCTIONS_PACKAGE]).toBe('^2.2.10')
286
+ })
287
+
288
+ it('never without a file that requires it', async () => {
289
+ const structure = await generate({})
290
+
291
+ expect(filesRequiringIt(structure)).toEqual([])
292
+ expect(structure.dependencies[VERCEL_FUNCTIONS_PACKAGE]).toBeUndefined()
293
+ })
294
+ })
@@ -0,0 +1,86 @@
1
+ import { generateSharedRuntimeUtilsCode } from '../src'
2
+
3
+ /**
4
+ * The editor's workflow templates (delete account, password reset request,
5
+ * bookings, reviews, social, loyalty) write an if-statement's sides as
6
+ * `leftOperand` / `rightOperand` with the operator `'='`. The runtime read only
7
+ * `leftValue` / `rightValue`, so each of those checks compared undefined with
8
+ * undefined and always took its "true" branch. Both spellings are read now,
9
+ * and `'='` is equality.
10
+ */
11
+
12
+ type SharedUtils = {
13
+ resolveConfig: (cfg: unknown, ctx: Record<string, unknown>) => Record<string, unknown>
14
+ evaluateCondition: (cfg: unknown, ctx: Record<string, unknown>) => boolean
15
+ }
16
+
17
+ function loadSharedRuntime(): SharedUtils {
18
+ const wrapper: { exports: Record<string, unknown> } = { exports: {} }
19
+ // tslint:disable-next-line:function-constructor
20
+ new Function('module', 'exports', generateSharedRuntimeUtilsCode())(wrapper, wrapper.exports)
21
+ return wrapper.exports as unknown as SharedUtils
22
+ }
23
+
24
+ const ref = (nodeId: string, ...path: string[]) => ({
25
+ type: 'workflowContext',
26
+ nodeId,
27
+ path: [nodeId, ...path],
28
+ })
29
+
30
+ // The "Is Password Correct?" step of the editor's delete-account template.
31
+ const isPasswordCorrect = {
32
+ conditionType: 'simple-comparison',
33
+ leftOperand: ref('verify-password', 'isMatch'),
34
+ operator: '=',
35
+ rightOperand: true,
36
+ }
37
+
38
+ describe('an if-statement written with leftOperand / rightOperand', () => {
39
+ const utils = loadSharedRuntime()
40
+ // What every route does with an if-statement node: resolve, then evaluate.
41
+ const evaluate = (config: unknown, context: Record<string, unknown>) =>
42
+ utils.evaluateCondition(utils.resolveConfig(config, context), context)
43
+
44
+ it('takes the branch its operands choose', () => {
45
+ expect(evaluate(isPasswordCorrect, { 'verify-password': { isMatch: true } })).toBe(true)
46
+ expect(evaluate(isPasswordCorrect, { 'verify-password': { isMatch: false } })).toBe(false)
47
+ })
48
+
49
+ it('reads a missing operand as missing, not as equal to the other side', () => {
50
+ expect(evaluate(isPasswordCorrect, {})).toBe(false)
51
+ })
52
+
53
+ it("treats '=' as equality without warning about the operator", () => {
54
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
55
+ try {
56
+ expect(evaluate({ leftOperand: 'a', operator: '=', rightOperand: 'a' }, {})).toBe(true)
57
+ expect(evaluate({ leftOperand: 'a', operator: '=', rightOperand: 'b' }, {})).toBe(false)
58
+ expect(evaluate({ leftValue: 3, operator: '=', rightValue: '3' }, {})).toBe(true)
59
+ expect(warn).not.toHaveBeenCalled()
60
+ } finally {
61
+ warn.mockRestore()
62
+ }
63
+ })
64
+
65
+ it('applies inside multiple conditions', () => {
66
+ const both = {
67
+ conditionType: 'multiple-conditions',
68
+ logicOperator: 'AND',
69
+ conditions: [
70
+ { leftOperand: ref('form', 'agreed'), operator: '=', rightOperand: true },
71
+ { leftOperand: ref('form', 'email'), operator: 'is-not-empty' },
72
+ ],
73
+ }
74
+ expect(evaluate(both, { form: { agreed: true, email: 'a@b.c' } })).toBe(true)
75
+ expect(evaluate(both, { form: { agreed: false, email: 'a@b.c' } })).toBe(false)
76
+ })
77
+
78
+ it('keeps leftValue / rightValue first when a config carries both', () => {
79
+ expect(
80
+ evaluate(
81
+ { leftValue: 'x', leftOperand: 'y', operator: '===', rightValue: 'x', rightOperand: 'z' },
82
+ {}
83
+ )
84
+ ).toBe(true)
85
+ })
86
+ })
@@ -76,30 +76,92 @@ const loadLease = (options: {
76
76
  }
77
77
 
78
78
  describe('webhook delivery lease module', () => {
79
- it('creates its table once, claims with a fresh token and releases only its own claim', async () => {
79
+ it('creates a missing table once, claims with a fresh token and releases only its own claim', async () => {
80
80
  const lease = loadLease({})
81
81
  try {
82
82
  const first = await lease.claim('webhooks/stripe-payment', Buffer.from('{"id":"evt_1"}'), {
83
83
  id: 'evt_1',
84
84
  })
85
85
  expect(first.claimed).toBe(true)
86
- expect(lease.queries[0].sql).toMatch(
86
+ expect(lease.queries[0]).toEqual({
87
+ sql: 'SELECT to_regclass($1) IS NOT NULL AS present',
88
+ values: ['teleport_webhook_deliveries'],
89
+ })
90
+ expect(lease.queries[1].sql).toMatch(
87
91
  /^CREATE TABLE IF NOT EXISTS "teleport_webhook_deliveries"/
88
92
  )
89
- const claim = lease.queries[1]
93
+ const claim = lease.queries[2]
90
94
  expect(claim.sql).toContain('ON CONFLICT (delivery_key) DO UPDATE')
91
95
  expect(claim.sql).toContain('WHERE d.claimed_at < NOW() - make_interval(secs => $3::int)')
92
96
  expect(claim.values[0]).toMatch(/^[0-9a-f]{64}$/)
93
97
  expect(claim.values[2]).toBe(300)
94
98
  await first.release()
95
- const release = lease.queries[2]
99
+ const release = lease.queries[3]
96
100
  expect(release.sql).toMatch(
97
101
  /^DELETE FROM "teleport_webhook_deliveries" WHERE \(delivery_key = \$1 AND claim_token = \$2\)/
98
102
  )
99
103
  expect(release.values).toEqual([claim.values[0], claim.values[1]])
100
- // The table is not created again by the next delivery.
104
+ // The table is neither looked up nor created again by the next delivery.
101
105
  await lease.claim('webhooks/stripe-payment', Buffer.from('{"id":"evt_2"}'), { id: 'evt_2' })
102
- expect(lease.queries[3].sql).toMatch(/^INSERT INTO/)
106
+ expect(lease.queries[4].sql).toMatch(/^INSERT INTO/)
107
+ } finally {
108
+ lease.restore()
109
+ }
110
+ })
111
+
112
+ it('uses a table that exists without trying to create it', async () => {
113
+ // The site's role may write the table the editor made, not create one.
114
+ const lease = loadLease({
115
+ queryImpl: async (sql: string, values: unknown[]) => {
116
+ if (sql.startsWith('SELECT to_regclass')) {
117
+ return { rows: [{ present: true }] }
118
+ }
119
+ if (sql.startsWith('CREATE')) {
120
+ throw Object.assign(new Error('permission denied for schema public'), { code: '42501' })
121
+ }
122
+ return sql.startsWith('INSERT') ? { rows: [{ claim_token: values[1] }] } : { rows: [] }
123
+ },
124
+ })
125
+ try {
126
+ const first = await lease.claim('webhooks/stripe-payment', Buffer.from('{"id":"evt_1"}'), {
127
+ id: 'evt_1',
128
+ })
129
+ expect(first.claimed).toBe(true)
130
+ expect(lease.queries.map((query) => query.sql.split(' ')[0])).toEqual(['SELECT', 'INSERT'])
131
+
132
+ // A twin of a held delivery is turned away: the lease is in force.
133
+ const twin = loadLease({
134
+ queryImpl: async (sql: string) =>
135
+ sql.startsWith('SELECT to_regclass') ? { rows: [{ present: true }] } : { rows: [] },
136
+ })
137
+ try {
138
+ expect((await twin.claim('w', Buffer.from('{}'), {})).claimed).toBe(false)
139
+ expect(twin.queries.some((query) => query.sql.startsWith('CREATE'))).toBe(false)
140
+ } finally {
141
+ twin.restore()
142
+ }
143
+ } finally {
144
+ lease.restore()
145
+ }
146
+ })
147
+
148
+ it('lets every delivery run when the table is missing and cannot be created', async () => {
149
+ const lease = loadLease({
150
+ queryImpl: async (sql: string) => {
151
+ if (sql.startsWith('SELECT to_regclass')) {
152
+ return { rows: [{ present: false }] }
153
+ }
154
+ if (sql.startsWith('CREATE')) {
155
+ throw Object.assign(new Error('permission denied for schema public'), { code: '42501' })
156
+ }
157
+ return { rows: [] }
158
+ },
159
+ })
160
+ try {
161
+ expect((await lease.claim('w', Buffer.from('{}'), {})).claimed).toBe(true)
162
+ const tried = lease.queries.length
163
+ expect((await lease.claim('w', Buffer.from('{}'), {})).claimed).toBe(true)
164
+ expect(lease.queries).toHaveLength(tried)
103
165
  } finally {
104
166
  lease.restore()
105
167
  }
@@ -4,9 +4,14 @@
4
4
  *
5
5
  * The client node calls this route, which:
6
6
  * 1. resolves the current user from the NextAuth session (never trusting a
7
- * client-supplied id),
7
+ * client-supplied id) and, when the request carries a password, goes on
8
+ * only if it is the account's own,
8
9
  * 2. runs a single DB transaction that detaches + anonymises business/legal
9
- * records (orders, withdrawal requests, bookings) so tracking survives,
10
+ * records (orders, withdrawal requests, bookings, downloads, gift cards
11
+ * the user bought or received) so tracking survives, takes the user's
12
+ * email out of the activity log and the sent-email ledger, leaves the
13
+ * invoices already issued as they are (an invoice must keep naming its
14
+ * buyer),
10
15
  * deletes the user's personal data (reviews, cart, favourites, chat
11
16
  * history), deletes the rows the user owns in the project's own tables —
12
17
  * the ones the AI made for the site, each through the one column the
@@ -1 +1 @@
1
- {"version":3,"file":"account-delete-route-generator.d.ts","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAaxE,MAAM,WAAW,yBAAyB;IAExC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAI3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IAGtB,kBAAkB,CAAC,EAAE,2BAA2B,CAAA;IAEhD,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAID,eAAO,MAAM,8BAA8B,+CAAiC,CAAA;AAE5E,eAAO,MAAM,0BAA0B,aAAa,yBAAyB,KAAQ,MAqXpF,CAAA"}
1
+ {"version":3,"file":"account-delete-route-generator.d.ts","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAaxE,MAAM,WAAW,yBAAyB;IAExC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAI3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IAGtB,kBAAkB,CAAC,EAAE,2BAA2B,CAAA;IAEhD,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAID,eAAO,MAAM,8BAA8B,+CAAiC,CAAA;AAE5E,eAAO,MAAM,0BAA0B,aAAa,yBAAyB,KAAQ,MAoapF,CAAA"}