@teleporthq/teleport-plugin-next-workflows 0.44.0 → 0.44.1
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__/_helpers/fake-store-database.ts +428 -0
- package/__tests__/_helpers/invoice-store.ts +382 -0
- package/__tests__/email-locale-senders.test.ts +3 -1
- package/__tests__/email-sender-name.test.ts +513 -0
- package/__tests__/graph-utils-topological-order.test.ts +155 -1
- package/__tests__/invoice-billing-details.test.ts +107 -1
- package/__tests__/invoice-data-access-replace.test.ts +236 -2
- package/__tests__/invoice-order-items-settle.test.ts +1 -1
- package/__tests__/invoice-payment-email.test.ts +191 -0
- package/__tests__/invoice-regenerate-email.test.ts +288 -0
- package/__tests__/invoice-regenerate-route.test.ts +9 -7
- package/__tests__/invoice-reservation.test.ts +1 -1
- package/__tests__/invoice-tender.test.ts +1 -1
- package/__tests__/segment-splitter-checkout-order.test.ts +306 -0
- package/__tests__/streaming-on-end-env-filter.test.ts +18 -18
- package/__tests__/streaming-on-end-order.test.ts +387 -0
- package/dist/cjs/account-delete-route-generator.d.ts +1 -0
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +2 -1
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts +1 -0
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +2 -1
- 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 +9 -26
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/graph-utils.d.ts +31 -1
- package/dist/cjs/graph-utils.d.ts.map +1 -1
- package/dist/cjs/graph-utils.js +112 -10
- package/dist/cjs/graph-utils.js.map +1 -1
- package/dist/cjs/invoice/api-routes-code.d.ts +12 -3
- package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/cjs/invoice/api-routes-code.js +152 -21
- 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 +133 -21
- package/dist/cjs/invoice/data-access-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 +15 -25
- package/dist/cjs/invoice/email-sender-code.js.map +1 -1
- package/dist/cjs/invoice/index.js +1 -1
- package/dist/cjs/invoice/index.js.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.js +10 -6
- package/dist/cjs/invoice/invoice-assembly-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 +3 -0
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/nodes/email/email-mailgun.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-mailgun.js +4 -2
- package/dist/cjs/nodes/email/email-mailgun.js.map +1 -1
- package/dist/cjs/nodes/email/email-postmark.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-postmark.js +5 -2
- package/dist/cjs/nodes/email/email-postmark.js.map +1 -1
- package/dist/cjs/nodes/email/email-resend.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-resend.js +5 -2
- package/dist/cjs/nodes/email/email-resend.js.map +1 -1
- package/dist/cjs/nodes/email/email-sendgrid.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-sendgrid.js +8 -2
- package/dist/cjs/nodes/email/email-sendgrid.js.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-remove.d.ts.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-remove.js +2 -0
- package/dist/cjs/nodes/storage/storage-local-remove.js.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-set.d.ts.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-set.js +4 -0
- package/dist/cjs/nodes/storage/storage-local-set.js.map +1 -1
- package/dist/cjs/segment-splitter.d.ts.map +1 -1
- package/dist/cjs/segment-splitter.js +65 -27
- package/dist/cjs/segment-splitter.js.map +1 -1
- package/dist/cjs/transactional-email-code.d.ts.map +1 -1
- package/dist/cjs/transactional-email-code.js +32 -18
- package/dist/cjs/transactional-email-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +2 -0
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/account-delete-route-generator.d.ts +1 -0
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +2 -1
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts +1 -0
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +2 -1
- 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 +9 -26
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/graph-utils.d.ts +31 -1
- package/dist/esm/graph-utils.d.ts.map +1 -1
- package/dist/esm/graph-utils.js +109 -9
- package/dist/esm/graph-utils.js.map +1 -1
- package/dist/esm/invoice/api-routes-code.d.ts +12 -3
- package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/esm/invoice/api-routes-code.js +152 -21
- 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 +133 -21
- package/dist/esm/invoice/data-access-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 +15 -25
- package/dist/esm/invoice/email-sender-code.js.map +1 -1
- package/dist/esm/invoice/index.js +1 -1
- package/dist/esm/invoice/index.js.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.js +10 -6
- package/dist/esm/invoice/invoice-assembly-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 +3 -0
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/nodes/email/email-mailgun.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-mailgun.js +4 -2
- package/dist/esm/nodes/email/email-mailgun.js.map +1 -1
- package/dist/esm/nodes/email/email-postmark.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-postmark.js +5 -2
- package/dist/esm/nodes/email/email-postmark.js.map +1 -1
- package/dist/esm/nodes/email/email-resend.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-resend.js +5 -2
- package/dist/esm/nodes/email/email-resend.js.map +1 -1
- package/dist/esm/nodes/email/email-sendgrid.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-sendgrid.js +8 -2
- package/dist/esm/nodes/email/email-sendgrid.js.map +1 -1
- package/dist/esm/nodes/storage/storage-local-remove.d.ts.map +1 -1
- package/dist/esm/nodes/storage/storage-local-remove.js +2 -0
- package/dist/esm/nodes/storage/storage-local-remove.js.map +1 -1
- package/dist/esm/nodes/storage/storage-local-set.d.ts.map +1 -1
- package/dist/esm/nodes/storage/storage-local-set.js +4 -0
- package/dist/esm/nodes/storage/storage-local-set.js.map +1 -1
- package/dist/esm/segment-splitter.d.ts.map +1 -1
- package/dist/esm/segment-splitter.js +66 -28
- package/dist/esm/segment-splitter.js.map +1 -1
- package/dist/esm/transactional-email-code.d.ts.map +1 -1
- package/dist/esm/transactional-email-code.js +32 -18
- package/dist/esm/transactional-email-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +2 -0
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +3 -3
- package/src/account-delete-route-generator.ts +3 -1
- package/src/auth-generator.ts +3 -1
- package/src/executor-generator.ts +9 -26
- package/src/graph-utils.ts +143 -11
- package/src/invoice/api-routes-code.ts +166 -21
- package/src/invoice/data-access-code.ts +139 -21
- package/src/invoice/email-sender-code.ts +15 -25
- package/src/invoice/index.ts +1 -1
- package/src/invoice/invoice-assembly-code.ts +10 -6
- package/src/invoice/invoice-html-code.ts +3 -0
- package/src/nodes/email/email-mailgun.ts +11 -2
- package/src/nodes/email/email-postmark.ts +12 -2
- package/src/nodes/email/email-resend.ts +12 -2
- package/src/nodes/email/email-sendgrid.ts +15 -2
- package/src/nodes/storage/storage-local-remove.ts +4 -0
- package/src/nodes/storage/storage-local-set.ts +6 -0
- package/src/segment-splitter.ts +90 -36
- package/src/transactional-email-code.ts +32 -21
- package/src/workflow-project-plugin.ts +2 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { NextWorkflowProjectPlugin } from '../src/workflow-project-plugin'
|
|
2
|
+
import { generateProviderSendFunction } from '../src/transactional-email-code'
|
|
3
|
+
import { generateEmailSenderCode } from '../src/invoice/email-sender-code'
|
|
4
|
+
import { loadHandler } from './_helpers/load-handler'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A store's emails name the sender the editor stores beside the address
|
|
8
|
+
* (`fromName`): every workflow email node, the welcome and farewell emails and
|
|
9
|
+
* the invoice email, on every provider. A name holding a comma or a quote is
|
|
10
|
+
* quoted, and a `from` that already names its sender is never wrapped twice.
|
|
11
|
+
* Executed against fake provider SDKs: what matters is the From each provider
|
|
12
|
+
* is handed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface SenderCase {
|
|
16
|
+
label: string
|
|
17
|
+
from: string
|
|
18
|
+
fromName: string
|
|
19
|
+
// The From a provider taking one mailbox string is handed.
|
|
20
|
+
header: string
|
|
21
|
+
// The two parts SendGrid and MailerSend are handed.
|
|
22
|
+
email: string
|
|
23
|
+
name: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const CASES: SenderCase[] = [
|
|
27
|
+
{
|
|
28
|
+
label: 'name set',
|
|
29
|
+
from: 'shop@lumen.test',
|
|
30
|
+
fromName: 'Lumen Pottery',
|
|
31
|
+
header: 'Lumen Pottery <shop@lumen.test>',
|
|
32
|
+
email: 'shop@lumen.test',
|
|
33
|
+
name: 'Lumen Pottery',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: 'no name',
|
|
37
|
+
from: 'shop@lumen.test',
|
|
38
|
+
fromName: '',
|
|
39
|
+
header: 'shop@lumen.test',
|
|
40
|
+
email: 'shop@lumen.test',
|
|
41
|
+
name: '',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: 'a name with a comma and a quote',
|
|
45
|
+
from: 'shop@lumen.test',
|
|
46
|
+
fromName: 'Smith, Jones & "Co"',
|
|
47
|
+
header: '"Smith, Jones & \\"Co\\"" <shop@lumen.test>',
|
|
48
|
+
email: 'shop@lumen.test',
|
|
49
|
+
name: 'Smith, Jones & "Co"',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: 'from already formatted',
|
|
53
|
+
from: 'Lumen Pottery <shop@lumen.test>',
|
|
54
|
+
fromName: 'Lumen Pottery',
|
|
55
|
+
header: 'Lumen Pottery <shop@lumen.test>',
|
|
56
|
+
email: 'shop@lumen.test',
|
|
57
|
+
name: 'Lumen Pottery',
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
const QUOTED_NAME = CASES[2]
|
|
62
|
+
|
|
63
|
+
type Provider = 'postmark' | 'resend' | 'sendgrid' | 'mailgun' | 'mailersend'
|
|
64
|
+
|
|
65
|
+
const expectedFrom = (provider: Provider, c: SenderCase): unknown => {
|
|
66
|
+
if (provider === 'sendgrid') {
|
|
67
|
+
// SendGrid reads a string's display name verbatim, quotes included.
|
|
68
|
+
return c.name ? { email: c.email, name: c.name } : c.email
|
|
69
|
+
}
|
|
70
|
+
if (provider === 'mailersend') {
|
|
71
|
+
return { email: c.email, name: c.name || c.email }
|
|
72
|
+
}
|
|
73
|
+
return c.header
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Fake provider SDKs that record the From of every email they are handed. */
|
|
77
|
+
const fakeSdks = (sent: unknown[]): Record<string, unknown> => {
|
|
78
|
+
const sendgrid = {
|
|
79
|
+
setApiKey: () => undefined,
|
|
80
|
+
send: async (msg: { from: unknown }) => {
|
|
81
|
+
sent.push(msg.from)
|
|
82
|
+
return [{ statusCode: 202, headers: { 'x-message-id': 'sg-1' } }]
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
const chainable = () => {
|
|
86
|
+
const params: Record<string, unknown> = {}
|
|
87
|
+
for (const key of ['From', 'To', 'Subject', 'Html', 'ReplyTo', 'Cc', 'Bcc', 'Attachments']) {
|
|
88
|
+
params['set' + key] = (value: unknown) => {
|
|
89
|
+
params[key.toLowerCase()] = value
|
|
90
|
+
return params
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return params
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
postmark: {
|
|
97
|
+
ServerClient: function ServerClient() {
|
|
98
|
+
return {
|
|
99
|
+
sendEmail: async (payload: { From: unknown }) => {
|
|
100
|
+
sent.push(payload.From)
|
|
101
|
+
return { MessageID: 'pm-1' }
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
resend: {
|
|
107
|
+
Resend: function Resend() {
|
|
108
|
+
return {
|
|
109
|
+
emails: {
|
|
110
|
+
send: async (payload: { from: unknown }) => {
|
|
111
|
+
sent.push(payload.from)
|
|
112
|
+
return { data: { id: 're-1' }, error: null }
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
'@sendgrid/mail': {
|
|
119
|
+
...sendgrid,
|
|
120
|
+
MailService: function MailService() {
|
|
121
|
+
return sendgrid
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
'mailgun.js': function Mailgun() {
|
|
125
|
+
return {
|
|
126
|
+
client: () => ({
|
|
127
|
+
messages: {
|
|
128
|
+
create: async (_domain: string, payload: { from: unknown }) => {
|
|
129
|
+
sent.push(payload.from)
|
|
130
|
+
return { id: 'mg-1' }
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
'form-data': function FormData() {
|
|
137
|
+
return {}
|
|
138
|
+
},
|
|
139
|
+
mailersend: {
|
|
140
|
+
MailerSend: function MailerSend() {
|
|
141
|
+
return {
|
|
142
|
+
email: {
|
|
143
|
+
send: async (params: { from: unknown }) => {
|
|
144
|
+
sent.push(params.from)
|
|
145
|
+
return { statusCode: 202, headers: { 'x-message-id': 'ms-1' } }
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
Sender: function Sender(email: string, name: string) {
|
|
151
|
+
return { email, name }
|
|
152
|
+
},
|
|
153
|
+
Recipient: function Recipient(email: string) {
|
|
154
|
+
return { email }
|
|
155
|
+
},
|
|
156
|
+
Attachment: function Attachment() {
|
|
157
|
+
return {}
|
|
158
|
+
},
|
|
159
|
+
EmailParams: function EmailParams() {
|
|
160
|
+
return chainable()
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const quietConsole = () => {
|
|
167
|
+
const spies = (['log', 'info', 'warn', 'error'] as const).map((method) =>
|
|
168
|
+
jest.spyOn(console, method).mockImplementation(() => undefined)
|
|
169
|
+
)
|
|
170
|
+
return () => spies.forEach((spy) => spy.mockRestore())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
describe('workflow email nodes send the sender name', () => {
|
|
174
|
+
// The handler loads its SDK through `__non_webpack_require__` whenever that
|
|
175
|
+
// exists, so the fakes take the SDKs' place through the same global.
|
|
176
|
+
const withSdks = async <T>(sdks: Record<string, unknown>, run: () => Promise<T>): Promise<T> => {
|
|
177
|
+
const scope = globalThis as { __non_webpack_require__?: unknown }
|
|
178
|
+
scope.__non_webpack_require__ = (id: string) => {
|
|
179
|
+
if (!(id in sdks)) {
|
|
180
|
+
throw new Error('unexpected require: ' + id)
|
|
181
|
+
}
|
|
182
|
+
return sdks[id]
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return await run()
|
|
186
|
+
} finally {
|
|
187
|
+
delete scope.__non_webpack_require__
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const NODES: Array<[Provider, string]> = [
|
|
192
|
+
['postmark', 'email-postmark'],
|
|
193
|
+
['resend', 'email-resend'],
|
|
194
|
+
['sendgrid', 'email-sendgrid'],
|
|
195
|
+
['mailgun', 'email-mailgun'],
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
for (const [provider, nodeType] of NODES) {
|
|
199
|
+
describe(nodeType, () => {
|
|
200
|
+
for (const senderCase of CASES) {
|
|
201
|
+
it(senderCase.label, async () => {
|
|
202
|
+
const sent: unknown[] = []
|
|
203
|
+
const handler = loadHandler(nodeType)
|
|
204
|
+
const config = {
|
|
205
|
+
apiKey: 'key',
|
|
206
|
+
serverToken: 'token',
|
|
207
|
+
domain: 'mg.lumen.test',
|
|
208
|
+
from: senderCase.from,
|
|
209
|
+
fromName: senderCase.fromName,
|
|
210
|
+
to: 'buyer@lumen.test',
|
|
211
|
+
subject: 'Your order',
|
|
212
|
+
body: '<p>Thanks</p>',
|
|
213
|
+
}
|
|
214
|
+
const result = await withSdks(fakeSdks(sent), () => handler(config, {}))
|
|
215
|
+
expect(result).toMatchObject({ success: true })
|
|
216
|
+
expect(sent).toEqual([expectedFrom(provider, senderCase)])
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('welcome and farewell emails send the sender name', () => {
|
|
224
|
+
const LEDGER_MODULE = '../../../utils/email/sent-email-log'
|
|
225
|
+
|
|
226
|
+
const loadSend = (provider: Provider, sent: unknown[], ledger: unknown[]) => {
|
|
227
|
+
const sdks = fakeSdks(sent)
|
|
228
|
+
const requireStub = (id: string) => {
|
|
229
|
+
if (id === LEDGER_MODULE) {
|
|
230
|
+
return { recordSentEmail: (entry: unknown) => ledger.push(entry) }
|
|
231
|
+
}
|
|
232
|
+
if (!(id in sdks)) {
|
|
233
|
+
throw new Error('unexpected require: ' + id)
|
|
234
|
+
}
|
|
235
|
+
return sdks[id]
|
|
236
|
+
}
|
|
237
|
+
const source = generateProviderSendFunction(provider, {
|
|
238
|
+
emailType: 'welcome',
|
|
239
|
+
source: 'signup',
|
|
240
|
+
sourceRef: 'api/auth/signup',
|
|
241
|
+
relativePrefix: '../../..',
|
|
242
|
+
})
|
|
243
|
+
// eslint-disable-next-line no-new-func
|
|
244
|
+
return new Function('require', 'process', `${source}\nreturn __sendProviderEmail;`)(
|
|
245
|
+
requireStub,
|
|
246
|
+
{ env: { MAILGUN_DOMAIN: 'mg.lumen.test' } }
|
|
247
|
+
) as (msg: Record<string, unknown>) => Promise<void>
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const PROVIDERS: Provider[] = ['postmark', 'resend', 'sendgrid', 'mailgun', 'mailersend']
|
|
251
|
+
|
|
252
|
+
for (const provider of PROVIDERS) {
|
|
253
|
+
describe(provider, () => {
|
|
254
|
+
for (const senderCase of CASES) {
|
|
255
|
+
it(senderCase.label, async () => {
|
|
256
|
+
const sent: unknown[] = []
|
|
257
|
+
const ledger: unknown[] = []
|
|
258
|
+
const send = loadSend(provider, sent, ledger)
|
|
259
|
+
await send({
|
|
260
|
+
from: senderCase.from,
|
|
261
|
+
fromName: senderCase.fromName,
|
|
262
|
+
to: 'buyer@lumen.test',
|
|
263
|
+
subject: 'Welcome',
|
|
264
|
+
html: '<p>Hi</p>',
|
|
265
|
+
apiKey: 'key',
|
|
266
|
+
})
|
|
267
|
+
expect(sent).toEqual([expectedFrom(provider, senderCase)])
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
it('the ledger records the name the email went out with', async () => {
|
|
274
|
+
const ledger: unknown[] = []
|
|
275
|
+
const send = loadSend('postmark', [], ledger)
|
|
276
|
+
await send({
|
|
277
|
+
from: QUOTED_NAME.from,
|
|
278
|
+
fromName: QUOTED_NAME.fromName,
|
|
279
|
+
to: 'buyer@lumen.test',
|
|
280
|
+
subject: 'Welcome',
|
|
281
|
+
html: '<p>Hi</p>',
|
|
282
|
+
apiKey: 'key',
|
|
283
|
+
})
|
|
284
|
+
expect(ledger).toEqual([
|
|
285
|
+
expect.objectContaining({ from: QUOTED_NAME.email, fromName: QUOTED_NAME.name }),
|
|
286
|
+
])
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// The account nodes carry the sender the editor stored on them; the plugin
|
|
290
|
+
// bakes it into the routes they call.
|
|
291
|
+
describe('the routes baked from the account nodes', () => {
|
|
292
|
+
const accountNodeConfig = {
|
|
293
|
+
emailProvider: 'email-resend',
|
|
294
|
+
from: 'shop@lumen.test',
|
|
295
|
+
fromName: QUOTED_NAME.fromName,
|
|
296
|
+
apiKey: 'RESEND_API_KEY',
|
|
297
|
+
body: '<p>Hello {{userName}}</p>',
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const generateRoutes = async () => {
|
|
301
|
+
const uidl: any = {
|
|
302
|
+
name: 'Lumen',
|
|
303
|
+
globals: { settings: { title: 'Lumen', language: 'en' }, env: {} },
|
|
304
|
+
root: {
|
|
305
|
+
name: 'Root',
|
|
306
|
+
stateDefinitions: {},
|
|
307
|
+
node: { type: 'element', content: { elementType: 'container', children: [] } },
|
|
308
|
+
},
|
|
309
|
+
components: {},
|
|
310
|
+
authentication: {
|
|
311
|
+
enabled: true,
|
|
312
|
+
passwordAuthEnabled: true,
|
|
313
|
+
dataSourceId: 'ds1',
|
|
314
|
+
dataSourceType: 'postgresql',
|
|
315
|
+
providers: [],
|
|
316
|
+
roles: [],
|
|
317
|
+
tables: {},
|
|
318
|
+
pageProtection: {},
|
|
319
|
+
folderProtection: {},
|
|
320
|
+
authPages: {},
|
|
321
|
+
callbackBaseUrl: '',
|
|
322
|
+
envKeys: {},
|
|
323
|
+
customUserProperties: [],
|
|
324
|
+
},
|
|
325
|
+
dataSources: { ds1: { type: 'postgresql', config: {} } },
|
|
326
|
+
workflows: {
|
|
327
|
+
workflows: {
|
|
328
|
+
account: {
|
|
329
|
+
id: 'account',
|
|
330
|
+
name: 'Account',
|
|
331
|
+
trigger: { type: 'event-custom-triggered', nodeId: 't', config: { eventName: 'x' } },
|
|
332
|
+
nodes: [
|
|
333
|
+
{ id: 'signup', type: 'account-signup', config: accountNodeConfig, stepNumber: 1 },
|
|
334
|
+
{
|
|
335
|
+
id: 'delete',
|
|
336
|
+
type: 'account-delete-current',
|
|
337
|
+
config: accountNodeConfig,
|
|
338
|
+
stepNumber: 2,
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
edges: [],
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
customNodes: {},
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
const structure: any = {
|
|
348
|
+
uidl,
|
|
349
|
+
strategy: { pages: { options: {} } },
|
|
350
|
+
files: new Map(),
|
|
351
|
+
dependencies: {},
|
|
352
|
+
devDependencies: {},
|
|
353
|
+
}
|
|
354
|
+
await new NextWorkflowProjectPlugin().runAfter(structure)
|
|
355
|
+
const content = (key: string): string => String(structure.files.get(key).files[0].content)
|
|
356
|
+
return {
|
|
357
|
+
signup: content('auth-signup-route'),
|
|
358
|
+
farewell: content('account-delete-current-route'),
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Evaluates a whole route module and hands back one of its functions.
|
|
363
|
+
const loadRouteFunction = (
|
|
364
|
+
source: string,
|
|
365
|
+
fnName: string,
|
|
366
|
+
sent: unknown[],
|
|
367
|
+
ledger: unknown[]
|
|
368
|
+
) => {
|
|
369
|
+
const sdks = fakeSdks(sent)
|
|
370
|
+
const requireStub = (id: string): unknown => {
|
|
371
|
+
if (id.endsWith('/utils/email/sent-email-log')) {
|
|
372
|
+
return {
|
|
373
|
+
recordSentEmail: (entry: unknown) => ledger.push(entry),
|
|
374
|
+
settleSentEmailLog: async () => undefined,
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (id.endsWith('/utils/email/email-locale')) {
|
|
378
|
+
return {
|
|
379
|
+
pickLocalizedTemplate: (base: { subject: string; body: string }) => ({
|
|
380
|
+
...base,
|
|
381
|
+
locale: 'en',
|
|
382
|
+
}),
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return id in sdks ? sdks[id] : {}
|
|
386
|
+
}
|
|
387
|
+
const moduleShim = { exports: {} }
|
|
388
|
+
// eslint-disable-next-line no-new-func
|
|
389
|
+
return new Function(
|
|
390
|
+
'module',
|
|
391
|
+
'exports',
|
|
392
|
+
'require',
|
|
393
|
+
'process',
|
|
394
|
+
`${source}\nreturn ${fnName};`
|
|
395
|
+
)(moduleShim, moduleShim.exports, requireStub, { env: { RESEND_API_KEY: 'key' } }) as (
|
|
396
|
+
...args: unknown[]
|
|
397
|
+
) => Promise<void>
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
it('the welcome email names the sender stored on the signup node', async () => {
|
|
401
|
+
const { signup } = await generateRoutes()
|
|
402
|
+
const sent: unknown[] = []
|
|
403
|
+
const ledger: unknown[] = []
|
|
404
|
+
const restore = quietConsole()
|
|
405
|
+
try {
|
|
406
|
+
const sendWelcomeEmail = loadRouteFunction(signup, 'sendWelcomeEmail', sent, ledger)
|
|
407
|
+
await sendWelcomeEmail('buyer@lumen.test', { userName: 'Ana' }, 'user-1', 'en')
|
|
408
|
+
} finally {
|
|
409
|
+
restore()
|
|
410
|
+
}
|
|
411
|
+
expect(sent).toEqual([QUOTED_NAME.header])
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('the farewell email names the sender stored on the delete-account node', async () => {
|
|
415
|
+
const { farewell } = await generateRoutes()
|
|
416
|
+
const sent: unknown[] = []
|
|
417
|
+
const ledger: unknown[] = []
|
|
418
|
+
const restore = quietConsole()
|
|
419
|
+
try {
|
|
420
|
+
const sendFarewellEmail = loadRouteFunction(farewell, 'sendFarewellEmail', sent, ledger)
|
|
421
|
+
await sendFarewellEmail('buyer@lumen.test', { userName: 'Ana' }, 'user-1', 'en')
|
|
422
|
+
} finally {
|
|
423
|
+
restore()
|
|
424
|
+
}
|
|
425
|
+
expect(sent).toEqual([QUOTED_NAME.header])
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
describe('the invoice email sends the sender name', () => {
|
|
431
|
+
const loadInvoiceSender = (
|
|
432
|
+
provider: Provider,
|
|
433
|
+
senderCase: SenderCase,
|
|
434
|
+
sent: unknown[],
|
|
435
|
+
ledger: unknown[]
|
|
436
|
+
) => {
|
|
437
|
+
const code = generateEmailSenderCode({
|
|
438
|
+
enabled: true,
|
|
439
|
+
provider,
|
|
440
|
+
fromEmail: senderCase.from,
|
|
441
|
+
fromName: senderCase.fromName,
|
|
442
|
+
subject: 'Invoice {{invoiceNumber}}',
|
|
443
|
+
body: '<p>Attached</p>',
|
|
444
|
+
secretKeys: {},
|
|
445
|
+
} as any)
|
|
446
|
+
const sdks = fakeSdks(sent)
|
|
447
|
+
const requireStub = (id: string): unknown => {
|
|
448
|
+
if (id === './pdf-generator') {
|
|
449
|
+
return { replacePlaceholders: (text: string) => text, buildDataContext: () => ({}) }
|
|
450
|
+
}
|
|
451
|
+
if (id === '../email/sent-email-log') {
|
|
452
|
+
return { recordSentEmail: (entry: unknown) => ledger.push(entry) }
|
|
453
|
+
}
|
|
454
|
+
if (id === '../email/email-locale') {
|
|
455
|
+
return { pickLocalizedTemplate: (base: unknown) => base }
|
|
456
|
+
}
|
|
457
|
+
if (!(id in sdks)) {
|
|
458
|
+
throw new Error('unexpected require: ' + id)
|
|
459
|
+
}
|
|
460
|
+
return sdks[id]
|
|
461
|
+
}
|
|
462
|
+
const moduleShim = { exports: {} as Record<string, (...args: unknown[]) => Promise<any>> }
|
|
463
|
+
// resolveSecretKey falls back to an env var named like the key.
|
|
464
|
+
// eslint-disable-next-line no-new-func
|
|
465
|
+
new Function('module', 'require', 'process', code)(moduleShim, requireStub, {
|
|
466
|
+
env: { apiKey: 'key', serverToken: 'token', domain: 'mg.lumen.test' },
|
|
467
|
+
})
|
|
468
|
+
return moduleShim.exports.sendInvoiceEmail
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const PROVIDERS: Provider[] = ['postmark', 'resend', 'sendgrid', 'mailgun', 'mailersend']
|
|
472
|
+
|
|
473
|
+
for (const provider of PROVIDERS) {
|
|
474
|
+
describe(provider, () => {
|
|
475
|
+
for (const senderCase of CASES) {
|
|
476
|
+
it(senderCase.label, async () => {
|
|
477
|
+
const sent: unknown[] = []
|
|
478
|
+
const ledger: unknown[] = []
|
|
479
|
+
const restore = quietConsole()
|
|
480
|
+
let result: { success?: boolean } = {}
|
|
481
|
+
try {
|
|
482
|
+
const sendInvoiceEmail = loadInvoiceSender(provider, senderCase, sent, ledger)
|
|
483
|
+
result = await sendInvoiceEmail(
|
|
484
|
+
{ id: 'inv-1', invoiceNumber: 'INV-1', customerEmail: 'buyer@lumen.test' },
|
|
485
|
+
Buffer.from('%PDF')
|
|
486
|
+
)
|
|
487
|
+
} finally {
|
|
488
|
+
restore()
|
|
489
|
+
}
|
|
490
|
+
expect(result).toMatchObject({ success: true })
|
|
491
|
+
expect(sent).toEqual([expectedFrom(provider, senderCase)])
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
it('the ledger records the name the email went out with', async () => {
|
|
498
|
+
const ledger: unknown[] = []
|
|
499
|
+
const restore = quietConsole()
|
|
500
|
+
try {
|
|
501
|
+
const sendInvoiceEmail = loadInvoiceSender('postmark', QUOTED_NAME, [], ledger)
|
|
502
|
+
await sendInvoiceEmail(
|
|
503
|
+
{ id: 'inv-1', invoiceNumber: 'INV-1', customerEmail: 'buyer@lumen.test' },
|
|
504
|
+
Buffer.from('%PDF')
|
|
505
|
+
)
|
|
506
|
+
} finally {
|
|
507
|
+
restore()
|
|
508
|
+
}
|
|
509
|
+
expect(ledger).toEqual([
|
|
510
|
+
expect.objectContaining({ from: QUOTED_NAME.header, fromName: QUOTED_NAME.name }),
|
|
511
|
+
])
|
|
512
|
+
})
|
|
513
|
+
})
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getTopologicalOrder,
|
|
3
|
+
getNeverRunTogether,
|
|
4
|
+
collectWorkflowContextNodeIds,
|
|
5
|
+
} from '../src/graph-utils'
|
|
2
6
|
import type { UIDLWorkflowNode, UIDLWorkflowEdge } from '@teleporthq/teleport-types'
|
|
3
7
|
|
|
4
8
|
const node = (
|
|
@@ -106,3 +110,153 @@ describe('getTopologicalOrder — implicit workflowContext dependencies', () =>
|
|
|
106
110
|
expect(getTopologicalOrder(nodes, edges)).toEqual(['trig', 'a'])
|
|
107
111
|
})
|
|
108
112
|
})
|
|
113
|
+
|
|
114
|
+
const edge = (source: string, target: string, sourceHandle?: string, targetHandle?: string) => ({
|
|
115
|
+
id: `${source}->${target}`,
|
|
116
|
+
source,
|
|
117
|
+
target,
|
|
118
|
+
...(sourceHandle ? { sourceHandle } : {}),
|
|
119
|
+
...(targetHandle ? { targetHandle } : {}),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// The segment splitter's view: data steps on the server, browser steps on the
|
|
123
|
+
// client, an if-statement wherever the step before it ran.
|
|
124
|
+
const sideOf = (n: UIDLWorkflowNode) =>
|
|
125
|
+
n.type === 'general-if-statement' ? null : n.type.startsWith('data-') ? 'server' : 'client'
|
|
126
|
+
|
|
127
|
+
describe('getTopologicalOrder — loop back edges', () => {
|
|
128
|
+
it('a loop is not held up by the edge that sends its body back for the next item', () => {
|
|
129
|
+
// Counted as a dependency, the back edge left the loop waiting forever and
|
|
130
|
+
// everything from the loop on was ordered by step alone, so `join` (step 4,
|
|
131
|
+
// also reached by the short path) ran before `long2` (step 5) it waits for.
|
|
132
|
+
const nodes = [
|
|
133
|
+
node('start', 'state-get-local-state', 1),
|
|
134
|
+
node('loop', 'general-loop', 2),
|
|
135
|
+
node('body', 'data-create-item', 3),
|
|
136
|
+
node('after', 'general-custom-js', 3),
|
|
137
|
+
node('long1', 'general-custom-js', 4),
|
|
138
|
+
node('long2', 'general-custom-js', 5),
|
|
139
|
+
node('join', 'data-update-item', 4),
|
|
140
|
+
]
|
|
141
|
+
const edges: UIDLWorkflowEdge[] = [
|
|
142
|
+
edge('start', 'loop'),
|
|
143
|
+
edge('loop', 'body', 'loop', 'loop-body-in'),
|
|
144
|
+
edge('body', 'loop', 'loop-body-out', 'loop-back'),
|
|
145
|
+
edge('loop', 'after', 'exit'),
|
|
146
|
+
edge('after', 'long1'),
|
|
147
|
+
edge('long1', 'long2'),
|
|
148
|
+
edge('long2', 'join'),
|
|
149
|
+
edge('after', 'join'),
|
|
150
|
+
]
|
|
151
|
+
const order = getTopologicalOrder(nodes, edges)
|
|
152
|
+
expect(order.indexOf('loop')).toBeLessThan(order.indexOf('body'))
|
|
153
|
+
expect(order.indexOf('loop')).toBeLessThan(order.indexOf('after'))
|
|
154
|
+
expect(order.indexOf('long2')).toBeLessThan(order.indexOf('join'))
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('getTopologicalOrder — staying on one side', () => {
|
|
159
|
+
// fetch → gate ─true→ save → gate2 ─true→ email
|
|
160
|
+
// └false→ toast └false→ reset
|
|
161
|
+
// The false edges come first, so by step alone each refusal step runs
|
|
162
|
+
// between two server steps of the main chain.
|
|
163
|
+
const nodes = [
|
|
164
|
+
node('fetch', 'data-raw-query', 1),
|
|
165
|
+
node('gate', 'general-if-statement', 2),
|
|
166
|
+
node('toast', 'toast-show', 3),
|
|
167
|
+
node('save', 'data-create-item', 3),
|
|
168
|
+
node('gate2', 'general-if-statement', 4),
|
|
169
|
+
node('reset', 'state-update-local-state', 5),
|
|
170
|
+
node('email', 'data-raw-query', 5),
|
|
171
|
+
]
|
|
172
|
+
const edges: UIDLWorkflowEdge[] = [
|
|
173
|
+
edge('fetch', 'gate'),
|
|
174
|
+
edge('gate', 'toast', 'false'),
|
|
175
|
+
edge('gate', 'save', 'true'),
|
|
176
|
+
edge('save', 'gate2'),
|
|
177
|
+
edge('gate2', 'reset', 'false'),
|
|
178
|
+
edge('gate2', 'email', 'true'),
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
it('orders by step alone without the splitter', () => {
|
|
182
|
+
expect(getTopologicalOrder(nodes, edges)).toEqual([
|
|
183
|
+
'fetch',
|
|
184
|
+
'gate',
|
|
185
|
+
'toast',
|
|
186
|
+
'save',
|
|
187
|
+
'gate2',
|
|
188
|
+
'reset',
|
|
189
|
+
'email',
|
|
190
|
+
])
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('lets the main chain stay on the server ahead of the refusal branches it never runs with', () => {
|
|
194
|
+
expect(getTopologicalOrder(nodes, edges, sideOf)).toEqual([
|
|
195
|
+
'fetch',
|
|
196
|
+
'gate',
|
|
197
|
+
'save',
|
|
198
|
+
'gate2',
|
|
199
|
+
'email',
|
|
200
|
+
'toast',
|
|
201
|
+
'reset',
|
|
202
|
+
])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('keeps the order of steps that both run (a fan-out is not a branch)', () => {
|
|
206
|
+
const fanOut = [
|
|
207
|
+
node('fetch', 'data-raw-query', 1),
|
|
208
|
+
node('notify', 'toast-show', 2),
|
|
209
|
+
node('save', 'data-create-item', 2),
|
|
210
|
+
]
|
|
211
|
+
const fanOutEdges: UIDLWorkflowEdge[] = [edge('fetch', 'notify'), edge('fetch', 'save')]
|
|
212
|
+
expect(getTopologicalOrder(fanOut, fanOutEdges, sideOf)).toEqual(['fetch', 'notify', 'save'])
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('getNeverRunTogether', () => {
|
|
217
|
+
it('pairs the nodes of different branches of one if-statement or switch, nothing else', () => {
|
|
218
|
+
const nodes = [
|
|
219
|
+
node('gate', 'general-if-statement', 1),
|
|
220
|
+
node('a1', 'data-select', 2),
|
|
221
|
+
node('a2', 'data-select', 3),
|
|
222
|
+
node('b1', 'toast-show', 2),
|
|
223
|
+
node('switch', 'general-switch', 4),
|
|
224
|
+
node('c1', 'data-select', 5),
|
|
225
|
+
node('c2', 'data-select', 5),
|
|
226
|
+
node('loop', 'general-loop', 6),
|
|
227
|
+
node('bodyGate', 'general-if-statement', 7),
|
|
228
|
+
node('x', 'data-create-item', 8),
|
|
229
|
+
node('y', 'toast-show', 8),
|
|
230
|
+
node('z', 'data-select', 7),
|
|
231
|
+
]
|
|
232
|
+
const edges: UIDLWorkflowEdge[] = [
|
|
233
|
+
edge('gate', 'a1', 'true'),
|
|
234
|
+
edge('a1', 'a2'),
|
|
235
|
+
edge('gate', 'b1', 'false'),
|
|
236
|
+
edge('a2', 'switch'),
|
|
237
|
+
{
|
|
238
|
+
id: 'switch->c1',
|
|
239
|
+
source: 'switch',
|
|
240
|
+
target: 'c1',
|
|
241
|
+
sourceHandle: 'switch',
|
|
242
|
+
data: { caseId: 'x' },
|
|
243
|
+
},
|
|
244
|
+
edge('switch', 'c2', 'default'),
|
|
245
|
+
edge('c1', 'loop'),
|
|
246
|
+
edge('loop', 'bodyGate', 'loop', 'loop-body-in'),
|
|
247
|
+
edge('bodyGate', 'x', 'true'),
|
|
248
|
+
edge('bodyGate', 'y', 'false'),
|
|
249
|
+
edge('x', 'loop', 'loop-body-out', 'loop-back'),
|
|
250
|
+
edge('loop', 'z', 'exit'),
|
|
251
|
+
]
|
|
252
|
+
const neverRunTogether = getNeverRunTogether(nodes, edges)
|
|
253
|
+
expect(neverRunTogether('a2', 'b1')).toBe(true)
|
|
254
|
+
expect(neverRunTogether('b1', 'z')).toBe(true)
|
|
255
|
+
expect(neverRunTogether('c1', 'c2')).toBe(true)
|
|
256
|
+
expect(neverRunTogether('a1', 'a2')).toBe(false)
|
|
257
|
+
expect(neverRunTogether('a1', 'c1')).toBe(false)
|
|
258
|
+
// The back edge is not followed: the step after the loop is on neither
|
|
259
|
+
// branch of the if-statement inside the loop body.
|
|
260
|
+
expect(neverRunTogether('z', 'y')).toBe(false)
|
|
261
|
+
})
|
|
262
|
+
})
|