@teleporthq/teleport-plugin-next-workflows 0.43.42 → 0.43.45
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__/cart-availability-add-to-cart.test.ts +47 -0
- package/__tests__/cron-route-loop-templateparams.test.ts +144 -0
- package/__tests__/data-api-on-conflict-do-nothing.test.ts +35 -0
- package/__tests__/email-body-context-resolution.test.ts +126 -0
- package/__tests__/raw-sql-param-binding.test.ts +138 -0
- package/dist/cjs/account-delete-route-generator.d.ts +13 -0
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -0
- package/dist/cjs/account-delete-route-generator.js +247 -0
- package/dist/cjs/account-delete-route-generator.js.map +1 -0
- package/dist/cjs/api-route-generator.d.ts +1 -1
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +29 -33
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts +9 -1
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +53 -2
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
- package/dist/cjs/data-api-route-generator.js +11 -2
- package/dist/cjs/data-api-route-generator.js.map +1 -1
- package/dist/cjs/ecommerce/cart-availability.d.ts.map +1 -1
- package/dist/cjs/ecommerce/cart-availability.js +49 -2
- package/dist/cjs/ecommerce/cart-availability.js.map +1 -1
- package/dist/cjs/ecommerce/stock-decrement.d.ts +1 -1
- package/dist/cjs/ecommerce/stock-decrement.d.ts.map +1 -1
- package/dist/cjs/ecommerce/stock-decrement.js +48 -10
- package/dist/cjs/ecommerce/stock-decrement.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +88 -1
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/graph-utils.d.ts.map +1 -1
- package/dist/cjs/graph-utils.js +1 -0
- package/dist/cjs/graph-utils.js.map +1 -1
- package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/cjs/invoice/api-routes-code.js +2 -0
- package/dist/cjs/invoice/api-routes-code.js.map +1 -1
- package/dist/cjs/invoice/data-access-code.d.ts.map +1 -1
- package/dist/cjs/invoice/data-access-code.js +2 -0
- package/dist/cjs/invoice/data-access-code.js.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.js +22 -2
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/nodes/account/account-delete-current.d.ts +3 -0
- package/dist/cjs/nodes/account/account-delete-current.d.ts.map +1 -0
- package/dist/cjs/nodes/account/account-delete-current.js +79 -0
- package/dist/cjs/nodes/account/account-delete-current.js.map +1 -0
- package/dist/cjs/nodes/account/account-signup.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-signup.js +20 -1
- package/dist/cjs/nodes/account/account-signup.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 +7 -0
- package/dist/cjs/nodes/data/data-create-item.js.map +1 -1
- package/dist/cjs/nodes/index.d.ts.map +1 -1
- package/dist/cjs/nodes/index.js +2 -1
- package/dist/cjs/nodes/index.js.map +1 -1
- package/dist/cjs/nodes/payment/payment-charge-user.d.ts.map +1 -1
- package/dist/cjs/nodes/payment/payment-charge-user.js +9 -1
- package/dist/cjs/nodes/payment/payment-charge-user.js.map +1 -1
- package/dist/cjs/pg-client-code.d.ts +11 -0
- package/dist/cjs/pg-client-code.d.ts.map +1 -0
- package/dist/cjs/pg-client-code.js +75 -0
- package/dist/cjs/pg-client-code.js.map +1 -0
- package/dist/cjs/raw-sql-param-binding.d.ts +82 -0
- package/dist/cjs/raw-sql-param-binding.d.ts.map +1 -0
- package/dist/cjs/raw-sql-param-binding.js +321 -0
- package/dist/cjs/raw-sql-param-binding.js.map +1 -0
- package/dist/cjs/transactional-email-code.d.ts +6 -0
- package/dist/cjs/transactional-email-code.d.ts.map +1 -0
- package/dist/cjs/transactional-email-code.js +112 -0
- package/dist/cjs/transactional-email-code.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-component-plugin.js +10 -1
- package/dist/cjs/workflow-component-plugin.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +153 -27
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/account-delete-route-generator.d.ts +13 -0
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -0
- package/dist/esm/account-delete-route-generator.js +243 -0
- package/dist/esm/account-delete-route-generator.js.map +1 -0
- package/dist/esm/api-route-generator.d.ts +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +29 -33
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts +9 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +53 -2
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/data-api-route-generator.d.ts.map +1 -1
- package/dist/esm/data-api-route-generator.js +11 -2
- package/dist/esm/data-api-route-generator.js.map +1 -1
- package/dist/esm/ecommerce/cart-availability.d.ts.map +1 -1
- package/dist/esm/ecommerce/cart-availability.js +49 -2
- package/dist/esm/ecommerce/cart-availability.js.map +1 -1
- package/dist/esm/ecommerce/stock-decrement.d.ts +1 -1
- package/dist/esm/ecommerce/stock-decrement.d.ts.map +1 -1
- package/dist/esm/ecommerce/stock-decrement.js +48 -10
- package/dist/esm/ecommerce/stock-decrement.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +88 -1
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/graph-utils.d.ts.map +1 -1
- package/dist/esm/graph-utils.js +1 -0
- package/dist/esm/graph-utils.js.map +1 -1
- package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/esm/invoice/api-routes-code.js +2 -0
- package/dist/esm/invoice/api-routes-code.js.map +1 -1
- package/dist/esm/invoice/data-access-code.d.ts.map +1 -1
- package/dist/esm/invoice/data-access-code.js +2 -0
- package/dist/esm/invoice/data-access-code.js.map +1 -1
- package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-html-code.js +22 -2
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/nodes/account/account-delete-current.d.ts +3 -0
- package/dist/esm/nodes/account/account-delete-current.d.ts.map +1 -0
- package/dist/esm/nodes/account/account-delete-current.js +76 -0
- package/dist/esm/nodes/account/account-delete-current.js.map +1 -0
- package/dist/esm/nodes/account/account-signup.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-signup.js +20 -1
- package/dist/esm/nodes/account/account-signup.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 +7 -0
- package/dist/esm/nodes/data/data-create-item.js.map +1 -1
- package/dist/esm/nodes/index.d.ts.map +1 -1
- package/dist/esm/nodes/index.js +2 -1
- package/dist/esm/nodes/index.js.map +1 -1
- package/dist/esm/nodes/payment/payment-charge-user.d.ts.map +1 -1
- package/dist/esm/nodes/payment/payment-charge-user.js +9 -1
- package/dist/esm/nodes/payment/payment-charge-user.js.map +1 -1
- package/dist/esm/pg-client-code.d.ts +11 -0
- package/dist/esm/pg-client-code.d.ts.map +1 -0
- package/dist/esm/pg-client-code.js +71 -0
- package/dist/esm/pg-client-code.js.map +1 -0
- package/dist/esm/raw-sql-param-binding.d.ts +82 -0
- package/dist/esm/raw-sql-param-binding.d.ts.map +1 -0
- package/dist/esm/raw-sql-param-binding.js +314 -0
- package/dist/esm/raw-sql-param-binding.js.map +1 -0
- package/dist/esm/transactional-email-code.d.ts +6 -0
- package/dist/esm/transactional-email-code.d.ts.map +1 -0
- package/dist/esm/transactional-email-code.js +106 -0
- package/dist/esm/transactional-email-code.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-component-plugin.js +10 -1
- package/dist/esm/workflow-component-plugin.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +153 -27
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +4 -4
- package/src/account-delete-route-generator.ts +269 -0
- package/src/api-route-generator.ts +35 -33
- package/src/auth-generator.ts +77 -2
- package/src/data-api-route-generator.ts +11 -2
- package/src/ecommerce/cart-availability.ts +49 -2
- package/src/ecommerce/stock-decrement.ts +48 -10
- package/src/executor-generator.ts +88 -1
- package/src/graph-utils.ts +1 -0
- package/src/invoice/api-routes-code.ts +2 -0
- package/src/invoice/data-access-code.ts +2 -0
- package/src/invoice/invoice-html-code.ts +22 -2
- package/src/nodes/account/account-delete-current.ts +86 -0
- package/src/nodes/account/account-signup.ts +20 -1
- package/src/nodes/data/data-create-item.ts +7 -0
- package/src/nodes/index.ts +2 -0
- package/src/nodes/payment/payment-charge-user.ts +9 -1
- package/src/pg-client-code.ts +70 -0
- package/src/raw-sql-param-binding.ts +406 -0
- package/src/transactional-email-code.ts +112 -0
- package/src/workflow-component-plugin.ts +14 -1
- package/src/workflow-project-plugin.ts +178 -25
|
@@ -352,6 +352,53 @@ describe('buildAddToCartLimitCheck — runtime semantics', () => {
|
|
|
352
352
|
expect(result.existingItemId).toBe('cart-1')
|
|
353
353
|
})
|
|
354
354
|
|
|
355
|
+
// Variant-aware de-dup: a cart line is identified by product AND variant.
|
|
356
|
+
const buildParamsV = (
|
|
357
|
+
productId: string,
|
|
358
|
+
variantId: string | null,
|
|
359
|
+
quantity: number,
|
|
360
|
+
settings: any,
|
|
361
|
+
product: any,
|
|
362
|
+
cartItems: any[]
|
|
363
|
+
): any[] => {
|
|
364
|
+
const arr: any[] = new Array(14).fill(null)
|
|
365
|
+
arr[0] = { productId, variantId, quantity }
|
|
366
|
+
arr[2] = settings
|
|
367
|
+
arr[6] = product ? { found: true, product } : { found: false, product: null }
|
|
368
|
+
arr[13] = cartItems
|
|
369
|
+
return arr
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
it('same product, DIFFERENT variant already in cart → treated as a NEW line (no increment)', () => {
|
|
373
|
+
const params = buildParamsV('a', 'a__blue', 1, baseSettings(), { name: 'Tee', quantity: 5 }, [
|
|
374
|
+
{ id: 'cart-red', productId: 'a', variantId: 'a__red', quantity: 1 },
|
|
375
|
+
])
|
|
376
|
+
const result = fn({}, params)
|
|
377
|
+
expect(result.canAdd).toBe(true)
|
|
378
|
+
expect(result.shouldIncrement).toBe(false)
|
|
379
|
+
expect(result.existingItemId).toBe(null)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('same product, SAME variant already in cart → increments THAT line', () => {
|
|
383
|
+
const params = buildParamsV('a', 'a__red', 1, baseSettings(), { name: 'Tee', quantity: 5 }, [
|
|
384
|
+
{ id: 'cart-red', productId: 'a', variantId: 'a__red', quantity: 1 },
|
|
385
|
+
{ id: 'cart-blue', productId: 'a', variantId: 'a__blue', quantity: 1 },
|
|
386
|
+
])
|
|
387
|
+
const result = fn({}, params)
|
|
388
|
+
expect(result.canAdd).toBe(true)
|
|
389
|
+
expect(result.shouldIncrement).toBe(true)
|
|
390
|
+
expect(result.existingItemId).toBe('cart-red')
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('flat product (no variantId) matches the flat cart line', () => {
|
|
394
|
+
const params = buildParamsV('a', null, 1, baseSettings(), { name: 'Mug', quantity: 5 }, [
|
|
395
|
+
{ id: 'cart-flat', productId: 'a', quantity: 2 },
|
|
396
|
+
])
|
|
397
|
+
const result = fn({}, params)
|
|
398
|
+
expect(result.shouldIncrement).toBe(true)
|
|
399
|
+
expect(result.existingItemId).toBe('cart-flat')
|
|
400
|
+
})
|
|
401
|
+
|
|
355
402
|
it('new item would exceed maxQuantityPerProduct → canAdd:false, names product', () => {
|
|
356
403
|
const params = buildParams(
|
|
357
404
|
'a',
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Regression test for the abandoned-cart reminder feature's codegen dependency:
|
|
2
|
+
// a scheduled (cron) workflow must generate a route that (a) executes a
|
|
3
|
+
// `general-loop` body per row and (b) merges an email node's `templateParams`
|
|
4
|
+
// into its component body — the cron route previously used a naive inline loop
|
|
5
|
+
// that did neither, so a per-cart reminder email would ship raw {{token}} merge
|
|
6
|
+
// fields and its loop body would never run.
|
|
7
|
+
import { generateCronAPIRoute } from '../src/api-route-generator'
|
|
8
|
+
|
|
9
|
+
const TRIGGER_ID = 'cron-1'
|
|
10
|
+
const SCAN_ID = 'scan-1'
|
|
11
|
+
const LOOP_ID = 'loop-1'
|
|
12
|
+
const SHIM_ID = 'shim-1'
|
|
13
|
+
const CREATE_ID = 'create-1'
|
|
14
|
+
const EMAIL_ID = 'email-1'
|
|
15
|
+
|
|
16
|
+
const buildCronWorkflow = (): any => ({
|
|
17
|
+
id: 'wf-abandoned-cart',
|
|
18
|
+
name: 'Abandoned Cart Reminder',
|
|
19
|
+
trigger: {
|
|
20
|
+
type: 'event-cron-triggered',
|
|
21
|
+
nodeId: TRIGGER_ID,
|
|
22
|
+
scope: 'global',
|
|
23
|
+
config: { schedule: '0 * * * *', timezone: 'UTC', urlPath: 'cron_abandoned' },
|
|
24
|
+
},
|
|
25
|
+
nodes: [
|
|
26
|
+
{
|
|
27
|
+
id: SCAN_ID,
|
|
28
|
+
type: 'data-raw-query',
|
|
29
|
+
config: { dataSourceId: 'ds-1', query: 'SELECT 1', params: ['2'] },
|
|
30
|
+
executionEnv: 'server',
|
|
31
|
+
stepNumber: 1,
|
|
32
|
+
label: 'Scan Abandoned Carts',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: LOOP_ID,
|
|
36
|
+
type: 'general-loop',
|
|
37
|
+
config: {
|
|
38
|
+
loopType: 'forEach',
|
|
39
|
+
collection: { type: 'workflowContext', nodeId: SCAN_ID, path: [SCAN_ID, 'result'] },
|
|
40
|
+
itemVariable: 'cart',
|
|
41
|
+
},
|
|
42
|
+
executionEnv: 'server',
|
|
43
|
+
stepNumber: 2,
|
|
44
|
+
label: 'Loop Over Each Abandoned Cart',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: SHIM_ID,
|
|
48
|
+
type: 'general-custom-js',
|
|
49
|
+
config: { code: 'function customHandler(p){return {email:"a@b.c"}}', context: 'server' },
|
|
50
|
+
executionEnv: 'server',
|
|
51
|
+
stepNumber: 3,
|
|
52
|
+
label: 'Build Reminder Payload',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: CREATE_ID,
|
|
56
|
+
type: 'data-create-item',
|
|
57
|
+
config: { dataSourceId: 'ds-1', tableName: 'teleport_abandoned_carts', columnMappings: [] },
|
|
58
|
+
executionEnv: 'server',
|
|
59
|
+
stepNumber: 4,
|
|
60
|
+
label: 'Record Abandoned Cart',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: EMAIL_ID,
|
|
64
|
+
type: 'email-resend',
|
|
65
|
+
config: {
|
|
66
|
+
to: { type: 'workflowContext', nodeId: SHIM_ID, path: [SHIM_ID, 'email'] },
|
|
67
|
+
subject: 'You left something in your cart at {{companyName}}',
|
|
68
|
+
body: '<p>Hi {{customerName}}</p>',
|
|
69
|
+
templateParams: [
|
|
70
|
+
{ key: 'customerName', value: 'Jane' },
|
|
71
|
+
{ key: 'companyName', value: 'Acme' },
|
|
72
|
+
],
|
|
73
|
+
apiKey: '',
|
|
74
|
+
emailType: 'transactional',
|
|
75
|
+
},
|
|
76
|
+
executionEnv: 'server',
|
|
77
|
+
stepNumber: 5,
|
|
78
|
+
label: 'Send Abandoned Cart Reminder',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
edges: [
|
|
82
|
+
{ id: 'e1', source: TRIGGER_ID, target: SCAN_ID },
|
|
83
|
+
{ id: 'e2', source: SCAN_ID, target: LOOP_ID },
|
|
84
|
+
{
|
|
85
|
+
id: 'e3',
|
|
86
|
+
source: LOOP_ID,
|
|
87
|
+
target: SHIM_ID,
|
|
88
|
+
sourceHandle: 'loop',
|
|
89
|
+
targetHandle: 'loop-body-in',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'e4',
|
|
93
|
+
source: SHIM_ID,
|
|
94
|
+
target: CREATE_ID,
|
|
95
|
+
sourceHandle: 'loop-body-out',
|
|
96
|
+
targetHandle: 'loop-body-in',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'e5',
|
|
100
|
+
source: CREATE_ID,
|
|
101
|
+
target: EMAIL_ID,
|
|
102
|
+
sourceHandle: 'loop-body-out',
|
|
103
|
+
targetHandle: 'loop-body-in',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'e6',
|
|
107
|
+
source: EMAIL_ID,
|
|
108
|
+
target: LOOP_ID,
|
|
109
|
+
sourceHandle: 'loop-body-out',
|
|
110
|
+
targetHandle: 'loop-back',
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('generateCronAPIRoute — loop + templateParams support', () => {
|
|
116
|
+
const route = generateCronAPIRoute(buildCronWorkflow())
|
|
117
|
+
|
|
118
|
+
it('uses the loop-capable executor (handles general-loop body discovery)', () => {
|
|
119
|
+
// The shared execution loop keys the loop body off the 'loop' sourceHandle
|
|
120
|
+
// and walks 'loop-body-out' edges — markers absent from the old naive loop.
|
|
121
|
+
expect(route).toContain("e.sourceHandle === 'loop'")
|
|
122
|
+
expect(route).toContain('loop-body-out')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('merges templateParams into email body + subject', () => {
|
|
126
|
+
// Both the top-level and loop-body execution paths apply the merge; the
|
|
127
|
+
// email node lives in the loop body here.
|
|
128
|
+
expect(route).toContain('utils.applyTemplateParams(bResolved.body, bResolved.templateParams)')
|
|
129
|
+
expect(route).toContain(
|
|
130
|
+
'utils.applyTemplateParams(bResolved.subject, bResolved.templateParams)'
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('bakes the scan + loop + email nodes into the server WORKFLOW_CONFIG', () => {
|
|
135
|
+
expect(route).toContain('"type": "general-loop"')
|
|
136
|
+
expect(route).toContain('"type": "email-resend"')
|
|
137
|
+
expect(route).toContain('"type": "data-raw-query"')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('emits a self-contained cron handler with the schedule + success response', () => {
|
|
141
|
+
expect(route).toContain("schedule: '0 * * * *'")
|
|
142
|
+
expect(route).toContain('res.status(200).json({ success: true')
|
|
143
|
+
})
|
|
144
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { generateDataAPIRoute } from '../src/data-api-route-generator'
|
|
2
|
+
import { dataCreateItem } from '../src/nodes/data/data-create-item'
|
|
3
|
+
|
|
4
|
+
// Idempotent insert support. The "Resolve Current User" custom node re-ensures
|
|
5
|
+
// a guest's anonymous `users` row on EVERY resolution so the identity is
|
|
6
|
+
// self-healing after a database reset / re-provision. That insert targets a
|
|
7
|
+
// row that usually already exists, so it MUST be a no-op on conflict rather
|
|
8
|
+
// than a 23505 error that aborts the resolve-user (and therefore checkout).
|
|
9
|
+
|
|
10
|
+
describe('data-api handleCreate — ON CONFLICT DO NOTHING', () => {
|
|
11
|
+
const code = generateDataAPIRoute()
|
|
12
|
+
|
|
13
|
+
it('appends ON CONFLICT DO NOTHING only when the caller opts in', () => {
|
|
14
|
+
expect(code).toContain('body.onConflictDoNothing')
|
|
15
|
+
expect(code).toContain('ON CONFLICT DO NOTHING')
|
|
16
|
+
// Default path stays a plain insert — the clause is gated behind the flag.
|
|
17
|
+
expect(code).toMatch(/onConflictClause\s*=\s*body\.onConflictDoNothing\s*\?/)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('falls back to the supplied id when a conflict returns no row', () => {
|
|
21
|
+
// With ON CONFLICT DO NOTHING a pre-existing row yields no RETURNING row,
|
|
22
|
+
// so the response must still surface the intended id for downstream nodes.
|
|
23
|
+
expect(code).toContain("var idIdx = columns.indexOf('id')")
|
|
24
|
+
expect(code).toMatch(/resolvedId\s*=\s*item\s*\?/)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('data_create_item — forwards the idempotency flag', () => {
|
|
29
|
+
const handlerCode = dataCreateItem.generateHandler()
|
|
30
|
+
|
|
31
|
+
it('passes config.onConflictDoNothing through to the data-api request body', () => {
|
|
32
|
+
expect(handlerCode).toContain('config.onConflictDoNothing === true')
|
|
33
|
+
expect(handlerCode).toContain('reqBody.onConflictDoNothing = true')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -19,6 +19,8 @@ import { generateSharedRuntimeUtilsCode } from '../src'
|
|
|
19
19
|
type SharedUtils = {
|
|
20
20
|
resolveConfig: (cfg: unknown, ctx: Record<string, unknown>) => any
|
|
21
21
|
resolveRichTextContext: (html: string, ctx: Record<string, unknown>) => string
|
|
22
|
+
applyTemplateParams: (text: string, params: unknown) => string
|
|
23
|
+
expandListBlocks: (text: string, lists: Record<string, unknown>) => string
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function loadSharedRuntime(): SharedUtils {
|
|
@@ -121,3 +123,127 @@ describe('email body inline-context resolution', () => {
|
|
|
121
123
|
expect(out).not.toContain('Reset URL</span>')
|
|
122
124
|
})
|
|
123
125
|
})
|
|
126
|
+
|
|
127
|
+
// Guards the component-body path: a send-email node whose body is a serialized
|
|
128
|
+
// email-template component carries flat {{token}} merge fields filled at runtime
|
|
129
|
+
// from templateParams (resolveConfig resolves each param's context ref first,
|
|
130
|
+
// then executeNodes calls applyTemplateParams on body + subject).
|
|
131
|
+
describe('email template params substitution', () => {
|
|
132
|
+
const utils = loadSharedRuntime()
|
|
133
|
+
|
|
134
|
+
it('exposes applyTemplateParams from the generated runtime', () => {
|
|
135
|
+
expect(typeof utils.applyTemplateParams).toBe('function')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('fills {{token}} in body + subject from resolved templateParams (executeNodes flow)', () => {
|
|
139
|
+
const config = {
|
|
140
|
+
subject: 'Reset {{recipientEmail}}',
|
|
141
|
+
body: '<a href="{{resetUrl}}">Reset</a> for {{recipientEmail}}',
|
|
142
|
+
templateParams: [
|
|
143
|
+
{
|
|
144
|
+
key: 'resetUrl',
|
|
145
|
+
value: { type: 'workflowContext', nodeId: 'N1', path: ['N1', 'result'] },
|
|
146
|
+
},
|
|
147
|
+
{ key: 'recipientEmail', value: 'jane@example.com' },
|
|
148
|
+
],
|
|
149
|
+
}
|
|
150
|
+
// Step 1: resolveConfig resolves each param's context ref (as executeNodes does).
|
|
151
|
+
const resolved = utils.resolveConfig(config, {
|
|
152
|
+
N1: { result: 'https://app.example.com/reset-password?token=abc' },
|
|
153
|
+
})
|
|
154
|
+
// Step 2: executeNodes applies the params to body + subject.
|
|
155
|
+
const body = utils.applyTemplateParams(resolved.body, resolved.templateParams)
|
|
156
|
+
const subject = utils.applyTemplateParams(resolved.subject, resolved.templateParams)
|
|
157
|
+
expect(body).toBe(
|
|
158
|
+
'<a href="https://app.example.com/reset-password?token=abc">Reset</a> for jane@example.com'
|
|
159
|
+
)
|
|
160
|
+
expect(subject).toBe('Reset jane@example.com')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('leaves unknown tokens verbatim (never blanks a campaign {{name}})', () => {
|
|
164
|
+
const out = utils.applyTemplateParams('Hi {{name}}, reset: {{resetUrl}}', [
|
|
165
|
+
{ key: 'resetUrl', value: 'https://x/reset?token=1' },
|
|
166
|
+
])
|
|
167
|
+
expect(out).toBe('Hi {{name}}, reset: https://x/reset?token=1')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('does not partial-match longer tokens ({{total}} vs {{totalAmount}})', () => {
|
|
171
|
+
const out = utils.applyTemplateParams('{{total}} of {{totalAmount}}', [
|
|
172
|
+
{ key: 'total', value: '5' },
|
|
173
|
+
{ key: 'totalAmount', value: '$99' },
|
|
174
|
+
])
|
|
175
|
+
expect(out).toBe('5 of $99')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('is a no-op when there are no params or body is not a string', () => {
|
|
179
|
+
expect(utils.applyTemplateParams('{{x}}', [])).toBe('{{x}}')
|
|
180
|
+
expect(utils.applyTemplateParams('{{x}}', undefined)).toBe('{{x}}')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// ── Array-mapper loop-block expansion (builder array mapper → email list) ──
|
|
184
|
+
|
|
185
|
+
it('expands a <!--tq:each--> block from an array templateParam, once per row', () => {
|
|
186
|
+
const body =
|
|
187
|
+
'Items:<!--tq:each products--><li>{{quantity}}x <strong>{{product_name}}</strong> — {{companyName}}</li><!--/tq:each-->end'
|
|
188
|
+
const out = utils.applyTemplateParams(body, [
|
|
189
|
+
{
|
|
190
|
+
key: 'products',
|
|
191
|
+
value: [
|
|
192
|
+
{ product_name: 'Espresso Beans', quantity: 2 },
|
|
193
|
+
{ product_name: 'Ceramic Mug', quantity: 1 },
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
{ key: 'companyName', value: 'Acme Inc.' },
|
|
197
|
+
])
|
|
198
|
+
// one <li> per row, row tokens filled from the row object
|
|
199
|
+
expect(out).toContain('<li>2x <strong>Espresso Beans</strong> — Acme Inc.</li>')
|
|
200
|
+
expect(out).toContain('<li>1x <strong>Ceramic Mug</strong> — Acme Inc.</li>')
|
|
201
|
+
// the {{companyName}} inside the row (not a row field) resolved via the flat fill
|
|
202
|
+
expect(out).not.toContain('{{companyName}}')
|
|
203
|
+
// no leftover sentinels or tokens
|
|
204
|
+
expect(out).not.toContain('<!--tq:each')
|
|
205
|
+
expect(out).not.toContain('<!--/tq:each-->')
|
|
206
|
+
expect(out).not.toContain('{{product_name}}')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('HTML-escapes per-row values (no markup injection from item data)', () => {
|
|
210
|
+
const out = utils.expandListBlocks('<!--tq:each products-->[{{name}}]<!--/tq:each-->', {
|
|
211
|
+
products: [{ name: '<b>x</b> & "y"' }],
|
|
212
|
+
})
|
|
213
|
+
expect(out).toBe('[<b>x</b> & "y"]')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('renders an empty string for a missing/non-array list key', () => {
|
|
217
|
+
const body = 'a<!--tq:each products-->{{name}}<!--/tq:each-->b'
|
|
218
|
+
expect(utils.applyTemplateParams(body, [])).toBe('ab')
|
|
219
|
+
expect(utils.applyTemplateParams(body, [{ key: 'products', value: 'not-an-array' }])).toBe('ab')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('resolveConfig preserves an array-valued templateParam so the block expands', () => {
|
|
223
|
+
// Mirrors the withdrawal send node: templateParams is an array of {key,value}
|
|
224
|
+
// where value is a workflowContext ref that resolves to the items array.
|
|
225
|
+
const config = {
|
|
226
|
+
body: 'X<!--tq:each products--><li>{{quantity}}x {{product_name}}</li><!--/tq:each-->Y',
|
|
227
|
+
templateParams: [
|
|
228
|
+
{
|
|
229
|
+
key: 'products',
|
|
230
|
+
value: {
|
|
231
|
+
type: 'workflowContext',
|
|
232
|
+
nodeId: 'assemble',
|
|
233
|
+
path: ['assemble', 'selectedItems'],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{ key: 'companyName', value: 'Acme Inc.' },
|
|
237
|
+
],
|
|
238
|
+
}
|
|
239
|
+
const ctx = {
|
|
240
|
+
assemble: { selectedItems: [{ product_name: 'Espresso Beans', quantity: 3 }] },
|
|
241
|
+
}
|
|
242
|
+
const resolved = utils.resolveConfig(config, ctx)
|
|
243
|
+
// the array survived resolution (was not String()-joined)
|
|
244
|
+
expect(Array.isArray(resolved.templateParams[0].value)).toBe(true)
|
|
245
|
+
const out = utils.applyTemplateParams(resolved.body, resolved.templateParams)
|
|
246
|
+
expect(out).toContain('<li>3x Espresso Beans</li>')
|
|
247
|
+
expect(out).not.toContain('<!--tq:each')
|
|
248
|
+
})
|
|
249
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parameterizeRawSqlInterpolations,
|
|
3
|
+
parameterizeWorkflowRawSqlInterpolations,
|
|
4
|
+
parameterizeAllWorkflowRawSql,
|
|
5
|
+
WorkflowSqlInterpolationError,
|
|
6
|
+
} from '../src/raw-sql-param-binding'
|
|
7
|
+
|
|
8
|
+
// Generation-time net (runs in the workflows plugin's runBefore). Converts each
|
|
9
|
+
// `{{ name }}` value token in a data node's raw SQL into a positional `$N` +
|
|
10
|
+
// a bound params entry, keeping the published-app runtime on the standard
|
|
11
|
+
// `$N` + params path. PAIRED with the GUI + worker copies.
|
|
12
|
+
|
|
13
|
+
function ref(nodeId: string, ...path: string[]) {
|
|
14
|
+
return { type: 'workflowContext' as const, nodeId, path: [nodeId, ...path] }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('parameterizeRawSqlInterpolations', () => {
|
|
18
|
+
it('binds a context-ref variable token in value position', () => {
|
|
19
|
+
const value = ref('normalize', 'ids')
|
|
20
|
+
const res = parameterizeRawSqlInterpolations(
|
|
21
|
+
'DELETE FROM t WHERE id::text = ANY({{ selectedIds }}::text[])',
|
|
22
|
+
[{ name: 'selectedIds', value }]
|
|
23
|
+
)
|
|
24
|
+
expect(res.query).toBe('DELETE FROM t WHERE id::text = ANY($1::text[])')
|
|
25
|
+
expect(res.params).toEqual([value])
|
|
26
|
+
expect(res.residuals).toEqual([])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('keeps an unmatched {{state.X}} token as its own string param', () => {
|
|
30
|
+
const res = parameterizeRawSqlInterpolations(
|
|
31
|
+
'SELECT * FROM t WHERE owner = {{state.userId}}',
|
|
32
|
+
[]
|
|
33
|
+
)
|
|
34
|
+
expect(res.query).toBe('SELECT * FROM t WHERE owner = $1')
|
|
35
|
+
expect(res.params).toEqual(['{{state.userId}}'])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('reports an identifier-position token as a residual', () => {
|
|
39
|
+
const res = parameterizeRawSqlInterpolations('SELECT * FROM {{ t }}', [
|
|
40
|
+
{ name: 't', value: ref('n', 't') },
|
|
41
|
+
])
|
|
42
|
+
expect(res.residuals).toEqual(['{{ t }}'])
|
|
43
|
+
expect(res.params).toEqual([])
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('parameterizeWorkflowRawSqlInterpolations', () => {
|
|
48
|
+
it('rewrites a data-raw-query node and drops queryVariables', () => {
|
|
49
|
+
const value = ref('n', 'ids')
|
|
50
|
+
const workflow = {
|
|
51
|
+
nodes: [
|
|
52
|
+
{
|
|
53
|
+
id: 'q',
|
|
54
|
+
type: 'data-raw-query',
|
|
55
|
+
config: {
|
|
56
|
+
query: 'DELETE FROM t WHERE id = ANY({{ ids }}::text[])',
|
|
57
|
+
queryVariables: [{ name: 'ids', value }],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
parameterizeWorkflowRawSqlInterpolations(workflow)
|
|
63
|
+
expect(workflow.nodes[0].config).toEqual({
|
|
64
|
+
query: 'DELETE FROM t WHERE id = ANY($1::text[])',
|
|
65
|
+
params: [value],
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('rewrites a data-select rawQueryUserPart', () => {
|
|
70
|
+
const value = ref('n', 'v')
|
|
71
|
+
const workflow = {
|
|
72
|
+
nodes: [
|
|
73
|
+
{
|
|
74
|
+
id: 's',
|
|
75
|
+
type: 'data-select',
|
|
76
|
+
config: {
|
|
77
|
+
rawQueryUserPart: 'owner_id = {{ v }}',
|
|
78
|
+
rawQueryUserPartVariables: [{ name: 'v', value }],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
parameterizeWorkflowRawSqlInterpolations(workflow)
|
|
84
|
+
expect(workflow.nodes[0].config).toEqual({
|
|
85
|
+
rawQueryUserPart: 'owner_id = $1',
|
|
86
|
+
rawQueryUserPartParams: [value],
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('throws on an unbindable token and mutates nothing', () => {
|
|
91
|
+
const workflow = {
|
|
92
|
+
nodes: [
|
|
93
|
+
{
|
|
94
|
+
id: 'bad',
|
|
95
|
+
type: 'data-raw-query',
|
|
96
|
+
config: {
|
|
97
|
+
query: 'SELECT * FROM {{ table }}',
|
|
98
|
+
queryVariables: [{ name: 'table', value: ref('n', 't') }],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
}
|
|
103
|
+
expect(() => parameterizeWorkflowRawSqlInterpolations(workflow)).toThrow(
|
|
104
|
+
WorkflowSqlInterpolationError
|
|
105
|
+
)
|
|
106
|
+
expect((workflow.nodes[0].config as { query: string }).query).toContain('{{ table }}')
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('parameterizeAllWorkflowRawSql', () => {
|
|
111
|
+
it('walks both workflows and customNodes (id-keyed records)', () => {
|
|
112
|
+
const value = ref('n', 'ids')
|
|
113
|
+
const uidlWorkflows = {
|
|
114
|
+
workflows: {
|
|
115
|
+
wf1: {
|
|
116
|
+
nodes: {
|
|
117
|
+
n1: {
|
|
118
|
+
type: 'data-raw-query',
|
|
119
|
+
config: {
|
|
120
|
+
query: 'DELETE FROM t WHERE id = ANY({{ ids }}::text[])',
|
|
121
|
+
queryVariables: [{ name: 'ids', value }],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
customNodes: {},
|
|
128
|
+
}
|
|
129
|
+
parameterizeAllWorkflowRawSql(uidlWorkflows)
|
|
130
|
+
const cfg = uidlWorkflows.workflows.wf1.nodes.n1.config as { query: string; params: unknown[] }
|
|
131
|
+
expect(cfg.query).toBe('DELETE FROM t WHERE id = ANY($1::text[])')
|
|
132
|
+
expect(cfg.params).toEqual([value])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('is a no-op when workflows is undefined', () => {
|
|
136
|
+
expect(() => parameterizeAllWorkflowRawSql(undefined)).not.toThrow()
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AccountDeleteRouteOptions {
|
|
2
|
+
authUsersTableName?: string;
|
|
3
|
+
emailProvider?: string | null;
|
|
4
|
+
fromEmail?: string;
|
|
5
|
+
emailSecretEnvName?: string | null;
|
|
6
|
+
emailSubject?: string;
|
|
7
|
+
emailBodyHtml?: string;
|
|
8
|
+
siteName?: string;
|
|
9
|
+
deletedEmailPattern?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const accountDeleteRouteDependencies: (provider?: string) => Record<string, string>;
|
|
12
|
+
export declare const generateAccountDeleteRoute: (options?: AccountDeleteRouteOptions) => string;
|
|
13
|
+
//# sourceMappingURL=account-delete-route-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-delete-route-generator.d.ts","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,yBAAyB;IAExC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAI3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB,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,MA6NpF,CAAA"}
|