@teleporthq/teleport-plugin-next-workflows 0.43.64 → 0.43.66
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-clear-db-mirror.test.ts +12 -0
- package/__tests__/cart-get-total-regional-pricing.test.ts +166 -0
- package/__tests__/continue-on-error.test.ts +119 -0
- package/__tests__/custom-node-page-route-rewrite.test.ts +109 -0
- package/__tests__/invoice-delivery-fee.test.ts +104 -0
- package/__tests__/invoice-regional-tax.test.ts +126 -0
- package/__tests__/payment-redirect-and-order-notification.test.ts +6 -0
- package/__tests__/paypal-webhook-order-notification.test.ts +41 -0
- package/__tests__/resolve-handler-entry-name.test.ts +25 -0
- package/__tests__/sent-email-log-module.test.ts +381 -0
- package/__tests__/sent-email-log-wiring.test.ts +267 -0
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +14 -5
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +44 -9
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +15 -6
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +17 -4
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/cjs/invoice/api-routes-code.js +42 -4
- package/dist/cjs/invoice/api-routes-code.js.map +1 -1
- package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/cjs/invoice/email-sender-code.js +58 -29
- package/dist/cjs/invoice/email-sender-code.js.map +1 -1
- package/dist/cjs/invoice/index.d.ts +1 -4
- package/dist/cjs/invoice/index.d.ts.map +1 -1
- package/dist/cjs/invoice/index.js +5 -19
- package/dist/cjs/invoice/index.js.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.js +31 -8
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/invoice/regional-invoice-tax-code.d.ts +25 -0
- package/dist/cjs/invoice/regional-invoice-tax-code.d.ts.map +1 -0
- package/dist/cjs/invoice/regional-invoice-tax-code.js +116 -0
- package/dist/cjs/invoice/regional-invoice-tax-code.js.map +1 -0
- package/dist/cjs/invoice/resolve-data-source.d.ts +6 -0
- package/dist/cjs/invoice/resolve-data-source.d.ts.map +1 -0
- package/dist/cjs/invoice/resolve-data-source.js +23 -0
- package/dist/cjs/invoice/resolve-data-source.js.map +1 -0
- package/dist/cjs/nodes/analytics/commerce-tracking.d.ts +38 -2
- package/dist/cjs/nodes/analytics/commerce-tracking.d.ts.map +1 -1
- package/dist/cjs/nodes/analytics/commerce-tracking.js +41 -26
- package/dist/cjs/nodes/analytics/commerce-tracking.js.map +1 -1
- package/dist/cjs/nodes/cart/cart-add-item.d.ts.map +1 -1
- package/dist/cjs/nodes/cart/cart-add-item.js +3 -1
- package/dist/cjs/nodes/cart/cart-add-item.js.map +1 -1
- package/dist/cjs/nodes/cart/cart-clear.d.ts.map +1 -1
- package/dist/cjs/nodes/cart/cart-clear.js +6 -0
- package/dist/cjs/nodes/cart/cart-clear.js.map +1 -1
- package/dist/cjs/nodes/cart/cart-get-total.d.ts.map +1 -1
- package/dist/cjs/nodes/cart/cart-get-total.js +81 -5
- package/dist/cjs/nodes/cart/cart-get-total.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 +8 -0
- package/dist/cjs/nodes/data/data-create-item.js.map +1 -1
- package/dist/cjs/nodes/types.d.ts.map +1 -1
- package/dist/cjs/nodes/types.js +19 -0
- package/dist/cjs/nodes/types.js.map +1 -1
- package/dist/cjs/sent-email-log/index.d.ts +16 -0
- package/dist/cjs/sent-email-log/index.d.ts.map +1 -0
- package/dist/cjs/sent-email-log/index.js +53 -0
- package/dist/cjs/sent-email-log/index.js.map +1 -0
- package/dist/cjs/sent-email-log/sent-email-log-code.d.ts +4 -0
- package/dist/cjs/sent-email-log/sent-email-log-code.d.ts.map +1 -0
- package/dist/cjs/sent-email-log/sent-email-log-code.js +415 -0
- package/dist/cjs/sent-email-log/sent-email-log-code.js.map +1 -0
- package/dist/cjs/sent-email-log/sent-email-log-scope.d.ts +21 -0
- package/dist/cjs/sent-email-log/sent-email-log-scope.d.ts.map +1 -0
- package/dist/cjs/sent-email-log/sent-email-log-scope.js +36 -0
- package/dist/cjs/sent-email-log/sent-email-log-scope.js.map +1 -0
- package/dist/cjs/transactional-email-code.d.ts +12 -1
- package/dist/cjs/transactional-email-code.d.ts.map +1 -1
- package/dist/cjs/transactional-email-code.js +43 -5
- package/dist/cjs/transactional-email-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/webhook-generator.d.ts.map +1 -1
- package/dist/cjs/webhook-generator.js +9 -2
- package/dist/cjs/webhook-generator.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +53 -30
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +15 -6
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +44 -9
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +15 -6
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +17 -4
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/esm/invoice/api-routes-code.js +42 -4
- package/dist/esm/invoice/api-routes-code.js.map +1 -1
- package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/esm/invoice/email-sender-code.js +58 -29
- package/dist/esm/invoice/email-sender-code.js.map +1 -1
- package/dist/esm/invoice/index.d.ts +1 -4
- package/dist/esm/invoice/index.d.ts.map +1 -1
- package/dist/esm/invoice/index.js +4 -18
- package/dist/esm/invoice/index.js.map +1 -1
- package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-html-code.js +31 -8
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/invoice/regional-invoice-tax-code.d.ts +25 -0
- package/dist/esm/invoice/regional-invoice-tax-code.d.ts.map +1 -0
- package/dist/esm/invoice/regional-invoice-tax-code.js +113 -0
- package/dist/esm/invoice/regional-invoice-tax-code.js.map +1 -0
- package/dist/esm/invoice/resolve-data-source.d.ts +6 -0
- package/dist/esm/invoice/resolve-data-source.d.ts.map +1 -0
- package/dist/esm/invoice/resolve-data-source.js +19 -0
- package/dist/esm/invoice/resolve-data-source.js.map +1 -0
- package/dist/esm/nodes/analytics/commerce-tracking.d.ts +38 -2
- package/dist/esm/nodes/analytics/commerce-tracking.d.ts.map +1 -1
- package/dist/esm/nodes/analytics/commerce-tracking.js +41 -26
- package/dist/esm/nodes/analytics/commerce-tracking.js.map +1 -1
- package/dist/esm/nodes/cart/cart-add-item.d.ts.map +1 -1
- package/dist/esm/nodes/cart/cart-add-item.js +3 -1
- package/dist/esm/nodes/cart/cart-add-item.js.map +1 -1
- package/dist/esm/nodes/cart/cart-clear.d.ts.map +1 -1
- package/dist/esm/nodes/cart/cart-clear.js +6 -0
- package/dist/esm/nodes/cart/cart-clear.js.map +1 -1
- package/dist/esm/nodes/cart/cart-get-total.d.ts.map +1 -1
- package/dist/esm/nodes/cart/cart-get-total.js +81 -5
- package/dist/esm/nodes/cart/cart-get-total.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 +8 -0
- package/dist/esm/nodes/data/data-create-item.js.map +1 -1
- package/dist/esm/nodes/types.d.ts.map +1 -1
- package/dist/esm/nodes/types.js +19 -0
- package/dist/esm/nodes/types.js.map +1 -1
- package/dist/esm/sent-email-log/index.d.ts +16 -0
- package/dist/esm/sent-email-log/index.d.ts.map +1 -0
- package/dist/esm/sent-email-log/index.js +45 -0
- package/dist/esm/sent-email-log/index.js.map +1 -0
- package/dist/esm/sent-email-log/sent-email-log-code.d.ts +4 -0
- package/dist/esm/sent-email-log/sent-email-log-code.d.ts.map +1 -0
- package/dist/esm/sent-email-log/sent-email-log-code.js +411 -0
- package/dist/esm/sent-email-log/sent-email-log-code.js.map +1 -0
- package/dist/esm/sent-email-log/sent-email-log-scope.d.ts +21 -0
- package/dist/esm/sent-email-log/sent-email-log-scope.d.ts.map +1 -0
- package/dist/esm/sent-email-log/sent-email-log-scope.js +31 -0
- package/dist/esm/sent-email-log/sent-email-log-scope.js.map +1 -0
- package/dist/esm/transactional-email-code.d.ts +12 -1
- package/dist/esm/transactional-email-code.d.ts.map +1 -1
- package/dist/esm/transactional-email-code.js +42 -4
- package/dist/esm/transactional-email-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/webhook-generator.d.ts.map +1 -1
- package/dist/esm/webhook-generator.js +9 -2
- package/dist/esm/webhook-generator.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +53 -30
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +4 -4
- package/src/account-delete-route-generator.ts +16 -5
- package/src/api-route-generator.ts +51 -9
- package/src/auth-generator.ts +17 -6
- package/src/executor-generator.ts +17 -4
- package/src/index.ts +1 -0
- package/src/invoice/api-routes-code.ts +48 -4
- package/src/invoice/email-sender-code.ts +58 -29
- package/src/invoice/index.ts +4 -23
- package/src/invoice/invoice-html-code.ts +31 -8
- package/src/invoice/regional-invoice-tax-code.ts +112 -0
- package/src/invoice/resolve-data-source.ts +25 -0
- package/src/nodes/analytics/commerce-tracking.ts +37 -21
- package/src/nodes/cart/cart-add-item.ts +3 -1
- package/src/nodes/cart/cart-clear.ts +6 -0
- package/src/nodes/cart/cart-get-total.ts +94 -5
- package/src/nodes/data/data-create-item.ts +8 -0
- package/src/nodes/types.ts +23 -0
- package/src/sent-email-log/index.ts +57 -0
- package/src/sent-email-log/sent-email-log-code.ts +419 -0
- package/src/sent-email-log/sent-email-log-scope.ts +36 -0
- package/src/transactional-email-code.ts +61 -4
- package/src/webhook-generator.ts +11 -2
- package/src/workflow-project-plugin.ts +52 -29
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { generatePaypalWebhookCode } from '../src/webhook-generator'
|
|
2
|
+
import type { UIDLEcommerceSettings, UIDLInvoiceSettings } from '@teleporthq/teleport-types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The PayPal webhook tells the order-notification route which order was paid.
|
|
6
|
+
* The capture resource's own id is a PayPal id — it matches no `teleport_orders`
|
|
7
|
+
* row, so a notification keyed on it reached the merchant with no lines at all.
|
|
8
|
+
* The internal id travels in `custom_id`, and the helper that reads it must be
|
|
9
|
+
* emitted whenever notifications are on, not only when invoices are.
|
|
10
|
+
*/
|
|
11
|
+
const ecommerce = (orderNotifications: boolean): UIDLEcommerceSettings =>
|
|
12
|
+
({ orderNotifications } as unknown as UIDLEcommerceSettings)
|
|
13
|
+
|
|
14
|
+
const invoices = (enabled: boolean): UIDLInvoiceSettings =>
|
|
15
|
+
({ enabled, autoGenerateOnPayment: enabled } as unknown as UIDLInvoiceSettings)
|
|
16
|
+
|
|
17
|
+
describe('PayPal webhook — order notification order id', () => {
|
|
18
|
+
it('sends the internal order id, falling back to the capture id', () => {
|
|
19
|
+
const source = generatePaypalWebhookCode(invoices(false), ecommerce(true))
|
|
20
|
+
expect(source).toContain("orderId: __extractInternalOrderId(resource) || resource.id || ''")
|
|
21
|
+
expect(source).toContain('function __extractInternalOrderId(resource)')
|
|
22
|
+
// Invoices are off: their handler is not emitted, only the id helper both share.
|
|
23
|
+
expect(source).not.toContain('async function handlePaypalInvoiceGeneration')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('emits the helper for invoices alone too, and both handlers when both are on', () => {
|
|
27
|
+
const invoicesOnly = generatePaypalWebhookCode(invoices(true), ecommerce(false))
|
|
28
|
+
expect(invoicesOnly).toContain('function __extractInternalOrderId(resource)')
|
|
29
|
+
expect(invoicesOnly).toContain('async function handlePaypalInvoiceGeneration')
|
|
30
|
+
expect(invoicesOnly).not.toContain('/api/ecommerce/order-notification')
|
|
31
|
+
|
|
32
|
+
const both = generatePaypalWebhookCode(invoices(true), ecommerce(true))
|
|
33
|
+
expect(both.match(/function __extractInternalOrderId\(resource\)/g)).toHaveLength(1)
|
|
34
|
+
expect(both).toContain('/api/ecommerce/order-notification')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('emits neither when neither feature is on', () => {
|
|
38
|
+
const source = generatePaypalWebhookCode(invoices(false), ecommerce(false))
|
|
39
|
+
expect(source).not.toContain('__extractInternalOrderId')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -80,6 +80,31 @@ async function zzz(config, context) { return one_param_helper(config); }
|
|
|
80
80
|
)
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The shipped break this guards: a helper read back through
|
|
85
|
+
* `helper.toString()` had that read as its ONLY reference, so a consumer's
|
|
86
|
+
* minifier inlined the declaration and dropped the name. The concatenated
|
|
87
|
+
* handler then carried `function(e){...}` in STATEMENT position, and
|
|
88
|
+
* `next build` of the generated project failed with SWC's "Expected ident"
|
|
89
|
+
* in utils/workflows/node-handlers-client.js — visible only in a minified
|
|
90
|
+
* build, so nothing reading the unminified source could catch it.
|
|
91
|
+
*/
|
|
92
|
+
it('rejects a helper that a minifier left anonymous in statement position', () => {
|
|
93
|
+
const source = `async function cart_add_item(config) { trackCommerceStep({ name: 'add_to_cart' }); }
|
|
94
|
+
function(e){try{const t=globalThis.tpTrackCommerce;"function"==typeof t&&t(e.name,e.detail)}catch(e){}}`
|
|
95
|
+
expect(() => resolveHandlerEntryName(source, 'cart-add-item')).toThrow(
|
|
96
|
+
/anonymous function where a statement is expected/
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('accepts an anonymous function that is an expression, not a statement', () => {
|
|
101
|
+
// `wrapWithGuard`'s re-declaration guard and any inline callback keep the
|
|
102
|
+
// `function` keyword off the start of a line — those are valid and common.
|
|
103
|
+
const source = `var util = typeof util !== 'undefined' ? util : function (val) { return val; };
|
|
104
|
+
async function my_node_type(config, context) { return [config].map(function (c) { return c; }); }`
|
|
105
|
+
expect(resolveHandlerEntryName(source, 'my-node-type')).toBe('my_node_type')
|
|
106
|
+
})
|
|
107
|
+
|
|
83
108
|
// Full-registry regression: every node type's generateHandler()/
|
|
84
109
|
// generateServerHandler() output must resolve to a real, callable function
|
|
85
110
|
// both normally AND after its entry function's declared name is renamed
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// The sent-email ledger module is emitted as a runtime string; these tests
|
|
2
|
+
// EXECUTE it against a stubbed `pg` so the row it writes, its allow-list, its
|
|
3
|
+
// disable rules and its drain contract are proven on the code that ships.
|
|
4
|
+
import { generateSentEmailLogCode } from '../src/sent-email-log/sent-email-log-code'
|
|
5
|
+
|
|
6
|
+
interface StubQuery {
|
|
7
|
+
sql: string
|
|
8
|
+
values: unknown[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const loadModule = (options: {
|
|
12
|
+
queryImpl?: (sql: string, values: unknown[]) => Promise<unknown>
|
|
13
|
+
pgAvailable?: boolean
|
|
14
|
+
env?: Record<string, string>
|
|
15
|
+
}) => {
|
|
16
|
+
const queries: StubQuery[] = []
|
|
17
|
+
const connects: number[] = []
|
|
18
|
+
const ends: number[] = []
|
|
19
|
+
const queryImpl = options.queryImpl || (async () => ({ rowCount: 1 }))
|
|
20
|
+
|
|
21
|
+
class Client {
|
|
22
|
+
async connect() {
|
|
23
|
+
connects.push(1)
|
|
24
|
+
}
|
|
25
|
+
async query(sql: string, values: unknown[]) {
|
|
26
|
+
queries.push({ sql, values })
|
|
27
|
+
return queryImpl(sql, values)
|
|
28
|
+
}
|
|
29
|
+
async end() {
|
|
30
|
+
ends.push(1)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const requireShim = (id: string) => {
|
|
35
|
+
if (id === 'pg') {
|
|
36
|
+
if (options.pgAvailable === false) {
|
|
37
|
+
throw new Error("Cannot find module 'pg'")
|
|
38
|
+
}
|
|
39
|
+
return { Client }
|
|
40
|
+
}
|
|
41
|
+
if (id === 'crypto') {
|
|
42
|
+
return require('crypto')
|
|
43
|
+
}
|
|
44
|
+
throw new Error('unexpected require: ' + id)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const savedEnv = { ...process.env }
|
|
48
|
+
process.env.TELEPORT_DB_CONNECTION_STRING = 'postgresql://u:p@localhost:5432/db'
|
|
49
|
+
Object.assign(process.env, options.env || {})
|
|
50
|
+
|
|
51
|
+
const module = { exports: {} as any }
|
|
52
|
+
const code = generateSentEmailLogCode({ pgSupported: true })
|
|
53
|
+
// eslint-disable-next-line no-new-func
|
|
54
|
+
new Function('module', 'exports', 'require', 'process', 'console', code)(
|
|
55
|
+
module,
|
|
56
|
+
module.exports,
|
|
57
|
+
requireShim,
|
|
58
|
+
process,
|
|
59
|
+
silentConsole
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const restore = () => {
|
|
63
|
+
for (const key of Object.keys(process.env)) {
|
|
64
|
+
if (!(key in savedEnv)) {
|
|
65
|
+
delete process.env[key]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
Object.assign(process.env, savedEnv)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { api: module.exports, queries, connects, ends, restore }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const noop = (): void => undefined
|
|
75
|
+
const silentConsole = { warn: noop, error: noop, log: noop, info: noop }
|
|
76
|
+
|
|
77
|
+
const rowOf = (query: StubQuery): Record<string, unknown> => {
|
|
78
|
+
const columns = /\(([^)]+)\) VALUES/
|
|
79
|
+
.exec(query.sql)![1]
|
|
80
|
+
.split(',')
|
|
81
|
+
.map((c) => c.trim().replace(/"/g, ''))
|
|
82
|
+
const row: Record<string, unknown> = {}
|
|
83
|
+
columns.forEach((column, index) => {
|
|
84
|
+
row[column] = query.values[index]
|
|
85
|
+
})
|
|
86
|
+
return row
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('sent-email-log runtime module', () => {
|
|
90
|
+
it('writes one parameterized row per attempt, with a fresh id and the allow-listed fields only', async () => {
|
|
91
|
+
const { api, queries, connects, ends, restore } = loadModule({})
|
|
92
|
+
try {
|
|
93
|
+
const landed = await api.recordSentEmail({
|
|
94
|
+
emailType: 'order-shipped',
|
|
95
|
+
audience: 'customer',
|
|
96
|
+
to: 'jane@example.com',
|
|
97
|
+
cc: 'ops@example.com, boss@example.com',
|
|
98
|
+
from: 'Acme Store <shop@acme.com>',
|
|
99
|
+
subject: 'Your parcel is on its way',
|
|
100
|
+
html: '<p>Hi Jane</p>',
|
|
101
|
+
payload: [
|
|
102
|
+
{ key: 'customerName', value: 'Jane' },
|
|
103
|
+
{ key: 'items', value: [{ name: 'Mug', qty: 2 }] },
|
|
104
|
+
],
|
|
105
|
+
provider: 'resend',
|
|
106
|
+
providerMessageId: 'msg_1',
|
|
107
|
+
status: 'sent',
|
|
108
|
+
source: 'workflow',
|
|
109
|
+
sourceRef: 'email-resend',
|
|
110
|
+
orderId: '5f2b1a1e-8d7f-4c2e-9a1b-2c3d4e5f6a7b',
|
|
111
|
+
userId: 'not-a-uuid',
|
|
112
|
+
apiKey: 'sk_live_secret',
|
|
113
|
+
})
|
|
114
|
+
expect(landed).toBe(true)
|
|
115
|
+
expect(queries).toHaveLength(1)
|
|
116
|
+
expect(connects).toHaveLength(1)
|
|
117
|
+
expect(ends).toHaveLength(1)
|
|
118
|
+
expect(queries[0].sql).toMatch(/^INSERT INTO "teleport_sent_emails" \(/)
|
|
119
|
+
expect(queries[0].sql).not.toContain('sk_live_secret')
|
|
120
|
+
const row = rowOf(queries[0])
|
|
121
|
+
expect(row.id).toMatch(/^[0-9a-f-]{36}$/)
|
|
122
|
+
expect(row.email_type).toBe('order-shipped')
|
|
123
|
+
expect(row.audience).toBe('customer')
|
|
124
|
+
expect(row.recipient_email).toBe('jane@example.com')
|
|
125
|
+
expect(row.recipients).toBe('["jane@example.com"]')
|
|
126
|
+
expect(row.cc).toBe('["ops@example.com","boss@example.com"]')
|
|
127
|
+
expect(row.from_email).toBe('shop@acme.com')
|
|
128
|
+
expect(row.from_name).toBe('Acme Store')
|
|
129
|
+
expect(row.subject).toBe('Your parcel is on its way')
|
|
130
|
+
expect(row.body_html).toBe('<p>Hi Jane</p>')
|
|
131
|
+
expect(JSON.parse(row.payload as string)).toEqual({
|
|
132
|
+
customerName: 'Jane',
|
|
133
|
+
items: [{ name: 'Mug', qty: 2 }],
|
|
134
|
+
})
|
|
135
|
+
expect(row.provider).toBe('resend')
|
|
136
|
+
expect(row.provider_message_id).toBe('msg_1')
|
|
137
|
+
expect(row.status).toBe('sent')
|
|
138
|
+
expect(row.source).toBe('workflow')
|
|
139
|
+
expect(row.order_id).toBe('5f2b1a1e-8d7f-4c2e-9a1b-2c3d4e5f6a7b')
|
|
140
|
+
// A non-UUID never reaches a UUID column (22P02 would fail the insert).
|
|
141
|
+
expect(row.user_id).toBeNull()
|
|
142
|
+
expect(Object.keys(row)).not.toContain('apiKey')
|
|
143
|
+
expect(queries[0].values.join(' ')).not.toContain('sk_live_secret')
|
|
144
|
+
} finally {
|
|
145
|
+
restore()
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('redacts token-bearing parameters in the payload AND scrubs their values from the body', async () => {
|
|
150
|
+
const { api, queries, restore } = loadModule({})
|
|
151
|
+
try {
|
|
152
|
+
await api.recordSentEmail({
|
|
153
|
+
emailType: 'forgot-password',
|
|
154
|
+
to: 'jane@example.com',
|
|
155
|
+
subject: 'Reset',
|
|
156
|
+
html: '<a href="https://shop.test/reset?token=abcdef123456">Reset</a>',
|
|
157
|
+
payload: [
|
|
158
|
+
{ key: 'resetUrl', value: 'https://shop.test/reset?token=abcdef123456' },
|
|
159
|
+
{ key: 'userName', value: 'Jane' },
|
|
160
|
+
],
|
|
161
|
+
source: 'workflow',
|
|
162
|
+
})
|
|
163
|
+
const row = rowOf(queries[0])
|
|
164
|
+
expect(JSON.parse(row.payload as string)).toEqual({
|
|
165
|
+
resetUrl: '[redacted]',
|
|
166
|
+
userName: 'Jane',
|
|
167
|
+
})
|
|
168
|
+
expect(row.body_html).toBe('<a href="[redacted]">Reset</a>')
|
|
169
|
+
} finally {
|
|
170
|
+
restore()
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('records nothing without a recipient, and never rejects', async () => {
|
|
175
|
+
const { api, queries, restore } = loadModule({})
|
|
176
|
+
try {
|
|
177
|
+
await expect(api.recordSentEmail({ emailType: 'x', to: '', html: 'a' })).resolves.toBe(false)
|
|
178
|
+
await expect(api.recordSentEmail(null)).resolves.toBe(false)
|
|
179
|
+
expect(queries).toHaveLength(0)
|
|
180
|
+
} finally {
|
|
181
|
+
restore()
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('disables itself for the process once the table is missing (42P01)', async () => {
|
|
186
|
+
const { api, queries, restore } = loadModule({
|
|
187
|
+
queryImpl: async () => {
|
|
188
|
+
const err: any = new Error('relation "teleport_sent_emails" does not exist')
|
|
189
|
+
err.code = '42P01'
|
|
190
|
+
throw err
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
try {
|
|
194
|
+
await expect(api.recordSentEmail({ to: 'a@b.c', html: 'x' })).resolves.toBe(false)
|
|
195
|
+
await expect(api.recordSentEmail({ to: 'a@b.c', html: 'x' })).resolves.toBe(false)
|
|
196
|
+
expect(queries).toHaveLength(1)
|
|
197
|
+
} finally {
|
|
198
|
+
restore()
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('keeps trying through transient failures and gives up after three in a row', async () => {
|
|
203
|
+
let calls = 0
|
|
204
|
+
const { api, queries, restore } = loadModule({
|
|
205
|
+
queryImpl: async () => {
|
|
206
|
+
calls += 1
|
|
207
|
+
throw new Error('connection reset')
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
try {
|
|
211
|
+
for (let i = 0; i < 5; i++) {
|
|
212
|
+
await api.recordSentEmail({ to: 'a@b.c', html: 'x' })
|
|
213
|
+
}
|
|
214
|
+
expect(calls).toBe(3)
|
|
215
|
+
expect(queries).toHaveLength(3)
|
|
216
|
+
} finally {
|
|
217
|
+
restore()
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('is a no-op when the pg driver is not installed or no database is configured', async () => {
|
|
222
|
+
const withoutPg = loadModule({ pgAvailable: false })
|
|
223
|
+
try {
|
|
224
|
+
await expect(withoutPg.api.recordSentEmail({ to: 'a@b.c', html: 'x' })).resolves.toBe(false)
|
|
225
|
+
expect(withoutPg.queries).toHaveLength(0)
|
|
226
|
+
} finally {
|
|
227
|
+
withoutPg.restore()
|
|
228
|
+
}
|
|
229
|
+
const withoutDb = loadModule({})
|
|
230
|
+
try {
|
|
231
|
+
delete process.env.TELEPORT_DB_CONNECTION_STRING
|
|
232
|
+
delete process.env.TELEPORT_DB_HOST
|
|
233
|
+
await expect(withoutDb.api.recordSentEmail({ to: 'a@b.c', html: 'x' })).resolves.toBe(false)
|
|
234
|
+
expect(withoutDb.queries).toHaveLength(0)
|
|
235
|
+
} finally {
|
|
236
|
+
withoutDb.restore()
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('caps an oversized body and payload with a marker instead of failing', async () => {
|
|
241
|
+
const { api, queries, restore } = loadModule({})
|
|
242
|
+
try {
|
|
243
|
+
await api.recordSentEmail({
|
|
244
|
+
to: 'a@b.c',
|
|
245
|
+
html: 'x'.repeat(1000100),
|
|
246
|
+
payload: { blob: 'y'.repeat(300000) },
|
|
247
|
+
})
|
|
248
|
+
const row = rowOf(queries[0])
|
|
249
|
+
expect((row.body_html as string).length).toBeLessThan(1000200)
|
|
250
|
+
expect(row.body_html).toContain('truncated by sent-email-log')
|
|
251
|
+
expect(row.payload).toContain('truncated by sent-email-log')
|
|
252
|
+
} finally {
|
|
253
|
+
restore()
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('settleSentEmailLog waits for every in-flight record', async () => {
|
|
258
|
+
let release: () => void = () => undefined
|
|
259
|
+
const { api, queries, restore } = loadModule({
|
|
260
|
+
queryImpl: () =>
|
|
261
|
+
new Promise((resolve) => {
|
|
262
|
+
release = () => resolve({ rowCount: 1 })
|
|
263
|
+
}),
|
|
264
|
+
})
|
|
265
|
+
try {
|
|
266
|
+
const pending = api.recordSentEmail({ to: 'a@b.c', html: 'x' })
|
|
267
|
+
let settled = false
|
|
268
|
+
const settle = api.settleSentEmailLog().then(() => {
|
|
269
|
+
settled = true
|
|
270
|
+
})
|
|
271
|
+
await new Promise((r) => setTimeout(r, 5))
|
|
272
|
+
expect(settled).toBe(false)
|
|
273
|
+
release()
|
|
274
|
+
await pending
|
|
275
|
+
await settle
|
|
276
|
+
expect(settled).toBe(true)
|
|
277
|
+
expect(queries).toHaveLength(1)
|
|
278
|
+
} finally {
|
|
279
|
+
restore()
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('wrapEmailNodeHandler', () => {
|
|
284
|
+
it('records after the handler resolves and registers the record on the workflow context queue', async () => {
|
|
285
|
+
const { api, queries, restore } = loadModule({})
|
|
286
|
+
try {
|
|
287
|
+
const handler = jest.fn(async () => ({ id: 'rs_1', success: true }))
|
|
288
|
+
const wrapped = api.wrapEmailNodeHandler('email-resend', handler)
|
|
289
|
+
const context: any = { __pendingNodePromises: [] }
|
|
290
|
+
const config = {
|
|
291
|
+
to: ['a@b.c'],
|
|
292
|
+
from: 'shop@acme.com',
|
|
293
|
+
subject: 'Hi',
|
|
294
|
+
body: '<p>x</p>',
|
|
295
|
+
emailPurpose: 'welcome',
|
|
296
|
+
emailAudience: 'customer',
|
|
297
|
+
apiKey: 'secret',
|
|
298
|
+
}
|
|
299
|
+
const result = await wrapped(config, context)
|
|
300
|
+
expect(result).toEqual({ id: 'rs_1', success: true })
|
|
301
|
+
expect(context.__pendingNodePromises).toHaveLength(1)
|
|
302
|
+
await Promise.all(context.__pendingNodePromises)
|
|
303
|
+
const row = rowOf(queries[0])
|
|
304
|
+
expect(row.email_type).toBe('welcome')
|
|
305
|
+
expect(row.audience).toBe('customer')
|
|
306
|
+
expect(row.provider).toBe('resend')
|
|
307
|
+
expect(row.provider_message_id).toBe('rs_1')
|
|
308
|
+
expect(row.source).toBe('workflow')
|
|
309
|
+
expect(row.source_ref).toBe('email-resend')
|
|
310
|
+
expect(row.status).toBe('sent')
|
|
311
|
+
} finally {
|
|
312
|
+
restore()
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('records a failed attempt when the handler reports failure or throws, and rethrows', async () => {
|
|
317
|
+
const { api, queries, restore } = loadModule({})
|
|
318
|
+
try {
|
|
319
|
+
const refused = api.wrapEmailNodeHandler('email-postmark', async () => ({
|
|
320
|
+
id: '',
|
|
321
|
+
success: false,
|
|
322
|
+
error: 'domain not verified',
|
|
323
|
+
}))
|
|
324
|
+
const ctx1: any = {}
|
|
325
|
+
await refused({ to: 'a@b.c', body: 'x' }, ctx1)
|
|
326
|
+
await Promise.all(ctx1.__pendingNodePromises)
|
|
327
|
+
expect(rowOf(queries[0]).status).toBe('failed')
|
|
328
|
+
expect(rowOf(queries[0]).error_message).toBe('domain not verified')
|
|
329
|
+
|
|
330
|
+
const thrown = api.wrapEmailNodeHandler('email-sendgrid', async () => {
|
|
331
|
+
throw new Error('boom')
|
|
332
|
+
})
|
|
333
|
+
const ctx2: any = {}
|
|
334
|
+
await expect(thrown({ to: 'a@b.c', body: 'x' }, ctx2)).rejects.toThrow('boom')
|
|
335
|
+
await Promise.all(ctx2.__pendingNodePromises)
|
|
336
|
+
expect(rowOf(queries[1]).status).toBe('failed')
|
|
337
|
+
expect(rowOf(queries[1]).error_message).toBe('boom')
|
|
338
|
+
} finally {
|
|
339
|
+
restore()
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('records an integration node only for its send-email action, and forwards extra arguments', async () => {
|
|
344
|
+
const { api, queries, restore } = loadModule({})
|
|
345
|
+
try {
|
|
346
|
+
const calls: unknown[][] = []
|
|
347
|
+
const handler = async (...args: unknown[]) => {
|
|
348
|
+
calls.push(args)
|
|
349
|
+
return { success: true, message: { id: 'gm_1' } }
|
|
350
|
+
}
|
|
351
|
+
const wrapped = api.wrapEmailNodeHandler('integration-gmail', handler)
|
|
352
|
+
const ctx: any = {}
|
|
353
|
+
const onChunk = noop
|
|
354
|
+
await wrapped({ action: 'list-messages' }, ctx, onChunk)
|
|
355
|
+
expect(calls[0]).toHaveLength(3)
|
|
356
|
+
expect(ctx.__pendingNodePromises).toBeUndefined()
|
|
357
|
+
|
|
358
|
+
await wrapped({ action: 'send-email', to: 'a@b.c', subject: 's', body: '<p>b</p>' }, ctx)
|
|
359
|
+
await Promise.all(ctx.__pendingNodePromises)
|
|
360
|
+
expect(queries).toHaveLength(1)
|
|
361
|
+
expect(rowOf(queries[0]).provider).toBe('gmail')
|
|
362
|
+
expect(rowOf(queries[0]).provider_message_id).toBe('gm_1')
|
|
363
|
+
expect(rowOf(queries[0]).email_type).toBe('custom')
|
|
364
|
+
} finally {
|
|
365
|
+
restore()
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('emits a same-shaped no-op module for a non-Postgres data source', async () => {
|
|
371
|
+
const code = generateSentEmailLogCode({ pgSupported: false })
|
|
372
|
+
const module = { exports: {} as any }
|
|
373
|
+
// eslint-disable-next-line no-new-func
|
|
374
|
+
new Function('module', 'exports', code)(module, module.exports)
|
|
375
|
+
expect(code).not.toContain("require('pg')")
|
|
376
|
+
await expect(module.exports.recordSentEmail({ to: 'a@b.c' })).resolves.toBe(false)
|
|
377
|
+
await expect(module.exports.settleSentEmailLog()).resolves.toBeUndefined()
|
|
378
|
+
const handler = async () => 1
|
|
379
|
+
expect(module.exports.wrapEmailNodeHandler('email-resend', handler)).toBe(handler)
|
|
380
|
+
})
|
|
381
|
+
})
|