@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.
Files changed (158) hide show
  1. package/__tests__/_helpers/fake-store-database.ts +428 -0
  2. package/__tests__/_helpers/invoice-store.ts +382 -0
  3. package/__tests__/email-locale-senders.test.ts +3 -1
  4. package/__tests__/email-sender-name.test.ts +513 -0
  5. package/__tests__/graph-utils-topological-order.test.ts +155 -1
  6. package/__tests__/invoice-billing-details.test.ts +107 -1
  7. package/__tests__/invoice-data-access-replace.test.ts +236 -2
  8. package/__tests__/invoice-order-items-settle.test.ts +1 -1
  9. package/__tests__/invoice-payment-email.test.ts +191 -0
  10. package/__tests__/invoice-regenerate-email.test.ts +288 -0
  11. package/__tests__/invoice-regenerate-route.test.ts +9 -7
  12. package/__tests__/invoice-reservation.test.ts +1 -1
  13. package/__tests__/invoice-tender.test.ts +1 -1
  14. package/__tests__/segment-splitter-checkout-order.test.ts +306 -0
  15. package/__tests__/streaming-on-end-env-filter.test.ts +18 -18
  16. package/__tests__/streaming-on-end-order.test.ts +387 -0
  17. package/dist/cjs/account-delete-route-generator.d.ts +1 -0
  18. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
  19. package/dist/cjs/account-delete-route-generator.js +2 -1
  20. package/dist/cjs/account-delete-route-generator.js.map +1 -1
  21. package/dist/cjs/auth-generator.d.ts +1 -0
  22. package/dist/cjs/auth-generator.d.ts.map +1 -1
  23. package/dist/cjs/auth-generator.js +2 -1
  24. package/dist/cjs/auth-generator.js.map +1 -1
  25. package/dist/cjs/executor-generator.d.ts.map +1 -1
  26. package/dist/cjs/executor-generator.js +9 -26
  27. package/dist/cjs/executor-generator.js.map +1 -1
  28. package/dist/cjs/graph-utils.d.ts +31 -1
  29. package/dist/cjs/graph-utils.d.ts.map +1 -1
  30. package/dist/cjs/graph-utils.js +112 -10
  31. package/dist/cjs/graph-utils.js.map +1 -1
  32. package/dist/cjs/invoice/api-routes-code.d.ts +12 -3
  33. package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
  34. package/dist/cjs/invoice/api-routes-code.js +152 -21
  35. package/dist/cjs/invoice/api-routes-code.js.map +1 -1
  36. package/dist/cjs/invoice/data-access-code.d.ts.map +1 -1
  37. package/dist/cjs/invoice/data-access-code.js +133 -21
  38. package/dist/cjs/invoice/data-access-code.js.map +1 -1
  39. package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
  40. package/dist/cjs/invoice/email-sender-code.js +15 -25
  41. package/dist/cjs/invoice/email-sender-code.js.map +1 -1
  42. package/dist/cjs/invoice/index.js +1 -1
  43. package/dist/cjs/invoice/index.js.map +1 -1
  44. package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
  45. package/dist/cjs/invoice/invoice-assembly-code.js +10 -6
  46. package/dist/cjs/invoice/invoice-assembly-code.js.map +1 -1
  47. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  48. package/dist/cjs/invoice/invoice-html-code.js +3 -0
  49. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  50. package/dist/cjs/nodes/email/email-mailgun.d.ts.map +1 -1
  51. package/dist/cjs/nodes/email/email-mailgun.js +4 -2
  52. package/dist/cjs/nodes/email/email-mailgun.js.map +1 -1
  53. package/dist/cjs/nodes/email/email-postmark.d.ts.map +1 -1
  54. package/dist/cjs/nodes/email/email-postmark.js +5 -2
  55. package/dist/cjs/nodes/email/email-postmark.js.map +1 -1
  56. package/dist/cjs/nodes/email/email-resend.d.ts.map +1 -1
  57. package/dist/cjs/nodes/email/email-resend.js +5 -2
  58. package/dist/cjs/nodes/email/email-resend.js.map +1 -1
  59. package/dist/cjs/nodes/email/email-sendgrid.d.ts.map +1 -1
  60. package/dist/cjs/nodes/email/email-sendgrid.js +8 -2
  61. package/dist/cjs/nodes/email/email-sendgrid.js.map +1 -1
  62. package/dist/cjs/nodes/storage/storage-local-remove.d.ts.map +1 -1
  63. package/dist/cjs/nodes/storage/storage-local-remove.js +2 -0
  64. package/dist/cjs/nodes/storage/storage-local-remove.js.map +1 -1
  65. package/dist/cjs/nodes/storage/storage-local-set.d.ts.map +1 -1
  66. package/dist/cjs/nodes/storage/storage-local-set.js +4 -0
  67. package/dist/cjs/nodes/storage/storage-local-set.js.map +1 -1
  68. package/dist/cjs/segment-splitter.d.ts.map +1 -1
  69. package/dist/cjs/segment-splitter.js +65 -27
  70. package/dist/cjs/segment-splitter.js.map +1 -1
  71. package/dist/cjs/transactional-email-code.d.ts.map +1 -1
  72. package/dist/cjs/transactional-email-code.js +32 -18
  73. package/dist/cjs/transactional-email-code.js.map +1 -1
  74. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  75. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  76. package/dist/cjs/workflow-project-plugin.js +2 -0
  77. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  78. package/dist/esm/account-delete-route-generator.d.ts +1 -0
  79. package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
  80. package/dist/esm/account-delete-route-generator.js +2 -1
  81. package/dist/esm/account-delete-route-generator.js.map +1 -1
  82. package/dist/esm/auth-generator.d.ts +1 -0
  83. package/dist/esm/auth-generator.d.ts.map +1 -1
  84. package/dist/esm/auth-generator.js +2 -1
  85. package/dist/esm/auth-generator.js.map +1 -1
  86. package/dist/esm/executor-generator.d.ts.map +1 -1
  87. package/dist/esm/executor-generator.js +9 -26
  88. package/dist/esm/executor-generator.js.map +1 -1
  89. package/dist/esm/graph-utils.d.ts +31 -1
  90. package/dist/esm/graph-utils.d.ts.map +1 -1
  91. package/dist/esm/graph-utils.js +109 -9
  92. package/dist/esm/graph-utils.js.map +1 -1
  93. package/dist/esm/invoice/api-routes-code.d.ts +12 -3
  94. package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
  95. package/dist/esm/invoice/api-routes-code.js +152 -21
  96. package/dist/esm/invoice/api-routes-code.js.map +1 -1
  97. package/dist/esm/invoice/data-access-code.d.ts.map +1 -1
  98. package/dist/esm/invoice/data-access-code.js +133 -21
  99. package/dist/esm/invoice/data-access-code.js.map +1 -1
  100. package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
  101. package/dist/esm/invoice/email-sender-code.js +15 -25
  102. package/dist/esm/invoice/email-sender-code.js.map +1 -1
  103. package/dist/esm/invoice/index.js +1 -1
  104. package/dist/esm/invoice/index.js.map +1 -1
  105. package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
  106. package/dist/esm/invoice/invoice-assembly-code.js +10 -6
  107. package/dist/esm/invoice/invoice-assembly-code.js.map +1 -1
  108. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  109. package/dist/esm/invoice/invoice-html-code.js +3 -0
  110. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  111. package/dist/esm/nodes/email/email-mailgun.d.ts.map +1 -1
  112. package/dist/esm/nodes/email/email-mailgun.js +4 -2
  113. package/dist/esm/nodes/email/email-mailgun.js.map +1 -1
  114. package/dist/esm/nodes/email/email-postmark.d.ts.map +1 -1
  115. package/dist/esm/nodes/email/email-postmark.js +5 -2
  116. package/dist/esm/nodes/email/email-postmark.js.map +1 -1
  117. package/dist/esm/nodes/email/email-resend.d.ts.map +1 -1
  118. package/dist/esm/nodes/email/email-resend.js +5 -2
  119. package/dist/esm/nodes/email/email-resend.js.map +1 -1
  120. package/dist/esm/nodes/email/email-sendgrid.d.ts.map +1 -1
  121. package/dist/esm/nodes/email/email-sendgrid.js +8 -2
  122. package/dist/esm/nodes/email/email-sendgrid.js.map +1 -1
  123. package/dist/esm/nodes/storage/storage-local-remove.d.ts.map +1 -1
  124. package/dist/esm/nodes/storage/storage-local-remove.js +2 -0
  125. package/dist/esm/nodes/storage/storage-local-remove.js.map +1 -1
  126. package/dist/esm/nodes/storage/storage-local-set.d.ts.map +1 -1
  127. package/dist/esm/nodes/storage/storage-local-set.js +4 -0
  128. package/dist/esm/nodes/storage/storage-local-set.js.map +1 -1
  129. package/dist/esm/segment-splitter.d.ts.map +1 -1
  130. package/dist/esm/segment-splitter.js +66 -28
  131. package/dist/esm/segment-splitter.js.map +1 -1
  132. package/dist/esm/transactional-email-code.d.ts.map +1 -1
  133. package/dist/esm/transactional-email-code.js +32 -18
  134. package/dist/esm/transactional-email-code.js.map +1 -1
  135. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  136. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  137. package/dist/esm/workflow-project-plugin.js +2 -0
  138. package/dist/esm/workflow-project-plugin.js.map +1 -1
  139. package/package.json +3 -3
  140. package/src/account-delete-route-generator.ts +3 -1
  141. package/src/auth-generator.ts +3 -1
  142. package/src/executor-generator.ts +9 -26
  143. package/src/graph-utils.ts +143 -11
  144. package/src/invoice/api-routes-code.ts +166 -21
  145. package/src/invoice/data-access-code.ts +139 -21
  146. package/src/invoice/email-sender-code.ts +15 -25
  147. package/src/invoice/index.ts +1 -1
  148. package/src/invoice/invoice-assembly-code.ts +10 -6
  149. package/src/invoice/invoice-html-code.ts +3 -0
  150. package/src/nodes/email/email-mailgun.ts +11 -2
  151. package/src/nodes/email/email-postmark.ts +12 -2
  152. package/src/nodes/email/email-resend.ts +12 -2
  153. package/src/nodes/email/email-sendgrid.ts +15 -2
  154. package/src/nodes/storage/storage-local-remove.ts +4 -0
  155. package/src/nodes/storage/storage-local-set.ts +6 -0
  156. package/src/segment-splitter.ts +90 -36
  157. package/src/transactional-email-code.ts +32 -21
  158. package/src/workflow-project-plugin.ts +2 -0
@@ -35,8 +35,9 @@ ${ProductOptions.generateProductOptionsHelperCode()}
35
35
 
36
36
  // The invoice details an order's metadata (JSON text) carries beside its
37
37
  // billing columns — the keys \`ORDER_BILLING_METADATA_KEYS\` (teleport-gui)
38
- // writes: the address in its parts and the buyer's tax or VAT number. Every
39
- // part is '' when the order has none or its metadata is no JSON object.
38
+ // writes: the address in its parts, the company and the buyer's tax or VAT
39
+ // number. Every part is '' when the order has none or its metadata is no JSON
40
+ // object.
40
41
  function readOrderBillingMetadata(raw) {
41
42
  var parsed = raw;
42
43
  if (typeof raw === 'string') {
@@ -53,6 +54,7 @@ function readOrderBillingMetadata(raw) {
53
54
  }
54
55
  return {
55
56
  taxId: part('billingTaxId'),
57
+ company: part('billingCompany'),
56
58
  street: part('billingStreet'),
57
59
  city: part('billingCity'),
58
60
  postalCode: part('billingPostalCode'),
@@ -446,10 +448,11 @@ function assembleInvoiceData(body, hydratedOrder, hydratedItems) {
446
448
  // who checked "bill to a different address" has both on the order.
447
449
  //
448
450
  // An order whose buyer gave invoice details of their own (a booking's
449
- // "Add invoice details") carries them in its metadata as well: the address
450
- // in its parts and the tax or VAT number. Those parts are the invoice's
451
- // address — never mixed with a shipping address — and the number is the
452
- // buyer's VAT.
451
+ // "Add invoice details", a store checkout's "Billing Details") carries them
452
+ // in its metadata as well: the address in its parts, the company and the tax
453
+ // or VAT number. Those parts are the invoice's address — never mixed with a
454
+ // shipping address — the company is printed under the buyer's name, and the
455
+ // number is the buyer's VAT.
453
456
  var orderRow = orderShippingSource;
454
457
  var orderBilling = readOrderBillingMetadata(orderRow.metadata);
455
458
  var billedApart = !!(orderBilling.street || orderBilling.city || orderBilling.country);
@@ -487,6 +490,7 @@ function assembleInvoiceData(body, hydratedOrder, hydratedItems) {
487
490
  customerZip: body.customerZip || fallbackCustomerZip || '',
488
491
  customerCountry: body.customerCountry || fallbackCustomerCountry || '',
489
492
  customerVat: body.customerVat || orderBilling.taxId || '',
493
+ customerCompany: body.customerCompany || orderBilling.company || '',
490
494
  companyName: pdfGenerator.COMPANY_DETAILS.companyName || '',
491
495
  companyAddress: pdfGenerator.COMPANY_DETAILS.companyAddress || '',
492
496
  companyCity: pdfGenerator.COMPANY_DETAILS.companyCity || '',
@@ -361,6 +361,7 @@ function buildInvoiceDataScope(invoiceData) {
361
361
 
362
362
  var realCustomer = {
363
363
  name: invoiceData.customerName || '',
364
+ company: invoiceData.customerCompany || '',
364
365
  email: invoiceData.customerEmail || '',
365
366
  address: invoiceData.customerAddress || '',
366
367
  city: invoiceData.customerCity || '',
@@ -371,6 +372,7 @@ function buildInvoiceDataScope(invoiceData) {
371
372
  };
372
373
  // The "Bill to" lines a buyer may leave out show only when they did not
373
374
  // (string flags: the template compares strings, as with \`hasShipping\`).
375
+ realCustomer.hasCompany = realCustomer.company ? 'true' : 'false';
374
376
  realCustomer.hasZip = realCustomer.zip ? 'true' : 'false';
375
377
  realCustomer.hasCountry = realCustomer.country ? 'true' : 'false';
376
378
  realCustomer.hasVat = realCustomer.vat ? 'true' : 'false';
@@ -980,6 +982,7 @@ function buildFallbackInvoiceHtml(scope) {
980
982
  escapeHtml(comp.city || '') + '<br/>' +
981
983
  escapeHtml(comp.email || '') + '</div>');
982
984
  parts.push('<div style="flex:1;"><strong>' + escapeHtml(cust.name || '') + '</strong><br/>' +
985
+ (cust.company ? escapeHtml(cust.company) + '<br/>' : '') +
983
986
  escapeHtml(cust.email || '') + '<br/>' +
984
987
  escapeHtml(cust.address || '') + '<br/>' +
985
988
  escapeHtml(cust.city || '') + '</div>');
@@ -1,4 +1,10 @@
1
+ import { EmailSender } from '@teleporthq/teleport-shared'
1
2
  import { NodeHandlerGenerator, handlerToString } from '../types'
3
+ import { assertHandlerHasNoModuleRefs } from '../analytics/commerce-tracking'
4
+
5
+ // Ambient, not imported: a serialized handler has no module scope, so
6
+ // generateHandler appends the helper's source (EmailSender, teleport-shared).
7
+ declare function __emailSender(sender: EmailSender.EmailSenderInput): EmailSender.EmailSenderParts
2
8
 
3
9
  async function email_mailgun(config: any, context: Record<string, unknown>) {
4
10
  const apiKey = config.apiKey
@@ -25,7 +31,7 @@ async function email_mailgun(config: any, context: Record<string, unknown>) {
25
31
  }
26
32
  const mg = mailgun.client(clientOpts)
27
33
 
28
- const fromField = fromName ? fromName + ' <' + from + '>' : from
34
+ const fromField = __emailSender({ from, name: fromName }).header
29
35
  const toField = Array.isArray(to) ? to : [to]
30
36
 
31
37
  const msgPayload: Record<string, any> = {
@@ -61,6 +67,9 @@ export const emailMailgun: NodeHandlerGenerator = {
61
67
  'form-data': '^4.0.0',
62
68
  },
63
69
  generateHandler(): string {
64
- return handlerToString(email_mailgun)
70
+ return assertHandlerHasNoModuleRefs(
71
+ handlerToString(email_mailgun) + '\n' + EmailSender.generateEmailSenderHelperCode(),
72
+ 'email-mailgun'
73
+ )
65
74
  },
66
75
  }
@@ -1,8 +1,15 @@
1
+ import { EmailSender } from '@teleporthq/teleport-shared'
1
2
  import { NodeHandlerGenerator, handlerToString } from '../types'
3
+ import { assertHandlerHasNoModuleRefs } from '../analytics/commerce-tracking'
4
+
5
+ // Ambient, not imported: a serialized handler has no module scope, so
6
+ // generateHandler appends the helper's source (EmailSender, teleport-shared).
7
+ declare function __emailSender(sender: EmailSender.EmailSenderInput): EmailSender.EmailSenderParts
2
8
 
3
9
  async function email_postmark(config: any, context: Record<string, unknown>) {
4
10
  const serverToken = config.serverToken
5
11
  const from = config.from
12
+ const fromName = config.fromName
6
13
  const to = config.to
7
14
  const subject = config.subject
8
15
  const body = config.body
@@ -21,7 +28,7 @@ async function email_postmark(config: any, context: Record<string, unknown>) {
21
28
  const toField = Array.isArray(to) ? to.join(', ') : to
22
29
 
23
30
  const emailPayload: Record<string, any> = {
24
- From: from,
31
+ From: __emailSender({ from, name: fromName }).header,
25
32
  To: toField,
26
33
  Subject: subject,
27
34
  HtmlBody: body,
@@ -58,6 +65,9 @@ export const emailPostmark: NodeHandlerGenerator = {
58
65
  postmark: '^4.0.0',
59
66
  },
60
67
  generateHandler(): string {
61
- return handlerToString(email_postmark)
68
+ return assertHandlerHasNoModuleRefs(
69
+ handlerToString(email_postmark) + '\n' + EmailSender.generateEmailSenderHelperCode(),
70
+ 'email-postmark'
71
+ )
62
72
  },
63
73
  }
@@ -1,8 +1,15 @@
1
+ import { EmailSender } from '@teleporthq/teleport-shared'
1
2
  import { NodeHandlerGenerator, handlerToString } from '../types'
3
+ import { assertHandlerHasNoModuleRefs } from '../analytics/commerce-tracking'
4
+
5
+ // Ambient, not imported: a serialized handler has no module scope, so
6
+ // generateHandler appends the helper's source (EmailSender, teleport-shared).
7
+ declare function __emailSender(sender: EmailSender.EmailSenderInput): EmailSender.EmailSenderParts
2
8
 
3
9
  async function email_resend(config: any, context: Record<string, unknown>) {
4
10
  const apiKey = config.apiKey
5
11
  const from = config.from
12
+ const fromName = config.fromName
6
13
  const to = config.to
7
14
  const subject = config.subject
8
15
  const body = config.body
@@ -17,7 +24,7 @@ async function email_resend(config: any, context: Record<string, unknown>) {
17
24
  const resend = new Resend(apiKey)
18
25
 
19
26
  const payload: Record<string, any> = {
20
- from,
27
+ from: __emailSender({ from, name: fromName }).header,
21
28
  to: Array.isArray(to) ? to : [to],
22
29
  subject,
23
30
  html: body,
@@ -53,6 +60,9 @@ export const emailResend: NodeHandlerGenerator = {
53
60
  resend: '^4.0.0',
54
61
  },
55
62
  generateHandler(): string {
56
- return handlerToString(email_resend)
63
+ return assertHandlerHasNoModuleRefs(
64
+ handlerToString(email_resend) + '\n' + EmailSender.generateEmailSenderHelperCode(),
65
+ 'email-resend'
66
+ )
57
67
  },
58
68
  }
@@ -1,8 +1,15 @@
1
+ import { EmailSender } from '@teleporthq/teleport-shared'
1
2
  import { NodeHandlerGenerator, handlerToString } from '../types'
3
+ import { assertHandlerHasNoModuleRefs } from '../analytics/commerce-tracking'
4
+
5
+ // Ambient, not imported: a serialized handler has no module scope, so
6
+ // generateHandler appends the helper's source (EmailSender, teleport-shared).
7
+ declare function __emailSender(sender: EmailSender.EmailSenderInput): EmailSender.EmailSenderParts
2
8
 
3
9
  async function email_sendgrid(config: any, context: Record<string, unknown>) {
4
10
  const apiKey = config.apiKey
5
11
  const from = config.from
12
+ const fromName = config.fromName
6
13
  const to = config.to
7
14
  const subject = config.subject
8
15
  const body = config.body
@@ -17,9 +24,12 @@ async function email_sendgrid(config: any, context: Record<string, unknown>) {
17
24
  const sgMail = new MailService()
18
25
  sgMail.setApiKey(apiKey)
19
26
 
27
+ // SendGrid reads a "Name <email>" string's name verbatim, quotes included,
28
+ // so a named sender goes as its two parts.
29
+ const sender = __emailSender({ from, name: fromName })
20
30
  const msg: Record<string, any> = {
21
31
  to: Array.isArray(to) ? to : [to],
22
- from,
32
+ from: sender.name ? { email: sender.email, name: sender.name } : sender.email,
23
33
  subject,
24
34
  html: body,
25
35
  }
@@ -62,6 +72,9 @@ export const emailSendgrid: NodeHandlerGenerator = {
62
72
  '@sendgrid/mail': '^8.0.0',
63
73
  },
64
74
  generateHandler(): string {
65
- return handlerToString(email_sendgrid)
75
+ return assertHandlerHasNoModuleRefs(
76
+ handlerToString(email_sendgrid) + '\n' + EmailSender.generateEmailSenderHelperCode(),
77
+ 'email-sendgrid'
78
+ )
66
79
  },
67
80
  }
@@ -1,10 +1,14 @@
1
1
  import { NodeHandlerGenerator, handlerToString } from '../types'
2
2
 
3
+ // Same-tab notice of the removal, as storage-local-set gives for a write.
3
4
  async function storage_local_remove(config: any, context: Record<string, unknown>) {
4
5
  const key = config.key
5
6
 
6
7
  try {
7
8
  localStorage.removeItem(key)
9
+ window.dispatchEvent(
10
+ new CustomEvent('teleport:local-storage', { detail: { key, value: null } })
11
+ )
8
12
  return { success: true }
9
13
  } catch (err: unknown) {
10
14
  return { success: false, error: (err as Error).message }
@@ -1,5 +1,8 @@
1
1
  import { NodeHandlerGenerator, handlerToString } from '../types'
2
2
 
3
+ // A write in the visitor's own tab fires no `storage` event. This one lets page
4
+ // scripts react at once: Google consent mode is granted by the cookie banner's
5
+ // Accept, which is this step (teleport-project-generator-next google-consent-mode).
3
6
  async function storage_local_set(config: any, context: Record<string, unknown>) {
4
7
  const key = config.key
5
8
  const value = config.value
@@ -7,6 +10,9 @@ async function storage_local_set(config: any, context: Record<string, unknown>)
7
10
  try {
8
11
  const serialized = typeof value === 'string' ? value : JSON.stringify(value)
9
12
  localStorage.setItem(key, serialized)
13
+ window.dispatchEvent(
14
+ new CustomEvent('teleport:local-storage', { detail: { key, value: serialized } })
15
+ )
10
16
  return { success: true }
11
17
  } catch (err: unknown) {
12
18
  return { success: false, error: (err as Error).message }
@@ -1,6 +1,6 @@
1
1
  import { UIDLWorkflow, UIDLWorkflowNode, UIDLWorkflowEdge } from '@teleporthq/teleport-types'
2
2
  import { WorkflowSegment, WorkflowExecutionEnv } from './types'
3
- import { getLoopBodyNodes, getNodeById, getTopologicalOrder } from './graph-utils'
3
+ import { getDependents, getLoopBodyNodes, getNodeById, getTopologicalOrder } from './graph-utils'
4
4
  import { nodeRegistry } from './nodes'
5
5
  import { AWAIT_RESULT_CONFIG_KEY } from './await-result'
6
6
  import { WELCOME_EMAIL_CONFIG_KEYS } from './transactional-email-code'
@@ -144,25 +144,54 @@ export const splitIntoSegments = (workflow: UIDLWorkflow): WorkflowSegment[] =>
144
144
  ? workflow.edges.filter((e) => !errorBranchIds.has(e.source) && !errorBranchIds.has(e.target))
145
145
  : workflow.edges
146
146
 
147
- const order = getTopologicalOrder(mainNodes, mainEdges)
148
147
  // Loop bodies must execute as a single unit at runtime. The split-by-env
149
148
  // walk below would otherwise drop AI/server steps in one segment and the
150
149
  // client-only post-processing into another, leaving the loop's body half-
151
- // executed for every iteration. We reorder the topo so all body nodes sit
152
- // immediately after their owning loop, and we record a single forced env
153
- // (server if any body or the loop itself needs server, else client) so the
154
- // forthcoming env walk emits one segment for the loop + body.
155
- const { reorderedOrder, forcedEnvByNodeId } = enforceLoopBodyIntegrity(
156
- order,
150
+ // executed for every iteration. Every loop + body gets a single forced env
151
+ // (server if any body or the loop itself needs server, else client), and the
152
+ // topo order is rearranged so all body nodes sit immediately after their
153
+ // owning loop, so the env walk emits one segment for the loop + body.
154
+ const forcedEnvByNodeId = resolveLoopClusterEnvs(mainNodes, mainEdges)
155
+ const order = getTopologicalOrder(
157
156
  mainNodes,
158
- mainEdges
157
+ mainEdges,
158
+ (node) => forcedEnvByNodeId.get(node.id) ?? resolveFixedExecutionEnv(node)
159
159
  )
160
+ const reorderedOrder = hoistLoopBodies(order, mainNodes, mainEdges)
160
161
  const segments: WorkflowSegment[] = []
161
162
  let currentEnv: WorkflowExecutionEnv | null = null
162
163
  let currentNodeIds: string[] = []
163
164
  let segClientCount = 0
164
165
  let segServerCount = 0
165
166
 
167
+ // A server route runs its segment's nodes by stepNumber, never by edge, so a
168
+ // node must not join a segment holding one of its inputs at a later step (a
169
+ // join also reached by a shorter path, a value read across branches): the
170
+ // route would run it first. Loop bodies run inside their loop, at the loop's
171
+ // step, and never leave its segment.
172
+ const inputsById = new Map<string, string[]>()
173
+ Object.entries(getDependents(mainNodes, mainEdges)).forEach(([source, dependents]) =>
174
+ dependents.forEach((id) => inputsById.set(id, [...(inputsById.get(id) || []), source]))
175
+ )
176
+ const loopStepByBodyNodeId = new Map<string, number>()
177
+ mainNodes
178
+ .filter((n) => n.type === 'general-loop')
179
+ .forEach((loop) =>
180
+ getLoopBodyNodes(loop.id, mainEdges).forEach((id) =>
181
+ loopStepByBodyNodeId.set(
182
+ id,
183
+ Math.min(loopStepByBodyNodeId.get(id) ?? Infinity, loop.stepNumber || 0)
184
+ )
185
+ )
186
+ )
187
+ const runsAtStep = (nodeId: string): number =>
188
+ loopStepByBodyNodeId.get(nodeId) ?? (getNodeById(mainNodes, nodeId)?.stepNumber || 0)
189
+ const readsALaterStepOfThisSegment = (node: UIDLWorkflowNode): boolean =>
190
+ !loopStepByBodyNodeId.has(node.id) &&
191
+ (inputsById.get(node.id) || []).some(
192
+ (inputId) => currentNodeIds.includes(inputId) && runsAtStep(inputId) > (node.stepNumber || 0)
193
+ )
194
+
166
195
  const flushSegment = () => {
167
196
  if (currentNodeIds.length === 0 || currentEnv === null) {
168
197
  return
@@ -196,7 +225,10 @@ export const splitIntoSegments = (workflow: UIDLWorkflow): WorkflowSegment[] =>
196
225
  const forced = forcedEnvByNodeId.get(nodeId)
197
226
  const nodeEnv = forced ?? resolveExecutionEnv(node, currentEnv)
198
227
 
199
- if (currentEnv !== null && nodeEnv !== currentEnv) {
228
+ if (
229
+ currentEnv !== null &&
230
+ (nodeEnv !== currentEnv || (nodeEnv === 'server' && readsALaterStepOfThisSegment(node)))
231
+ ) {
200
232
  flushSegment()
201
233
  }
202
234
  currentEnv = nodeEnv
@@ -208,10 +240,7 @@ export const splitIntoSegments = (workflow: UIDLWorkflow): WorkflowSegment[] =>
208
240
  }
209
241
 
210
242
  /**
211
- * Re-arrange the topological node order so every loop's body nodes sit
212
- * directly after the loop, and pick a single execution env for the
213
- * (loop + body) cluster. Nested loops are processed inner-first so the outer
214
- * loop's body window already contains the inner body in the right order.
243
+ * The single execution env of every (loop + body) cluster.
215
244
  *
216
245
  * Why an explicit env override is needed: a body that mixes universal /
217
246
  * server / client nodes (e.g. fetch → AI prompt → custom-js with
@@ -224,19 +253,58 @@ export const splitIntoSegments = (workflow: UIDLWorkflow): WorkflowSegment[] =>
224
253
  * runs the same string of JS regardless of where it executes); the inverse
225
254
  * is not true for AI / HTTP-with-secret nodes. When no member is server, we
226
255
  * keep the cluster on the client so DOM- or window-bound bodies still work.
256
+ * A node of nested loops takes its outermost loop's env, which is server
257
+ * whenever an inner loop's is.
227
258
  */
228
- const enforceLoopBodyIntegrity = (
229
- order: string[],
259
+ const resolveLoopClusterEnvs = (
230
260
  mainNodes: UIDLWorkflowNode[],
231
261
  mainEdges: UIDLWorkflowEdge[]
232
- ): {
233
- reorderedOrder: string[]
234
- forcedEnvByNodeId: Map<string, WorkflowExecutionEnv>
235
- } => {
262
+ ): Map<string, WorkflowExecutionEnv> => {
236
263
  const forcedEnvByNodeId = new Map<string, WorkflowExecutionEnv>()
264
+ for (const loop of mainNodes.filter((n) => n.type === 'general-loop')) {
265
+ const bodyIds = getLoopBodyNodes(loop.id, mainEdges)
266
+ if (bodyIds.length === 0) {
267
+ continue
268
+ }
269
+ const clusterMembers = [loop.id, ...bodyIds]
270
+ .map((id) => getNodeById(mainNodes, id))
271
+ .filter((n): n is UIDLWorkflowNode => Boolean(n))
272
+ const clusterEnv: WorkflowExecutionEnv = clusterMembers.some(
273
+ (member) => resolveExecutionEnv(member, null) === 'server'
274
+ )
275
+ ? 'server'
276
+ : 'client'
277
+ for (const member of clusterMembers) {
278
+ if (forcedEnvByNodeId.get(member.id) !== 'server') {
279
+ forcedEnvByNodeId.set(member.id, clusterEnv)
280
+ }
281
+ }
282
+ }
283
+ return forcedEnvByNodeId
284
+ }
285
+
286
+ /**
287
+ * Where a node runs whatever ran before it, or `null` for a node that takes the
288
+ * env of the node before it (an if-statement): it fits either side.
289
+ */
290
+ const resolveFixedExecutionEnv = (node: UIDLWorkflowNode): WorkflowExecutionEnv | null => {
291
+ const env = resolveExecutionEnv(node, 'server')
292
+ return env === resolveExecutionEnv(node, 'client') ? env : null
293
+ }
294
+
295
+ /**
296
+ * Re-arrange the topological node order so every loop's body nodes sit
297
+ * directly after the loop. Nested loops are processed inner-first so the outer
298
+ * loop's body window already contains the inner body in the right order.
299
+ */
300
+ const hoistLoopBodies = (
301
+ order: string[],
302
+ mainNodes: UIDLWorkflowNode[],
303
+ mainEdges: UIDLWorkflowEdge[]
304
+ ): string[] => {
237
305
  const loopNodes = mainNodes.filter((n) => n.type === 'general-loop')
238
306
  if (loopNodes.length === 0) {
239
- return { reorderedOrder: order, forcedEnvByNodeId }
307
+ return order
240
308
  }
241
309
 
242
310
  // Process inner loops before outer ones: an inner loop's body is a subset
@@ -253,20 +321,6 @@ const enforceLoopBodyIntegrity = (
253
321
  }
254
322
 
255
323
  const clusterIds = new Set<string>([loopId, ...bodyIds])
256
- const clusterMembers = Array.from(clusterIds)
257
- .map((id) => getNodeById(mainNodes, id))
258
- .filter((n): n is UIDLWorkflowNode => Boolean(n))
259
-
260
- let clusterEnv: WorkflowExecutionEnv = 'client'
261
- for (const member of clusterMembers) {
262
- if (resolveExecutionEnv(member, null) === 'server') {
263
- clusterEnv = 'server'
264
- break
265
- }
266
- }
267
- for (const member of clusterMembers) {
268
- forcedEnvByNodeId.set(member.id, clusterEnv)
269
- }
270
324
 
271
325
  // Hoist body nodes (preserving their existing relative order) immediately
272
326
  // after the loop. Other nodes keep their relative position.
@@ -283,7 +337,7 @@ const enforceLoopBodyIntegrity = (
283
337
  ]
284
338
  }
285
339
 
286
- return { reorderedOrder: working, forcedEnvByNodeId }
340
+ return working
287
341
  }
288
342
 
289
343
  /**
@@ -5,6 +5,7 @@
5
5
  * every route stays byte-for-byte consistent.
6
6
  */
7
7
  import { UIDLLocalizedEmailTemplates } from '@teleporthq/teleport-types'
8
+ import { EmailSender } from '@teleporthq/teleport-shared'
8
9
  import { getEmailProviderDependencies } from './invoice/email-sender-code'
9
10
  import { EMAIL_LOCALE_FILE_NAME, EMAIL_LOCALE_PATH } from './email-locale/email-locale-scope'
10
11
 
@@ -39,24 +40,28 @@ export interface TransactionalEmailLedger {
39
40
  relativePrefix: string
40
41
  }
41
42
 
42
- // Provider-specific `__sendProviderEmail({ from, to, subject, html, apiKey,
43
- // tokenValues?, userId? })`. No attachment (unlike the invoice sender). When
44
- // no provider is configured the function is a no-op so the route stays
45
- // self-contained and never throws. With `ledger`, the provider function is
46
- // emitted as `__sendProviderEmailRaw` and `__sendProviderEmail` records every
47
- // attempt (success or throw) before returning / rethrowing.
43
+ // Provider-specific `__sendProviderEmail({ from, fromName?, to, subject, html,
44
+ // apiKey, tokenValues?, userId? })`, preceded by the `__emailSender` helper that
45
+ // names the sender. No attachment (unlike the invoice sender). When no provider
46
+ // is configured the function is a no-op so the route stays self-contained and
47
+ // never throws. With `ledger`, the provider function is emitted as
48
+ // `__sendProviderEmailRaw` and `__sendProviderEmail` records every attempt
49
+ // (success or throw) before returning / rethrowing.
48
50
  export const generateProviderSendFunction = (
49
51
  provider?: string | null,
50
52
  ledger?: TransactionalEmailLedger
51
53
  ): string => {
52
54
  const raw = generateRawProviderSendFunction(provider)
53
- if (!ledger || !provider || !SUPPORTED_EMAIL_PROVIDERS.has(provider)) {
55
+ if (!provider || !SUPPORTED_EMAIL_PROVIDERS.has(provider)) {
54
56
  return raw
55
57
  }
56
- return `${raw.replace(
57
- 'async function __sendProviderEmail(',
58
- 'async function __sendProviderEmailRaw('
59
- )}
58
+ const senderHelper = EmailSender.generateEmailSenderHelperCode()
59
+ if (!ledger) {
60
+ return `${senderHelper}
61
+ ${raw}`
62
+ }
63
+ return `${senderHelper}
64
+ ${raw.replace('async function __sendProviderEmail(', 'async function __sendProviderEmailRaw(')}
60
65
 
61
66
  var __sentEmailLog = require('${ledger.relativePrefix}/utils/email/sent-email-log');
62
67
 
@@ -67,12 +72,14 @@ async function __sendProviderEmail(msg) {
67
72
  } catch (err) {
68
73
  failure = err;
69
74
  }
75
+ var sender = __emailSender({ from: msg.from, name: msg.fromName });
70
76
  // Recorded in the background; the route settles the ledger before it replies.
71
77
  __sentEmailLog.recordSentEmail({
72
78
  emailType: ${JSON.stringify(ledger.emailType)},
73
79
  audience: 'customer',
74
80
  to: msg.to,
75
- from: msg.from,
81
+ from: sender.email,
82
+ fromName: sender.name,
76
83
  subject: msg.subject,
77
84
  html: msg.html,
78
85
  payload: msg.tokenValues,
@@ -93,20 +100,26 @@ const generateRawProviderSendFunction = (provider?: string | null): string => {
93
100
  return `async function __sendProviderEmail(msg) {
94
101
  var Resend = require('resend').Resend;
95
102
  var resend = new Resend(msg.apiKey);
96
- var result = await resend.emails.send({ from: msg.from, to: [msg.to], subject: msg.subject, html: msg.html });
103
+ var sender = __emailSender({ from: msg.from, name: msg.fromName });
104
+ var result = await resend.emails.send({ from: sender.header, to: [msg.to], subject: msg.subject, html: msg.html });
97
105
  if (result && result.error) { throw new Error(result.error.message || 'Failed to send email'); }
98
106
  }`
99
107
  case 'sendgrid':
108
+ // SendGrid reads a "Name <email>" string's name verbatim, quotes included,
109
+ // so a named sender goes as its two parts.
100
110
  return `async function __sendProviderEmail(msg) {
101
111
  var sgMail = require('@sendgrid/mail');
102
112
  sgMail.setApiKey(msg.apiKey);
103
- await sgMail.send({ to: msg.to, from: msg.from, subject: msg.subject, html: msg.html });
113
+ var sender = __emailSender({ from: msg.from, name: msg.fromName });
114
+ var from = sender.name ? { email: sender.email, name: sender.name } : sender.email;
115
+ await sgMail.send({ to: msg.to, from: from, subject: msg.subject, html: msg.html });
104
116
  }`
105
117
  case 'postmark':
106
118
  return `async function __sendProviderEmail(msg) {
107
119
  var postmark = require('postmark');
108
120
  var client = new postmark.ServerClient(msg.apiKey);
109
- await client.sendEmail({ From: msg.from, To: msg.to, Subject: msg.subject, HtmlBody: msg.html });
121
+ var sender = __emailSender({ from: msg.from, name: msg.fromName });
122
+ await client.sendEmail({ From: sender.header, To: msg.to, Subject: msg.subject, HtmlBody: msg.html });
110
123
  }`
111
124
  case 'mailgun':
112
125
  return `async function __sendProviderEmail(msg) {
@@ -116,7 +129,8 @@ const generateRawProviderSendFunction = (provider?: string | null): string => {
116
129
  if (!domain) { throw new Error('Mailgun domain not configured'); }
117
130
  var mailgun = new Mailgun(formData);
118
131
  var mg = mailgun.client({ username: 'api', key: msg.apiKey });
119
- await mg.messages.create(domain, { from: msg.from, to: [msg.to], subject: msg.subject, html: msg.html });
132
+ var sender = __emailSender({ from: msg.from, name: msg.fromName });
133
+ await mg.messages.create(domain, { from: sender.header, to: [msg.to], subject: msg.subject, html: msg.html });
120
134
  }`
121
135
  case 'mailersend':
122
136
  return `async function __sendProviderEmail(msg) {
@@ -125,11 +139,8 @@ const generateRawProviderSendFunction = (provider?: string | null): string => {
125
139
  var Sender = require('mailersend').Sender;
126
140
  var Recipient = require('mailersend').Recipient;
127
141
  var mailerSend = new MailerSend({ apiKey: msg.apiKey });
128
- var fromStr = String(msg.from || '').trim();
129
- var m = /^(.*)<([^<>]+)>\\s*$/.exec(fromStr);
130
- var fromEmail = m ? m[2].trim() : fromStr;
131
- var fromName = m ? m[1].trim() : '';
132
- var sender = new Sender(fromEmail, fromName || fromEmail);
142
+ var from = __emailSender({ from: msg.from, name: msg.fromName });
143
+ var sender = new Sender(from.email, from.name || from.email);
133
144
  var params = new EmailParams().setFrom(sender).setTo([new Recipient(msg.to)]).setSubject(msg.subject).setHtml(msg.html);
134
145
  await mailerSend.email.send(params);
135
146
  }`
@@ -783,6 +783,7 @@ export class NextWorkflowProjectPlugin implements ProjectPlugin {
783
783
  authUsersTableName: resolveAuthUsersTableName(uidl.authentication),
784
784
  emailProvider,
785
785
  fromEmail: typeof deleteConfig.from === 'string' ? deleteConfig.from : '',
786
+ fromName: typeof deleteConfig.fromName === 'string' ? deleteConfig.fromName : '',
786
787
  emailSecretEnvName,
787
788
  emailSubject: typeof deleteConfig.subject === 'string' ? deleteConfig.subject : '',
788
789
  emailBodyHtml: typeof deleteConfig.body === 'string' ? deleteConfig.body : '',
@@ -1807,6 +1808,7 @@ module.exports = __customNodeRegistry;
1807
1808
  const signupRouteCode = generateSignupRouteFile(auth, {
1808
1809
  emailProvider: welcomeProvider,
1809
1810
  fromEmail: typeof signupConfig.from === 'string' ? signupConfig.from : '',
1811
+ fromName: typeof signupConfig.fromName === 'string' ? signupConfig.fromName : '',
1810
1812
  emailSecretEnvName: welcomeSecretEnvName,
1811
1813
  emailSubject: typeof signupConfig.subject === 'string' ? signupConfig.subject : '',
1812
1814
  emailBodyHtml: typeof signupConfig.body === 'string' ? signupConfig.body : '',