@teleporthq/teleport-plugin-next-workflows 0.43.74 → 0.44.0

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 (148) hide show
  1. package/__tests__/booking-calendar-feed-route.test.ts +494 -0
  2. package/__tests__/custom-node-result-taken-branch.test.ts +107 -0
  3. package/__tests__/data-api-date-parser.test.ts +45 -0
  4. package/__tests__/emit-custom-event-channel.test.ts +13 -0
  5. package/__tests__/invoice-billing-details.test.ts +278 -0
  6. package/__tests__/middleware-admin-panel-roles.test.ts +127 -0
  7. package/__tests__/page-arrival-remount.test.ts +247 -0
  8. package/__tests__/payment-driver-stripe.test.ts +23 -0
  9. package/__tests__/payment-drivers-module.test.ts +19 -0
  10. package/__tests__/payment-subscription-nodes.test.ts +8 -0
  11. package/__tests__/raw-query-error-code.test.ts +47 -0
  12. package/__tests__/runtime-template-token-resolution.test.ts +16 -0
  13. package/__tests__/segment-reply-changed-keys.test.ts +31 -0
  14. package/__tests__/segment-route-skipped-branch.test.ts +101 -0
  15. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  16. package/dist/cjs/api-route-generator.js +23 -13
  17. package/dist/cjs/api-route-generator.js.map +1 -1
  18. package/dist/cjs/auth-generator.d.ts.map +1 -1
  19. package/dist/cjs/auth-generator.js +75 -27
  20. package/dist/cjs/auth-generator.js.map +1 -1
  21. package/dist/cjs/booking-calendar-feed-route-generator.d.ts +28 -0
  22. package/dist/cjs/booking-calendar-feed-route-generator.d.ts.map +1 -0
  23. package/dist/cjs/booking-calendar-feed-route-generator.js +296 -0
  24. package/dist/cjs/booking-calendar-feed-route-generator.js.map +1 -0
  25. package/dist/cjs/data-api-route-generator.d.ts +6 -8
  26. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  27. package/dist/cjs/data-api-route-generator.js +12 -16
  28. package/dist/cjs/data-api-route-generator.js.map +1 -1
  29. package/dist/cjs/executor-generator.d.ts.map +1 -1
  30. package/dist/cjs/executor-generator.js +30 -2
  31. package/dist/cjs/executor-generator.js.map +1 -1
  32. package/dist/cjs/index.d.ts +1 -1
  33. package/dist/cjs/index.d.ts.map +1 -1
  34. package/dist/cjs/index.js +2 -1
  35. package/dist/cjs/index.js.map +1 -1
  36. package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
  37. package/dist/cjs/invoice/invoice-assembly-code.js +46 -6
  38. package/dist/cjs/invoice/invoice-assembly-code.js.map +1 -1
  39. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  40. package/dist/cjs/invoice/invoice-html-code.js +10 -2
  41. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  42. package/dist/cjs/nodes/data/data-raw-query.d.ts.map +1 -1
  43. package/dist/cjs/nodes/data/data-raw-query.js +11 -1
  44. package/dist/cjs/nodes/data/data-raw-query.js.map +1 -1
  45. package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
  46. package/dist/cjs/nodes/general/general-custom-js.js +11 -0
  47. package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
  48. package/dist/cjs/nodes/general/general-emit-custom-event.d.ts.map +1 -1
  49. package/dist/cjs/nodes/general/general-emit-custom-event.js +3 -1
  50. package/dist/cjs/nodes/general/general-emit-custom-event.js.map +1 -1
  51. package/dist/cjs/page-arrival.d.ts +10 -0
  52. package/dist/cjs/page-arrival.d.ts.map +1 -0
  53. package/dist/cjs/page-arrival.js +86 -0
  54. package/dist/cjs/page-arrival.js.map +1 -0
  55. package/dist/cjs/payments/drivers/stripe-driver-code.d.ts.map +1 -1
  56. package/dist/cjs/payments/drivers/stripe-driver-code.js +11 -0
  57. package/dist/cjs/payments/drivers/stripe-driver-code.js.map +1 -1
  58. package/dist/cjs/payments/index.d.ts +10 -4
  59. package/dist/cjs/payments/index.d.ts.map +1 -1
  60. package/dist/cjs/payments/index.js +18 -7
  61. package/dist/cjs/payments/index.js.map +1 -1
  62. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  63. package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
  64. package/dist/cjs/workflow-component-plugin.js +35 -149
  65. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  66. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  67. package/dist/cjs/workflow-project-plugin.js +14 -4
  68. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  69. package/dist/cjs/workflow-scope.d.ts +19 -0
  70. package/dist/cjs/workflow-scope.d.ts.map +1 -0
  71. package/dist/cjs/workflow-scope.js +147 -0
  72. package/dist/cjs/workflow-scope.js.map +1 -0
  73. package/dist/esm/api-route-generator.d.ts.map +1 -1
  74. package/dist/esm/api-route-generator.js +23 -13
  75. package/dist/esm/api-route-generator.js.map +1 -1
  76. package/dist/esm/auth-generator.d.ts.map +1 -1
  77. package/dist/esm/auth-generator.js +75 -27
  78. package/dist/esm/auth-generator.js.map +1 -1
  79. package/dist/esm/booking-calendar-feed-route-generator.d.ts +28 -0
  80. package/dist/esm/booking-calendar-feed-route-generator.d.ts.map +1 -0
  81. package/dist/esm/booking-calendar-feed-route-generator.js +291 -0
  82. package/dist/esm/booking-calendar-feed-route-generator.js.map +1 -0
  83. package/dist/esm/data-api-route-generator.d.ts +6 -8
  84. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  85. package/dist/esm/data-api-route-generator.js +13 -17
  86. package/dist/esm/data-api-route-generator.js.map +1 -1
  87. package/dist/esm/executor-generator.d.ts.map +1 -1
  88. package/dist/esm/executor-generator.js +30 -2
  89. package/dist/esm/executor-generator.js.map +1 -1
  90. package/dist/esm/index.d.ts +1 -1
  91. package/dist/esm/index.d.ts.map +1 -1
  92. package/dist/esm/index.js +1 -1
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
  95. package/dist/esm/invoice/invoice-assembly-code.js +46 -6
  96. package/dist/esm/invoice/invoice-assembly-code.js.map +1 -1
  97. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  98. package/dist/esm/invoice/invoice-html-code.js +10 -2
  99. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  100. package/dist/esm/nodes/data/data-raw-query.d.ts.map +1 -1
  101. package/dist/esm/nodes/data/data-raw-query.js +11 -1
  102. package/dist/esm/nodes/data/data-raw-query.js.map +1 -1
  103. package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
  104. package/dist/esm/nodes/general/general-custom-js.js +11 -0
  105. package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
  106. package/dist/esm/nodes/general/general-emit-custom-event.d.ts.map +1 -1
  107. package/dist/esm/nodes/general/general-emit-custom-event.js +3 -1
  108. package/dist/esm/nodes/general/general-emit-custom-event.js.map +1 -1
  109. package/dist/esm/page-arrival.d.ts +10 -0
  110. package/dist/esm/page-arrival.d.ts.map +1 -0
  111. package/dist/esm/page-arrival.js +81 -0
  112. package/dist/esm/page-arrival.js.map +1 -0
  113. package/dist/esm/payments/drivers/stripe-driver-code.d.ts.map +1 -1
  114. package/dist/esm/payments/drivers/stripe-driver-code.js +11 -0
  115. package/dist/esm/payments/drivers/stripe-driver-code.js.map +1 -1
  116. package/dist/esm/payments/index.d.ts +10 -4
  117. package/dist/esm/payments/index.d.ts.map +1 -1
  118. package/dist/esm/payments/index.js +16 -6
  119. package/dist/esm/payments/index.js.map +1 -1
  120. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  121. package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
  122. package/dist/esm/workflow-component-plugin.js +32 -146
  123. package/dist/esm/workflow-component-plugin.js.map +1 -1
  124. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  125. package/dist/esm/workflow-project-plugin.js +14 -4
  126. package/dist/esm/workflow-project-plugin.js.map +1 -1
  127. package/dist/esm/workflow-scope.d.ts +19 -0
  128. package/dist/esm/workflow-scope.d.ts.map +1 -0
  129. package/dist/esm/workflow-scope.js +142 -0
  130. package/dist/esm/workflow-scope.js.map +1 -0
  131. package/package.json +4 -4
  132. package/src/api-route-generator.ts +23 -13
  133. package/src/auth-generator.ts +80 -25
  134. package/src/booking-calendar-feed-route-generator.ts +293 -0
  135. package/src/data-api-route-generator.ts +14 -19
  136. package/src/executor-generator.ts +30 -2
  137. package/src/index.ts +1 -0
  138. package/src/invoice/invoice-assembly-code.ts +46 -6
  139. package/src/invoice/invoice-html-code.ts +10 -2
  140. package/src/nodes/data/data-raw-query.ts +11 -1
  141. package/src/nodes/general/general-custom-js.ts +10 -0
  142. package/src/nodes/general/general-emit-custom-event.ts +3 -1
  143. package/src/page-arrival.ts +115 -0
  144. package/src/payments/drivers/stripe-driver-code.ts +11 -0
  145. package/src/payments/index.ts +18 -5
  146. package/src/workflow-component-plugin.ts +43 -188
  147. package/src/workflow-project-plugin.ts +15 -4
  148. package/src/workflow-scope.ts +184 -0
@@ -13,6 +13,7 @@ import {
13
13
  generateLocalizedEmailCopyFn,
14
14
  WELCOME_EMAIL_CONFIG_KEYS,
15
15
  } from './transactional-email-code'
16
+ import { TableAccess } from '@teleporthq/teleport-shared'
16
17
  import { generateSessionTokenResolverCode } from './session-cookie-resolver'
17
18
 
18
19
  // GUI provider id → the actual next-auth v4 provider MODULE name, for the few
@@ -1413,6 +1414,70 @@ const selfGuardedRoutePatternOf = (protection: {
1413
1414
  return base === '/' ? `/[${differentiator}]` : `${base}/[${differentiator}]`
1414
1415
  }
1415
1416
 
1417
+ /** The same-site path the project redirects `/` to, or null (absent, `/` itself, or off-site). */
1418
+ const resolveHomeRedirect = (auth: UIDLAuthentication): string | null => {
1419
+ const value = typeof auth.homeRedirect === 'string' ? auth.homeRedirect : ''
1420
+ const path = value.split('?')[0].split('#')[0]
1421
+ if (!path.startsWith('/') || path.startsWith('//') || path.indexOf('\\') !== -1) {
1422
+ return null
1423
+ }
1424
+ const normalized = path.length > 1 ? path.replace(/\/+$/, '') : path
1425
+ return normalized === '/' ? null : normalized
1426
+ }
1427
+
1428
+ /**
1429
+ * Where a signed-in visitor without the role a page asks for lands. A project
1430
+ * whose home page is not redirected keeps the code it always had. One that
1431
+ * redirects `/` (e.g. to an admin dashboard) only sends a visitor home when
1432
+ * that page admits them; otherwise to sign-in, saying why.
1433
+ */
1434
+ const generateRoleDeniedRedirectCode = (
1435
+ signInRoute: string,
1436
+ homeRedirect: string | null
1437
+ ): string => {
1438
+ if (!homeRedirect) {
1439
+ return `// Where to send an authenticated user who lacks the required role. Normally the
1440
+ // home page ("you don't have access, here's the public site"). But when "/" is
1441
+ // itself a protected page (e.g. an admin dashboard published at the root), a
1442
+ // redirect to "/" re-enters this middleware and loops forever — so fall back to
1443
+ // the sign-in page (with a callbackUrl) in that case.
1444
+ function roleDeniedRedirect(request, pathname) {
1445
+ if (pathname !== '/' && !protectedRoutes['/']) {
1446
+ return redirectWithinLocale(request, '/');
1447
+ }
1448
+ return redirectWithinLocale(request, '${signInRoute}', pathname);
1449
+ }`
1450
+ }
1451
+ return `// The project redirects "/" here before this middleware ever runs.
1452
+ var homeRedirect = ${JSON.stringify(homeRedirect)};
1453
+
1454
+ // Whether "/" is a way out for this visitor: the page it redirects to is not
1455
+ // the one being left, and admits their role. Otherwise sending them home would
1456
+ // land them right back on a page that refuses them — a redirect loop.
1457
+ function homeAdmitsRole(pathname, userRole) {
1458
+ var landing = normalizeMatchPath(homeRedirect);
1459
+ if (landing === normalizeMatchPath(pathname)) return false;
1460
+ var protection = resolveProtection(landing).matched;
1461
+ if (!protection) return true;
1462
+ if (protection.requiresSubscription) return false;
1463
+ var roles = protection.allowedRoles || [];
1464
+ return roles.length === 0 || roles.indexOf(userRole) >= 0;
1465
+ }
1466
+
1467
+ // Where to send an authenticated user who lacks the required role: home when
1468
+ // home admits them, else the sign-in page, which is told why (error=forbidden)
1469
+ // and keeps a callbackUrl for an account that may open the page.
1470
+ function roleDeniedRedirect(request, pathname, userRole) {
1471
+ if (pathname !== '/' && !protectedRoutes['/'] && homeAdmitsRole(pathname, userRole)) {
1472
+ return redirectWithinLocale(request, '/');
1473
+ }
1474
+ var target = new URL(localizePathname(request, '${signInRoute}'), request.url);
1475
+ target.searchParams.set('callbackUrl', localizePathname(request, pathname));
1476
+ target.searchParams.set('error', 'forbidden');
1477
+ return NextResponse.redirect(target);
1478
+ }`
1479
+ }
1480
+
1416
1481
  export const generateMiddlewareFile = (
1417
1482
  auth: UIDLAuthentication,
1418
1483
  options: MiddlewareOptions = {}
@@ -1506,20 +1571,19 @@ export const generateMiddlewareFile = (
1506
1571
  delete protectedRoutes[route]
1507
1572
  }
1508
1573
 
1509
- let sawAdminRoute = false
1510
- let adminRoles: string[] = ['admin']
1511
- for (const k of Object.keys(protectedRoutes)) {
1512
- if (k === '/admin' || k.indexOf('/admin/') === 0) {
1513
- sawAdminRoute = true
1514
- const ar = protectedRoutes[k]?.allowedRoles
1515
- if (ar && ar.length > 0) {
1516
- adminRoles = ar
1517
- break
1518
- }
1519
- }
1520
- }
1574
+ // `/admin` itself, and every admin page the map has no entry for (one an
1575
+ // owner added without protection), admit the admin panel's own roles — its
1576
+ // folder's — never the first admin route's, which may also admit another
1577
+ // role (a staff dashboard).
1578
+ const sawAdminRoute = Object.keys(protectedRoutes).some(
1579
+ (k) => k === '/admin' || k.indexOf('/admin/') === 0
1580
+ )
1521
1581
  if (sawAdminRoute && !protectedRoutes['/admin']) {
1522
- protectedRoutes['/admin'] = { requiresAuth: true, allowedRoles: adminRoles }
1582
+ const panelRoles = TableAccess.resolveAdminPanelRoles(auth)
1583
+ protectedRoutes['/admin'] = {
1584
+ requiresAuth: true,
1585
+ allowedRoles: panelRoles.length > 0 ? panelRoles : ['admin'],
1586
+ }
1523
1587
  }
1524
1588
 
1525
1589
  const protectedRoutesJson = JSON.stringify(protectedRoutes, null, 2)
@@ -1530,6 +1594,7 @@ export const generateMiddlewareFile = (
1530
1594
  options.subscriptionFallbackRoute && options.subscriptionFallbackRoute.startsWith('/')
1531
1595
  ? options.subscriptionFallbackRoute
1532
1596
  : '/'
1597
+ const homeRedirect = resolveHomeRedirect(auth)
1533
1598
 
1534
1599
  return `import { NextResponse } from 'next/server';
1535
1600
  import { getToken } from 'next-auth/jwt';
@@ -1642,17 +1707,7 @@ function redirectWithinLocale(request, pathname, callbackPath) {
1642
1707
  return NextResponse.redirect(target);
1643
1708
  }
1644
1709
 
1645
- // Where to send an authenticated user who lacks the required role. Normally the
1646
- // home page ("you don't have access, here's the public site"). But when "/" is
1647
- // itself a protected page (e.g. an admin dashboard published at the root), a
1648
- // redirect to "/" re-enters this middleware and loops forever — so fall back to
1649
- // the sign-in page (with a callbackUrl) in that case.
1650
- function roleDeniedRedirect(request, pathname) {
1651
- if (pathname !== '/' && !protectedRoutes['/']) {
1652
- return redirectWithinLocale(request, '/');
1653
- }
1654
- return redirectWithinLocale(request, '${signInRoute}', pathname);
1655
- }
1710
+ ${generateRoleDeniedRedirectCode(signInRoute, homeRedirect)}
1656
1711
 
1657
1712
  // Route keys carry no trailing slash, so neither may the path we match with.
1658
1713
  // Next.js serves "/orders/ORD-42/" and "/orders/ORD-42" as the same route;
@@ -1839,7 +1894,7 @@ async function middleware(request) {
1839
1894
  }
1840
1895
  var userRole = getUserRoleFromToken(sessionUser);
1841
1896
  if (userRole == null || allowedRoles.indexOf(userRole) < 0) {
1842
- return roleDeniedRedirect(request, pathname);
1897
+ return roleDeniedRedirect(request, pathname${homeRedirect ? ', userRole' : ''});
1843
1898
  }
1844
1899
  }
1845
1900
 
@@ -0,0 +1,293 @@
1
+ /**
2
+ * The bookings' calendar feeds: `pages/api/bookings/ical/[file].js`, emitted
3
+ * when the UIDL carries `bookingCalendars.feedStatement`.
4
+ *
5
+ * Each calendar link of a site's bookings is an iCalendar (RFC 5545) feed that
6
+ * Airbnb, Booking.com, Vrbo or Google Calendar import to block the dates the
7
+ * site has booked. The editor builds the one statement behind every feed (its
8
+ * tables and columns are the editor's business); the generated route only runs
9
+ * it with the link's key and writes the rows out as a calendar.
10
+ */
11
+ import { FileType, ProjectPluginStructure } from '@teleporthq/teleport-types'
12
+ import { generatePgClientCode } from './pg-client-code'
13
+ import { getDatabaseDriverDependencies } from './auth-generator'
14
+
15
+ /**
16
+ * The route's source (CommonJS). GET or HEAD `/api/bookings/ical/<key>.ics`:
17
+ * - another method → 405 with `Allow: GET, HEAD`;
18
+ * - a key of the wrong shape, or one the statement returns no calendar row
19
+ * for → 404 (the key is the link's only credential, so both look the same);
20
+ * - a failed query → 500 'Calendar unavailable', the cause logged only;
21
+ * - else 200 with the calendar, never cached. An event row with a missing or
22
+ * invalid value is left out rather than failing the whole feed.
23
+ */
24
+ export const generateBookingCalendarFeedRoute = (feedStatement: string): string => {
25
+ return `'use strict';
26
+
27
+ // GENERATED — see generateBookingCalendarFeedRoute in
28
+ // @teleporthq/teleport-plugin-next-workflows/src/booking-calendar-feed-route-generator.ts.
29
+ //
30
+ // The iCalendar feed of one calendar link of the site's bookings: Airbnb,
31
+ // Booking.com, Vrbo or Google Calendar poll it and block the booked dates.
32
+ ${generatePgClientCode()}
33
+
34
+ // Built by the editor: $1 is the link's key. It returns one 'calendar' row and
35
+ // one 'event' row per blocked period.
36
+ var FEED_STATEMENT = ${JSON.stringify(feedStatement)};
37
+ var FEED_FILE_RE = /^([A-Za-z0-9_-]{20,80})[.]ics$/;
38
+ var DATE_RE = /^([0-9]{4})([0-9]{2})([0-9]{2})$/;
39
+ var DATE_TIME_RE = /^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z$/;
40
+ var TEXT_TYPE = 'text/plain; charset=utf-8';
41
+ var CALENDAR_TYPE = 'text/calendar; charset=utf-8';
42
+ var CRLF = String.fromCharCode(13, 10);
43
+ var BACKSLASH = String.fromCharCode(92);
44
+ // RFC 5545: a content line is at most 75 octets, the line break excluded.
45
+ var MAX_LINE_OCTETS = 75;
46
+
47
+ // RFC 5545 TEXT: a backslash, semicolon or comma is escaped with a backslash
48
+ // and every line break is written as the two characters backslash and n.
49
+ // Other control characters are not allowed in TEXT, so they are dropped.
50
+ function icalText(value) {
51
+ var text = String(value == null ? '' : value);
52
+ var out = '';
53
+ for (var i = 0; i < text.length; i++) {
54
+ var ch = text.charAt(i);
55
+ var code = text.charCodeAt(i);
56
+ if (ch === BACKSLASH || ch === ';' || ch === ',') {
57
+ out += BACKSLASH + ch;
58
+ } else if (code === 13 || code === 10) {
59
+ // CRLF, a lone CR and a lone LF are each one line break.
60
+ if (code === 13 && text.charCodeAt(i + 1) === 10) {
61
+ i += 1;
62
+ }
63
+ out += BACKSLASH + 'n';
64
+ } else if (code === 9 || (code >= 32 && code !== 127)) {
65
+ out += ch;
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+
71
+ // One content line, folded: a continuation line starts with one space (which
72
+ // counts toward its 75 octets), and no fold splits a UTF-8 character.
73
+ function icalFold(line) {
74
+ var out = '';
75
+ var used = 0;
76
+ for (var i = 0; i < line.length; i++) {
77
+ var ch = line.charAt(i);
78
+ var code = line.charCodeAt(i);
79
+ var octets = code < 0x80 ? 1 : code < 0x800 ? 2 : 3;
80
+ if (code >= 0xd800 && code <= 0xdbff) {
81
+ var low = line.charCodeAt(i + 1);
82
+ if (low >= 0xdc00 && low <= 0xdfff) {
83
+ // A surrogate pair is one 4-octet character.
84
+ ch = line.slice(i, i + 2);
85
+ octets = 4;
86
+ i += 1;
87
+ }
88
+ }
89
+ if (used + octets > MAX_LINE_OCTETS) {
90
+ out += CRLF + ' ';
91
+ used = 1;
92
+ }
93
+ out += ch;
94
+ used += octets;
95
+ }
96
+ return out + CRLF;
97
+ }
98
+
99
+ // all_day as Postgres hands it over: a boolean, or 't' / 'f' / 'true' / 'false'.
100
+ // null for anything else.
101
+ function icalAllDay(value) {
102
+ if (value === true || value === false) {
103
+ return value;
104
+ }
105
+ var text = String(value == null ? '' : value).trim().toLowerCase();
106
+ if (text === 't' || text === 'true') {
107
+ return true;
108
+ }
109
+ if (text === 'f' || text === 'false') {
110
+ return false;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ // 'YYYYMMDD' (all day) or 'YYYYMMDDTHHMMSSZ' naming a real day and time, else
116
+ // null — a value like 20260231 would otherwise reach the importing calendar.
117
+ function icalDateValue(value, allDay) {
118
+ if (typeof value !== 'string') {
119
+ return null;
120
+ }
121
+ var text = value.trim();
122
+ var match = (allDay ? DATE_RE : DATE_TIME_RE).exec(text);
123
+ if (!match) {
124
+ return null;
125
+ }
126
+ var parts = [];
127
+ for (var i = 1; i <= 6; i++) {
128
+ parts.push(allDay && i > 3 ? 0 : Number(match[i]));
129
+ }
130
+ var date = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]));
131
+ var actual = [
132
+ date.getUTCFullYear(),
133
+ date.getUTCMonth() + 1,
134
+ date.getUTCDate(),
135
+ date.getUTCHours(),
136
+ date.getUTCMinutes(),
137
+ date.getUTCSeconds(),
138
+ ];
139
+ return actual.join(',') === parts.join(',') ? text : null;
140
+ }
141
+
142
+ // The VEVENT lines of one event row, or null when a value is missing or invalid.
143
+ function icalEventLines(row, stamp) {
144
+ var uid = row.uid == null ? '' : String(row.uid).trim();
145
+ var allDay = icalAllDay(row.all_day);
146
+ if (uid === '' || allDay === null) {
147
+ return null;
148
+ }
149
+ var start = icalDateValue(row.start_value, allDay);
150
+ var end = icalDateValue(row.end_value, allDay);
151
+ // The end is exclusive, so it must come after the start. Two values of the
152
+ // same format compare correctly as strings.
153
+ if (start === null || end === null || end <= start) {
154
+ return null;
155
+ }
156
+ var valueParam = allDay ? ';VALUE=DATE:' : ':';
157
+ return [
158
+ 'BEGIN:VEVENT',
159
+ 'UID:' + icalText(uid),
160
+ 'DTSTAMP:' + stamp,
161
+ 'DTSTART' + valueParam + start,
162
+ 'DTEND' + valueParam + end,
163
+ 'SUMMARY:' + icalText(row.summary),
164
+ 'TRANSP:OPAQUE',
165
+ 'END:VEVENT',
166
+ ];
167
+ }
168
+
169
+ // 'YYYYMMDDTHHMMSSZ' of an instant, in UTC.
170
+ function icalStamp(date) {
171
+ var iso = date.toISOString();
172
+ return (
173
+ iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10) + 'T' +
174
+ iso.slice(11, 13) + iso.slice(14, 16) + iso.slice(17, 19) + 'Z'
175
+ );
176
+ }
177
+
178
+ function icalCalendar(calendarRow, rows, now) {
179
+ var stamp = icalStamp(now);
180
+ var lines = [
181
+ 'BEGIN:VCALENDAR',
182
+ 'VERSION:2.0',
183
+ 'PRODID:-//TeleportHQ//Bookings//EN',
184
+ 'CALSCALE:GREGORIAN',
185
+ 'METHOD:PUBLISH',
186
+ 'X-WR-CALNAME:' + icalText(calendarRow.calendar_name),
187
+ 'REFRESH-INTERVAL;VALUE=DURATION:PT30M',
188
+ 'X-PUBLISHED-TTL:PT30M',
189
+ ];
190
+ for (var i = 0; i < rows.length; i++) {
191
+ if (rows[i] && rows[i].kind === 'event') {
192
+ var eventLines = icalEventLines(rows[i], stamp);
193
+ if (eventLines) {
194
+ lines = lines.concat(eventLines);
195
+ }
196
+ }
197
+ }
198
+ lines.push('END:VCALENDAR');
199
+ var body = '';
200
+ for (var j = 0; j < lines.length; j++) {
201
+ body += icalFold(lines[j]);
202
+ }
203
+ return body;
204
+ }
205
+
206
+ // Never cached: a booking can block a date between two polls. A HEAD request
207
+ // gets the headers GET would, without the body.
208
+ function icalSend(res, status, contentType, body, headOnly) {
209
+ res.statusCode = status;
210
+ res.setHeader('Content-Type', contentType);
211
+ res.setHeader('Cache-Control', 'no-store');
212
+ res.setHeader('Content-Length', String(Buffer.byteLength(body, 'utf8')));
213
+ if (headOnly) {
214
+ res.end();
215
+ } else {
216
+ res.end(body);
217
+ }
218
+ }
219
+
220
+ module.exports = async function handler(req, res) {
221
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
222
+ res.setHeader('Allow', 'GET, HEAD');
223
+ return icalSend(res, 405, TEXT_TYPE, 'Method not allowed', false);
224
+ }
225
+ var headOnly = req.method === 'HEAD';
226
+ var file = req.query && typeof req.query.file === 'string' ? req.query.file : '';
227
+ var fileMatch = FEED_FILE_RE.exec(file);
228
+ if (!fileMatch) {
229
+ return icalSend(res, 404, TEXT_TYPE, 'Not found', headOnly);
230
+ }
231
+
232
+ var rows = null;
233
+ var client = null;
234
+ try {
235
+ client = getClient();
236
+ await client.connect();
237
+ var result = await client.query(FEED_STATEMENT, [fileMatch[1]]);
238
+ rows = result && Array.isArray(result.rows) ? result.rows : [];
239
+ } catch (e) {
240
+ console.error('[bookings-ical] calendar feed query failed:', e && e.message ? e.message : e);
241
+ } finally {
242
+ if (client) {
243
+ try { await client.end(); } catch (e) {}
244
+ }
245
+ }
246
+ if (rows === null) {
247
+ return icalSend(res, 500, TEXT_TYPE, 'Calendar unavailable', headOnly);
248
+ }
249
+
250
+ var calendarRow = null;
251
+ for (var i = 0; i < rows.length && calendarRow === null; i++) {
252
+ if (rows[i] && rows[i].kind === 'calendar') {
253
+ calendarRow = rows[i];
254
+ }
255
+ }
256
+ if (calendarRow === null) {
257
+ return icalSend(res, 404, TEXT_TYPE, 'Not found', headOnly);
258
+ }
259
+ res.setHeader('Content-Disposition', 'inline; filename="' + file + '"');
260
+ return icalSend(res, 200, CALENDAR_TYPE, icalCalendar(calendarRow, rows, new Date()), headOnly);
261
+ };
262
+ `
263
+ }
264
+
265
+ /**
266
+ * Emits the feed route when the UIDL names a feed statement, plus the `pg`
267
+ * driver it connects with (already present on a project with a Teleport
268
+ * database, which every bookings site has).
269
+ */
270
+ export const generateBookingCalendarFeedFiles = (structure: ProjectPluginStructure): void => {
271
+ const feedStatement = structure.uidl.bookingCalendars?.feedStatement
272
+ if (typeof feedStatement !== 'string' || feedStatement.trim() === '') {
273
+ return
274
+ }
275
+
276
+ structure.files.set('bookings-ical-feed-route', {
277
+ path: ['pages', 'api', 'bookings', 'ical'],
278
+ files: [
279
+ {
280
+ name: '[file]',
281
+ fileType: FileType.JS,
282
+ content: generateBookingCalendarFeedRoute(feedStatement),
283
+ },
284
+ ],
285
+ })
286
+
287
+ const driverDependencies = getDatabaseDriverDependencies('teleport')
288
+ for (const [pkg, version] of Object.entries(driverDependencies)) {
289
+ if (!structure.dependencies[pkg]) {
290
+ structure.dependencies[pkg] = version
291
+ }
292
+ }
293
+ }
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { generateSqlValidatorCode } from './sql-validator'
15
15
  import { generateCommonJsSessionTokenResolverCode } from './session-cookie-resolver'
16
- import { TableAccess } from '@teleporthq/teleport-shared'
16
+ import { ContentTables, TableAccess } from '@teleporthq/teleport-shared'
17
17
 
18
18
  const DATA_NODE_TYPES = new Set([
19
19
  'data-select',
@@ -79,28 +79,18 @@ export const BROWSER_READABLE_TABLES: ReadonlyArray<string> = [
79
79
  'teleport_shipping_rates',
80
80
  'teleport_tax_rates',
81
81
  'teleport_store_locations',
82
- 'teleport_blog_posts',
82
+ ...ContentTables.CONTENT_POSTS_TABLES,
83
83
  ]
84
84
 
85
85
  /**
86
86
  * What a browser is served of the readable tables whose unpublished rows sit
87
- * beside the public ones — the same policy the per-table read routes apply
88
- * (teleport-plugin-next-data-source `browser-row-policy.ts`, which this mirrors;
89
- * keep the two in step). A browser may not filter or sort by a hidden column:
90
- * a filter is an oracle for a column it never returns.
87
+ * beside the public ones — the same map the per-table read routes apply
88
+ * (teleport-plugin-next-data-source `browser-row-policy.ts`): both read it from
89
+ * teleport-shared. A browser may not filter or sort by a hidden column: a
90
+ * filter is an oracle for a column it never returns.
91
91
  */
92
- export const BROWSER_ROW_POLICIES: Readonly<
93
- Record<string, { predicate: string; hiddenColumns: ReadonlyArray<string> }>
94
- > = {
95
- teleport_blog_posts: {
96
- predicate: "status = 'published'",
97
- hiddenColumns: ['author_email', 'authorEmail'],
98
- },
99
- teleport_products: {
100
- predicate: "LOWER(TRIM(status)) = 'active'",
101
- hiddenColumns: [],
102
- },
103
- }
92
+ export const BROWSER_ROW_POLICIES: Readonly<Record<string, ContentTables.VisitorRowPolicy>> =
93
+ ContentTables.VISITOR_ROW_POLICIES
104
94
 
105
95
  export const generateDataAPIRoute = (options: DataAPIRouteOptions = {}): string => {
106
96
  const validatorCode = generateSqlValidatorCode()
@@ -111,7 +101,12 @@ export const generateDataAPIRoute = (options: DataAPIRouteOptions = {}): string
111
101
  ? options.lowStockThreshold
112
102
  : 5
113
103
 
114
- return `const { Client } = require('pg');
104
+ return `const { Client, types: pgTypes } = require('pg');
105
+ // A DATE stays the day it names, whatever the server's time zone: node-pg would read it as
106
+ // local midnight, which a server east of UTC sends as the day before.
107
+ if (pgTypes && typeof pgTypes.setTypeParser === 'function') {
108
+ pgTypes.setTypeParser(1082, (value) => value);
109
+ }
115
110
  ${
116
111
  // The session cookie is resolved from the REQUEST rather than from
117
112
  // process.env.NEXTAUTH_URL — see session-cookie-resolver.ts. Reading it the
@@ -387,8 +387,19 @@ function applyTemplateParams(text, params) {
387
387
  function finalizeResolvedConfig(nodeType, config) {
388
388
  var isDataNode = typeof nodeType === 'string' && nodeType.indexOf('data-') === 0;
389
389
  var error = null;
390
+ // A config value may be a live element (a trigger's target handed to a
391
+ // custom-js node) or an object that refers back to itself: walking into a
392
+ // DOM node follows its React fibers forever (a page's every element-bound
393
+ // behavior died with "Maximum call stack size exceeded"). Elements are
394
+ // left as they are; an object is walked once.
395
+ var seen = typeof WeakSet === 'function' ? new WeakSet() : null;
390
396
  function walk(obj, insideFilters) {
391
397
  if (!obj || typeof obj !== 'object') return;
398
+ if (typeof obj.nodeType === 'number' && typeof obj.nodeName === 'string') return;
399
+ if (seen) {
400
+ if (seen.has(obj)) return;
401
+ seen.add(obj);
402
+ }
392
403
  if (Array.isArray(obj)) {
393
404
  for (var ai = 0; ai < obj.length; ai++) walk(obj[ai], insideFilters);
394
405
  return;
@@ -1398,15 +1409,31 @@ var SEGMENT_REPLY_PRIVATE_KEYS = {
1398
1409
  // be echoed back wholesale (every earlier node's result, the trigger, the whole
1399
1410
  // page state), so each round trip carried the page twice over and the network
1400
1411
  // tab showed every node's data on every hop.
1412
+ //
1413
+ // A node marked \`keepResultOnServer\` is the exception: its result serves the
1414
+ // nodes after it in the same segment and never leaves the server (a statement's
1415
+ // rows read only to build what the browser may see, a sender's recipients).
1416
+ // Nothing in a later segment can read it.
1417
+ // The nodes whose result the client branches on: never kept on the server.
1418
+ function isBranchingNodeType(type) {
1419
+ return type === 'general-if-statement' || type === 'general-switch';
1420
+ }
1421
+
1401
1422
  function segmentReply(segmentConfig, context, snapshot) {
1402
1423
  var reply = {};
1403
1424
  var own = {};
1425
+ var kept = {};
1404
1426
  var nodes = (segmentConfig && segmentConfig.nodes) || [];
1405
- for (var ni = 0; ni < nodes.length; ni++) own[nodes[ni].id] = true;
1427
+ for (var ni = 0; ni < nodes.length; ni++) {
1428
+ own[nodes[ni].id] = true;
1429
+ if (nodes[ni].config && nodes[ni].config.keepResultOnServer === true && !isBranchingNodeType(nodes[ni].type)) {
1430
+ kept[nodes[ni].id] = true;
1431
+ }
1432
+ }
1406
1433
  var keys = Object.keys(context);
1407
1434
  for (var i = 0; i < keys.length; i++) {
1408
1435
  var key = keys[i];
1409
- if (SEGMENT_REPLY_PRIVATE_KEYS[key]) continue;
1436
+ if (SEGMENT_REPLY_PRIVATE_KEYS[key] || kept[key]) continue;
1410
1437
  var value = context[key];
1411
1438
  if (value === undefined) continue;
1412
1439
  if (own[key] || !snapshot || !Object.prototype.hasOwnProperty.call(snapshot, key)) {
@@ -1497,6 +1524,7 @@ module.exports = {
1497
1524
  fatalNodeResultMessage,
1498
1525
  continuesOnError,
1499
1526
  isFireAndForgetNode,
1527
+ isBranchingNodeType,
1500
1528
  startFireAndForgetNode,
1501
1529
  registerPendingNodePromise,
1502
1530
  settlePendingNodePromises
package/src/index.ts CHANGED
@@ -57,6 +57,7 @@ export { generateWebhookFiles } from './webhook-generator'
57
57
  export {
58
58
  declarePaymentCredentialEnv,
59
59
  ensurePaymentDriversModule,
60
+ readUidlPaymentProviders,
60
61
  resolveStorePaymentDriverIds,
61
62
  } from './payments'
62
63
  export {
@@ -33,6 +33,34 @@ var TEMPLATE_DOCUMENT = ${templateDocumentJson};
33
33
  ${REGIONAL_INVOICE_TAX_CODE}
34
34
  ${ProductOptions.generateProductOptionsHelperCode()}
35
35
 
36
+ // The invoice details an order's metadata (JSON text) carries beside its
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.
40
+ function readOrderBillingMetadata(raw) {
41
+ var parsed = raw;
42
+ if (typeof raw === 'string') {
43
+ try {
44
+ parsed = JSON.parse(raw);
45
+ } catch (e) {
46
+ parsed = null;
47
+ }
48
+ }
49
+ var meta = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
50
+ function part(key) {
51
+ var value = meta[key];
52
+ return value === null || value === undefined ? '' : String(value).trim();
53
+ }
54
+ return {
55
+ taxId: part('billingTaxId'),
56
+ street: part('billingStreet'),
57
+ city: part('billingCity'),
58
+ postalCode: part('billingPostalCode'),
59
+ region: part('billingRegion'),
60
+ country: part('billingCountry'),
61
+ };
62
+ }
63
+
36
64
  // The product options an order line was bought with: the short one-line label
37
65
  // (after the name) and every answer in full, one per line (the description).
38
66
  // Same rule as the order emails: a current snapshot names its options, while
@@ -416,15 +444,27 @@ function assembleInvoiceData(body, hydratedOrder, hydratedItems) {
416
444
  // field but the order has one, use it. Kept permissive on field
417
445
  // names (billing_* wins, falls back to shipping_*) because a buyer
418
446
  // who checked "bill to a different address" has both on the order.
447
+ //
448
+ // 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.
419
453
  var orderRow = orderShippingSource;
454
+ var orderBilling = readOrderBillingMetadata(orderRow.metadata);
455
+ var billedApart = !!(orderBilling.street || orderBilling.city || orderBilling.country);
420
456
  var fallbackCustomerName =
421
457
  orderRow.billing_name || orderRow.shipping_name || orderRow.customer_name || '';
422
458
  var fallbackCustomerEmail = orderRow.billing_email || orderRow.customer_email || '';
423
- var fallbackCustomerAddress = orderRow.billing_address || orderRow.shipping_address || '';
424
- var fallbackCustomerCity = orderRow.shipping_city || '';
425
- var fallbackCustomerState = orderRow.shipping_state || '';
426
- var fallbackCustomerZip = orderRow.shipping_zip || '';
427
- var fallbackCustomerCountry = orderRow.shipping_country || '';
459
+ var fallbackCustomerAddress = billedApart
460
+ ? orderBilling.street
461
+ : orderRow.billing_address || orderRow.shipping_address || '';
462
+ var fallbackCustomerCity = billedApart ? orderBilling.city : orderRow.shipping_city || '';
463
+ var fallbackCustomerState = billedApart ? orderBilling.region : orderRow.shipping_state || '';
464
+ var fallbackCustomerZip = billedApart ? orderBilling.postalCode : orderRow.shipping_zip || '';
465
+ var fallbackCustomerCountry = billedApart
466
+ ? orderBilling.country
467
+ : orderRow.shipping_country || '';
428
468
  var fallbackPaymentMethod = orderRow.payment_method || '';
429
469
  var fallbackPaymentProvider = orderRow.payment_provider || '';
430
470
  var fallbackPaymentIntentId = orderRow.payment_intent_id || '';
@@ -446,7 +486,7 @@ function assembleInvoiceData(body, hydratedOrder, hydratedItems) {
446
486
  customerState: body.customerState || fallbackCustomerState || '',
447
487
  customerZip: body.customerZip || fallbackCustomerZip || '',
448
488
  customerCountry: body.customerCountry || fallbackCustomerCountry || '',
449
- customerVat: body.customerVat || '',
489
+ customerVat: body.customerVat || orderBilling.taxId || '',
450
490
  companyName: pdfGenerator.COMPANY_DETAILS.companyName || '',
451
491
  companyAddress: pdfGenerator.COMPANY_DETAILS.companyAddress || '',
452
492
  companyCity: pdfGenerator.COMPANY_DETAILS.companyCity || '',
@@ -355,6 +355,9 @@ function buildInvoiceDataScope(invoiceData) {
355
355
  // sample — right for a company address, wrong for a code or a card tail: an
356
356
  // order without a voucher must not print the designer's "SAVE20".
357
357
  var realInvoiceNeverSampled = { voucherCode: voucherCode, giftCardLast4: giftCardLast4 };
358
+ // The buyer and their payment are never sampled either: an order with no
359
+ // city (a booking, a pickup, a download) must not print the designer's
360
+ // sample customer's, nor a card it was not paid with.
358
361
 
359
362
  var realCustomer = {
360
363
  name: invoiceData.customerName || '',
@@ -366,6 +369,11 @@ function buildInvoiceDataScope(invoiceData) {
366
369
  country: invoiceData.customerCountry || '',
367
370
  vat: invoiceData.customerVat || '',
368
371
  };
372
+ // The "Bill to" lines a buyer may leave out show only when they did not
373
+ // (string flags: the template compares strings, as with \`hasShipping\`).
374
+ realCustomer.hasZip = realCustomer.zip ? 'true' : 'false';
375
+ realCustomer.hasCountry = realCustomer.country ? 'true' : 'false';
376
+ realCustomer.hasVat = realCustomer.vat ? 'true' : 'false';
369
377
 
370
378
  var realPayment = {
371
379
  method: invoiceData.paymentMethod || '',
@@ -462,8 +470,8 @@ function buildInvoiceDataScope(invoiceData) {
462
470
  invoiceData: {
463
471
  Company: mergeWithDefaults(realCompany, defaults.Company),
464
472
  Invoice: Object.assign(mergeWithDefaults(realInvoice, defaults.Invoice), realInvoiceNeverSampled),
465
- Customer: mergeWithDefaults(realCustomer, defaults.Customer),
466
- Payment: mergeWithDefaults(realPayment, defaults.Payment),
473
+ Customer: realCustomer,
474
+ Payment: realPayment,
467
475
  Products: realProducts.length > 0 ? realProducts : (defaults.Products || []),
468
476
  },
469
477
  };