@teleporthq/teleport-plugin-next-workflows 0.43.41 → 0.43.44

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 (180) hide show
  1. package/__tests__/cart-availability-add-to-cart.test.ts +47 -0
  2. package/__tests__/component-scope-trigger-routing.test.ts +130 -0
  3. package/__tests__/cron-route-loop-templateparams.test.ts +144 -0
  4. package/__tests__/data-api-on-conflict-do-nothing.test.ts +35 -0
  5. package/__tests__/email-body-context-resolution.test.ts +126 -0
  6. package/__tests__/page-loaded-scoping-skip.test.ts +97 -0
  7. package/__tests__/raw-sql-param-binding.test.ts +138 -0
  8. package/__tests__/route-changed-trigger.test.ts +106 -0
  9. package/__tests__/state-update-missing-setter-warn.test.ts +180 -0
  10. package/dist/cjs/account-delete-route-generator.d.ts +13 -0
  11. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -0
  12. package/dist/cjs/account-delete-route-generator.js +247 -0
  13. package/dist/cjs/account-delete-route-generator.js.map +1 -0
  14. package/dist/cjs/api-route-generator.d.ts +1 -1
  15. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  16. package/dist/cjs/api-route-generator.js +29 -33
  17. package/dist/cjs/api-route-generator.js.map +1 -1
  18. package/dist/cjs/auth-generator.d.ts +9 -1
  19. package/dist/cjs/auth-generator.d.ts.map +1 -1
  20. package/dist/cjs/auth-generator.js +53 -2
  21. package/dist/cjs/auth-generator.js.map +1 -1
  22. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  23. package/dist/cjs/data-api-route-generator.js +11 -2
  24. package/dist/cjs/data-api-route-generator.js.map +1 -1
  25. package/dist/cjs/ecommerce/cart-availability.d.ts.map +1 -1
  26. package/dist/cjs/ecommerce/cart-availability.js +49 -2
  27. package/dist/cjs/ecommerce/cart-availability.js.map +1 -1
  28. package/dist/cjs/ecommerce/stock-decrement.d.ts +1 -1
  29. package/dist/cjs/ecommerce/stock-decrement.d.ts.map +1 -1
  30. package/dist/cjs/ecommerce/stock-decrement.js +48 -10
  31. package/dist/cjs/ecommerce/stock-decrement.js.map +1 -1
  32. package/dist/cjs/executor-generator.d.ts.map +1 -1
  33. package/dist/cjs/executor-generator.js +62 -0
  34. package/dist/cjs/executor-generator.js.map +1 -1
  35. package/dist/cjs/graph-utils.d.ts.map +1 -1
  36. package/dist/cjs/graph-utils.js +1 -0
  37. package/dist/cjs/graph-utils.js.map +1 -1
  38. package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
  39. package/dist/cjs/invoice/api-routes-code.js +2 -0
  40. package/dist/cjs/invoice/api-routes-code.js.map +1 -1
  41. package/dist/cjs/invoice/data-access-code.d.ts.map +1 -1
  42. package/dist/cjs/invoice/data-access-code.js +2 -0
  43. package/dist/cjs/invoice/data-access-code.js.map +1 -1
  44. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  45. package/dist/cjs/invoice/invoice-html-code.js +22 -2
  46. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  47. package/dist/cjs/nodes/account/account-delete-current.d.ts +3 -0
  48. package/dist/cjs/nodes/account/account-delete-current.d.ts.map +1 -0
  49. package/dist/cjs/nodes/account/account-delete-current.js +79 -0
  50. package/dist/cjs/nodes/account/account-delete-current.js.map +1 -0
  51. package/dist/cjs/nodes/account/account-signup.d.ts.map +1 -1
  52. package/dist/cjs/nodes/account/account-signup.js +20 -1
  53. package/dist/cjs/nodes/account/account-signup.js.map +1 -1
  54. package/dist/cjs/nodes/data/data-create-item.d.ts.map +1 -1
  55. package/dist/cjs/nodes/data/data-create-item.js +7 -0
  56. package/dist/cjs/nodes/data/data-create-item.js.map +1 -1
  57. package/dist/cjs/nodes/index.d.ts.map +1 -1
  58. package/dist/cjs/nodes/index.js +2 -1
  59. package/dist/cjs/nodes/index.js.map +1 -1
  60. package/dist/cjs/nodes/payment/payment-charge-user.d.ts.map +1 -1
  61. package/dist/cjs/nodes/payment/payment-charge-user.js +9 -1
  62. package/dist/cjs/nodes/payment/payment-charge-user.js.map +1 -1
  63. package/dist/cjs/pg-client-code.d.ts +11 -0
  64. package/dist/cjs/pg-client-code.d.ts.map +1 -0
  65. package/dist/cjs/pg-client-code.js +75 -0
  66. package/dist/cjs/pg-client-code.js.map +1 -0
  67. package/dist/cjs/raw-sql-param-binding.d.ts +82 -0
  68. package/dist/cjs/raw-sql-param-binding.d.ts.map +1 -0
  69. package/dist/cjs/raw-sql-param-binding.js +321 -0
  70. package/dist/cjs/raw-sql-param-binding.js.map +1 -0
  71. package/dist/cjs/transactional-email-code.d.ts +6 -0
  72. package/dist/cjs/transactional-email-code.d.ts.map +1 -0
  73. package/dist/cjs/transactional-email-code.js +112 -0
  74. package/dist/cjs/transactional-email-code.js.map +1 -0
  75. package/dist/cjs/trigger-generator.d.ts.map +1 -1
  76. package/dist/cjs/trigger-generator.js +29 -0
  77. package/dist/cjs/trigger-generator.js.map +1 -1
  78. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  79. package/dist/cjs/workflow-component-plugin.js +60 -2
  80. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  81. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  82. package/dist/cjs/workflow-project-plugin.js +188 -28
  83. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  84. package/dist/esm/account-delete-route-generator.d.ts +13 -0
  85. package/dist/esm/account-delete-route-generator.d.ts.map +1 -0
  86. package/dist/esm/account-delete-route-generator.js +243 -0
  87. package/dist/esm/account-delete-route-generator.js.map +1 -0
  88. package/dist/esm/api-route-generator.d.ts +1 -1
  89. package/dist/esm/api-route-generator.d.ts.map +1 -1
  90. package/dist/esm/api-route-generator.js +29 -33
  91. package/dist/esm/api-route-generator.js.map +1 -1
  92. package/dist/esm/auth-generator.d.ts +9 -1
  93. package/dist/esm/auth-generator.d.ts.map +1 -1
  94. package/dist/esm/auth-generator.js +53 -2
  95. package/dist/esm/auth-generator.js.map +1 -1
  96. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  97. package/dist/esm/data-api-route-generator.js +11 -2
  98. package/dist/esm/data-api-route-generator.js.map +1 -1
  99. package/dist/esm/ecommerce/cart-availability.d.ts.map +1 -1
  100. package/dist/esm/ecommerce/cart-availability.js +49 -2
  101. package/dist/esm/ecommerce/cart-availability.js.map +1 -1
  102. package/dist/esm/ecommerce/stock-decrement.d.ts +1 -1
  103. package/dist/esm/ecommerce/stock-decrement.d.ts.map +1 -1
  104. package/dist/esm/ecommerce/stock-decrement.js +48 -10
  105. package/dist/esm/ecommerce/stock-decrement.js.map +1 -1
  106. package/dist/esm/executor-generator.d.ts.map +1 -1
  107. package/dist/esm/executor-generator.js +62 -0
  108. package/dist/esm/executor-generator.js.map +1 -1
  109. package/dist/esm/graph-utils.d.ts.map +1 -1
  110. package/dist/esm/graph-utils.js +1 -0
  111. package/dist/esm/graph-utils.js.map +1 -1
  112. package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
  113. package/dist/esm/invoice/api-routes-code.js +2 -0
  114. package/dist/esm/invoice/api-routes-code.js.map +1 -1
  115. package/dist/esm/invoice/data-access-code.d.ts.map +1 -1
  116. package/dist/esm/invoice/data-access-code.js +2 -0
  117. package/dist/esm/invoice/data-access-code.js.map +1 -1
  118. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  119. package/dist/esm/invoice/invoice-html-code.js +22 -2
  120. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  121. package/dist/esm/nodes/account/account-delete-current.d.ts +3 -0
  122. package/dist/esm/nodes/account/account-delete-current.d.ts.map +1 -0
  123. package/dist/esm/nodes/account/account-delete-current.js +76 -0
  124. package/dist/esm/nodes/account/account-delete-current.js.map +1 -0
  125. package/dist/esm/nodes/account/account-signup.d.ts.map +1 -1
  126. package/dist/esm/nodes/account/account-signup.js +20 -1
  127. package/dist/esm/nodes/account/account-signup.js.map +1 -1
  128. package/dist/esm/nodes/data/data-create-item.d.ts.map +1 -1
  129. package/dist/esm/nodes/data/data-create-item.js +7 -0
  130. package/dist/esm/nodes/data/data-create-item.js.map +1 -1
  131. package/dist/esm/nodes/index.d.ts.map +1 -1
  132. package/dist/esm/nodes/index.js +2 -1
  133. package/dist/esm/nodes/index.js.map +1 -1
  134. package/dist/esm/nodes/payment/payment-charge-user.d.ts.map +1 -1
  135. package/dist/esm/nodes/payment/payment-charge-user.js +9 -1
  136. package/dist/esm/nodes/payment/payment-charge-user.js.map +1 -1
  137. package/dist/esm/pg-client-code.d.ts +11 -0
  138. package/dist/esm/pg-client-code.d.ts.map +1 -0
  139. package/dist/esm/pg-client-code.js +71 -0
  140. package/dist/esm/pg-client-code.js.map +1 -0
  141. package/dist/esm/raw-sql-param-binding.d.ts +82 -0
  142. package/dist/esm/raw-sql-param-binding.d.ts.map +1 -0
  143. package/dist/esm/raw-sql-param-binding.js +314 -0
  144. package/dist/esm/raw-sql-param-binding.js.map +1 -0
  145. package/dist/esm/transactional-email-code.d.ts +6 -0
  146. package/dist/esm/transactional-email-code.d.ts.map +1 -0
  147. package/dist/esm/transactional-email-code.js +106 -0
  148. package/dist/esm/transactional-email-code.js.map +1 -0
  149. package/dist/esm/trigger-generator.d.ts.map +1 -1
  150. package/dist/esm/trigger-generator.js +29 -0
  151. package/dist/esm/trigger-generator.js.map +1 -1
  152. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  153. package/dist/esm/workflow-component-plugin.js +60 -2
  154. package/dist/esm/workflow-component-plugin.js.map +1 -1
  155. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  156. package/dist/esm/workflow-project-plugin.js +188 -28
  157. package/dist/esm/workflow-project-plugin.js.map +1 -1
  158. package/package.json +4 -4
  159. package/src/account-delete-route-generator.ts +269 -0
  160. package/src/api-route-generator.ts +35 -33
  161. package/src/auth-generator.ts +77 -2
  162. package/src/data-api-route-generator.ts +11 -2
  163. package/src/ecommerce/cart-availability.ts +49 -2
  164. package/src/ecommerce/stock-decrement.ts +48 -10
  165. package/src/executor-generator.ts +62 -0
  166. package/src/graph-utils.ts +1 -0
  167. package/src/invoice/api-routes-code.ts +2 -0
  168. package/src/invoice/data-access-code.ts +2 -0
  169. package/src/invoice/invoice-html-code.ts +22 -2
  170. package/src/nodes/account/account-delete-current.ts +86 -0
  171. package/src/nodes/account/account-signup.ts +20 -1
  172. package/src/nodes/data/data-create-item.ts +7 -0
  173. package/src/nodes/index.ts +2 -0
  174. package/src/nodes/payment/payment-charge-user.ts +9 -1
  175. package/src/pg-client-code.ts +70 -0
  176. package/src/raw-sql-param-binding.ts +406 -0
  177. package/src/transactional-email-code.ts +112 -0
  178. package/src/trigger-generator.ts +35 -0
  179. package/src/workflow-component-plugin.ts +71 -2
  180. package/src/workflow-project-plugin.ts +221 -26
@@ -0,0 +1,138 @@
1
+ import {
2
+ parameterizeRawSqlInterpolations,
3
+ parameterizeWorkflowRawSqlInterpolations,
4
+ parameterizeAllWorkflowRawSql,
5
+ WorkflowSqlInterpolationError,
6
+ } from '../src/raw-sql-param-binding'
7
+
8
+ // Generation-time net (runs in the workflows plugin's runBefore). Converts each
9
+ // `{{ name }}` value token in a data node's raw SQL into a positional `$N` +
10
+ // a bound params entry, keeping the published-app runtime on the standard
11
+ // `$N` + params path. PAIRED with the GUI + worker copies.
12
+
13
+ function ref(nodeId: string, ...path: string[]) {
14
+ return { type: 'workflowContext' as const, nodeId, path: [nodeId, ...path] }
15
+ }
16
+
17
+ describe('parameterizeRawSqlInterpolations', () => {
18
+ it('binds a context-ref variable token in value position', () => {
19
+ const value = ref('normalize', 'ids')
20
+ const res = parameterizeRawSqlInterpolations(
21
+ 'DELETE FROM t WHERE id::text = ANY({{ selectedIds }}::text[])',
22
+ [{ name: 'selectedIds', value }]
23
+ )
24
+ expect(res.query).toBe('DELETE FROM t WHERE id::text = ANY($1::text[])')
25
+ expect(res.params).toEqual([value])
26
+ expect(res.residuals).toEqual([])
27
+ })
28
+
29
+ it('keeps an unmatched {{state.X}} token as its own string param', () => {
30
+ const res = parameterizeRawSqlInterpolations(
31
+ 'SELECT * FROM t WHERE owner = {{state.userId}}',
32
+ []
33
+ )
34
+ expect(res.query).toBe('SELECT * FROM t WHERE owner = $1')
35
+ expect(res.params).toEqual(['{{state.userId}}'])
36
+ })
37
+
38
+ it('reports an identifier-position token as a residual', () => {
39
+ const res = parameterizeRawSqlInterpolations('SELECT * FROM {{ t }}', [
40
+ { name: 't', value: ref('n', 't') },
41
+ ])
42
+ expect(res.residuals).toEqual(['{{ t }}'])
43
+ expect(res.params).toEqual([])
44
+ })
45
+ })
46
+
47
+ describe('parameterizeWorkflowRawSqlInterpolations', () => {
48
+ it('rewrites a data-raw-query node and drops queryVariables', () => {
49
+ const value = ref('n', 'ids')
50
+ const workflow = {
51
+ nodes: [
52
+ {
53
+ id: 'q',
54
+ type: 'data-raw-query',
55
+ config: {
56
+ query: 'DELETE FROM t WHERE id = ANY({{ ids }}::text[])',
57
+ queryVariables: [{ name: 'ids', value }],
58
+ },
59
+ },
60
+ ],
61
+ }
62
+ parameterizeWorkflowRawSqlInterpolations(workflow)
63
+ expect(workflow.nodes[0].config).toEqual({
64
+ query: 'DELETE FROM t WHERE id = ANY($1::text[])',
65
+ params: [value],
66
+ })
67
+ })
68
+
69
+ it('rewrites a data-select rawQueryUserPart', () => {
70
+ const value = ref('n', 'v')
71
+ const workflow = {
72
+ nodes: [
73
+ {
74
+ id: 's',
75
+ type: 'data-select',
76
+ config: {
77
+ rawQueryUserPart: 'owner_id = {{ v }}',
78
+ rawQueryUserPartVariables: [{ name: 'v', value }],
79
+ },
80
+ },
81
+ ],
82
+ }
83
+ parameterizeWorkflowRawSqlInterpolations(workflow)
84
+ expect(workflow.nodes[0].config).toEqual({
85
+ rawQueryUserPart: 'owner_id = $1',
86
+ rawQueryUserPartParams: [value],
87
+ })
88
+ })
89
+
90
+ it('throws on an unbindable token and mutates nothing', () => {
91
+ const workflow = {
92
+ nodes: [
93
+ {
94
+ id: 'bad',
95
+ type: 'data-raw-query',
96
+ config: {
97
+ query: 'SELECT * FROM {{ table }}',
98
+ queryVariables: [{ name: 'table', value: ref('n', 't') }],
99
+ },
100
+ },
101
+ ],
102
+ }
103
+ expect(() => parameterizeWorkflowRawSqlInterpolations(workflow)).toThrow(
104
+ WorkflowSqlInterpolationError
105
+ )
106
+ expect((workflow.nodes[0].config as { query: string }).query).toContain('{{ table }}')
107
+ })
108
+ })
109
+
110
+ describe('parameterizeAllWorkflowRawSql', () => {
111
+ it('walks both workflows and customNodes (id-keyed records)', () => {
112
+ const value = ref('n', 'ids')
113
+ const uidlWorkflows = {
114
+ workflows: {
115
+ wf1: {
116
+ nodes: {
117
+ n1: {
118
+ type: 'data-raw-query',
119
+ config: {
120
+ query: 'DELETE FROM t WHERE id = ANY({{ ids }}::text[])',
121
+ queryVariables: [{ name: 'ids', value }],
122
+ },
123
+ },
124
+ },
125
+ },
126
+ },
127
+ customNodes: {},
128
+ }
129
+ parameterizeAllWorkflowRawSql(uidlWorkflows)
130
+ const cfg = uidlWorkflows.workflows.wf1.nodes.n1.config as { query: string; params: unknown[] }
131
+ expect(cfg.query).toBe('DELETE FROM t WHERE id = ANY($1::text[])')
132
+ expect(cfg.params).toEqual([value])
133
+ })
134
+
135
+ it('is a no-op when workflows is undefined', () => {
136
+ expect(() => parameterizeAllWorkflowRawSql(undefined)).not.toThrow()
137
+ })
138
+ })
@@ -0,0 +1,106 @@
1
+ // `event-route-changed` codegen: component-scoped triggers register a
2
+ // `Router.events` listener inside the owning component's lifecycle effect
3
+ // (with an `.off` cleanup so remounts don't leak listeners); unbound triggers
4
+ // are global-scoped and register in the `_app`-mounted global workflows hook,
5
+ // which must then import Router itself.
6
+
7
+ import { createNextWorkflowPlugin } from '../src/workflow-component-plugin'
8
+ import { NextWorkflowProjectPlugin } from '../src/workflow-project-plugin'
9
+
10
+ const routeChangedWorkflow = (scope: string, config: Record<string, unknown>) => ({
11
+ id: 'wf-route-changed',
12
+ name: 'Track Route',
13
+ trigger: {
14
+ type: 'event-route-changed',
15
+ nodeId: 'trigger-route-changed',
16
+ scope,
17
+ config,
18
+ },
19
+ nodes: [
20
+ {
21
+ id: 'update-1',
22
+ type: 'state-update-local-state',
23
+ config: { property: 'currentRoute', value: '' },
24
+ stepNumber: 1,
25
+ label: 'Set route',
26
+ },
27
+ ],
28
+ edges: [{ id: 'e1', source: 'trigger-route-changed', target: 'update-1' }],
29
+ })
30
+
31
+ const jsxComponentChunk = () => ({
32
+ type: 'chunk-type-ast',
33
+ name: 'jsx-component',
34
+ content: {
35
+ type: 'VariableDeclaration',
36
+ declarations: [
37
+ {
38
+ type: 'VariableDeclarator',
39
+ init: {
40
+ type: 'ArrowFunctionExpression',
41
+ body: {
42
+ type: 'BlockStatement',
43
+ body: [{ type: 'ReturnStatement', argument: null }],
44
+ },
45
+ },
46
+ },
47
+ ],
48
+ },
49
+ })
50
+
51
+ describe('event-route-changed — component scope', () => {
52
+ it('registers Router.events with an .off cleanup in the component lifecycle', async () => {
53
+ const workflow = routeChangedWorkflow('component', { componentName: 'Navigation' })
54
+ const plugin = createNextWorkflowPlugin({ isPage: false })
55
+ const structure: any = {
56
+ uidl: {
57
+ name: 'Navigation',
58
+ node: { type: 'element', content: { elementType: 'container', name: 'Container' } },
59
+ stateDefinitions: { currentRoute: { type: 'string', defaultValue: '' } },
60
+ },
61
+ chunks: [jsxComponentChunk()],
62
+ options: {
63
+ workflows: { workflows: { [workflow.id]: workflow }, customNodes: {} },
64
+ },
65
+ dependencies: {},
66
+ }
67
+ await plugin(structure)
68
+ const moduleChunk = (structure.chunks as any[]).find((c: any) => c.name === 'workflow-module')
69
+ expect(moduleChunk).toBeDefined()
70
+ const code = String(moduleChunk.content)
71
+ expect(code).toContain("Router.events.on('routeChangeComplete'")
72
+ expect(code).toContain("Router.events.off('routeChangeComplete'")
73
+ expect(code).toContain('cleanups.push')
74
+ expect(code).toContain('previousUrl')
75
+ })
76
+ })
77
+
78
+ describe('event-route-changed — global scope', () => {
79
+ const plugin = new NextWorkflowProjectPlugin()
80
+ // generateGlobalWorkflowsHook is private; call it directly for a unit test.
81
+ const code = (plugin as any).generateGlobalWorkflowsHook([
82
+ routeChangedWorkflow('global', {}),
83
+ ]) as string
84
+
85
+ it('registers Router.events with an .off cleanup in the global hook', () => {
86
+ expect(code).toContain("Router.events.on('routeChangeComplete'")
87
+ expect(code).toContain("Router.events.off('routeChangeComplete'")
88
+ expect(code).toContain('previousUrl')
89
+ })
90
+
91
+ it('imports Router in the generated global-workflows file', () => {
92
+ expect(code).toContain("import Router from 'next/router';")
93
+ })
94
+
95
+ it('does NOT import Router when no route-changed workflow exists', () => {
96
+ const withoutRoute = (plugin as any).generateGlobalWorkflowsHook([
97
+ {
98
+ id: 'wf-other',
99
+ trigger: { type: 'event-user-logged-in', config: {} },
100
+ nodes: [],
101
+ edges: [],
102
+ },
103
+ ]) as string
104
+ expect(withoutRoute).not.toContain("import Router from 'next/router';")
105
+ })
106
+ })
@@ -0,0 +1,180 @@
1
+ // `__stateUpdateHandler` observability guard: writing to a state property the
2
+ // current page/component does NOT own must console.warn (and still resolve)
3
+ // instead of silently no-oping. The silent no-op masked the original
4
+ // "Auto-open Navigation Sidebar Group" bug for weeks: the workflow ran on
5
+ // every page, found no `sidebarGroupOpen` setter, and did nothing without a
6
+ // trace. Harness mirrors state-update-default-value.test.ts.
7
+
8
+ import { createNextWorkflowPlugin } from '../src/workflow-component-plugin'
9
+
10
+ const extractFunctionSource = (haystack: string, funcDecl: string): string => {
11
+ const startIdx = haystack.indexOf(funcDecl)
12
+ if (startIdx === -1) {
13
+ throw new Error('Helper not found: ' + funcDecl)
14
+ }
15
+ return braceMatchFrom(haystack, startIdx)
16
+ }
17
+
18
+ const braceMatchFrom = (haystack: string, startIdx: number): string => {
19
+ let depth = 0
20
+ let i = haystack.indexOf('{', startIdx)
21
+ if (i === -1) {
22
+ throw new Error('No opening brace from index ' + startIdx)
23
+ }
24
+ for (; i < haystack.length; i++) {
25
+ const ch = haystack.charAt(i)
26
+ if (ch === '{') {
27
+ depth++
28
+ } else if (ch === '}') {
29
+ depth--
30
+ if (depth === 0) {
31
+ return haystack.slice(startIdx, i + 1)
32
+ }
33
+ }
34
+ }
35
+ throw new Error('Unbalanced braces from index ' + startIdx)
36
+ }
37
+
38
+ const buildStructure = (): any => ({
39
+ uidl: {
40
+ name: 'Page',
41
+ outputOptions: { pageId: 'page-1', fileName: 'page-1' },
42
+ node: { type: 'element', content: { elementType: 'container', name: 'Container' } },
43
+ stateDefinitions: { known: { type: 'string', defaultValue: '' } },
44
+ },
45
+ chunks: [
46
+ {
47
+ type: 'chunk-type-ast',
48
+ name: 'jsx-component',
49
+ content: {
50
+ type: 'VariableDeclaration',
51
+ declarations: [
52
+ {
53
+ type: 'VariableDeclarator',
54
+ init: {
55
+ type: 'ArrowFunctionExpression',
56
+ body: {
57
+ type: 'BlockStatement',
58
+ body: [{ type: 'ReturnStatement', argument: null }],
59
+ },
60
+ },
61
+ },
62
+ ],
63
+ },
64
+ },
65
+ ],
66
+ options: {
67
+ workflows: {
68
+ workflows: {
69
+ 'wf-1': {
70
+ id: 'wf-1',
71
+ name: 'Test WF',
72
+ trigger: {
73
+ type: 'event-page-loaded',
74
+ nodeId: 'trigger-1',
75
+ scope: 'page',
76
+ config: { pageId: 'page-1' },
77
+ },
78
+ nodes: [
79
+ {
80
+ id: 'update-1',
81
+ type: 'state-update-local-state',
82
+ config: { property: 'known', value: 'x' },
83
+ stepNumber: 1,
84
+ label: 'X',
85
+ },
86
+ ],
87
+ edges: [{ id: 'e', source: 'trigger-1', target: 'update-1' }],
88
+ },
89
+ },
90
+ customNodes: {},
91
+ },
92
+ },
93
+ dependencies: {},
94
+ })
95
+
96
+ type HandlerFn = (config: any, context: any) => Promise<any>
97
+
98
+ const buildUpdateHandler = async (
99
+ stateTypes: Record<string, string>
100
+ ): Promise<{ update: HandlerFn; writes: Record<string, unknown> }> => {
101
+ const plugin = createNextWorkflowPlugin({ isPage: true })
102
+ const structure = buildStructure()
103
+ await plugin(structure as any)
104
+ const moduleChunk = (structure.chunks as any[]).find((c: any) => c.name === 'workflow-module')
105
+ if (!moduleChunk) {
106
+ throw new Error('workflow-module chunk not emitted by plugin')
107
+ }
108
+ const moduleCode = String(moduleChunk.content)
109
+
110
+ const coerceSrc = extractFunctionSource(moduleCode, 'function __coerceValue')
111
+ const defaultSrc = extractFunctionSource(moduleCode, 'function __defaultValueForType')
112
+ const updateSrc = extractFunctionSource(moduleCode, 'function __stateUpdateHandler')
113
+
114
+ const writes: Record<string, unknown> = {}
115
+ const stateSetters: Record<string, (v: unknown) => void> = {}
116
+ for (const key of Object.keys(stateTypes)) {
117
+ stateSetters[key] = (v: unknown) => {
118
+ writes[key] = v
119
+ }
120
+ }
121
+ const stateValuesRef = { current: {} as Record<string, unknown> }
122
+
123
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
124
+ const factory = new Function(
125
+ 'stateSetters',
126
+ 'stateTypes',
127
+ 'stateValuesRef',
128
+ `
129
+ var __stateNameMap = {};
130
+ Object.keys(stateSetters).forEach(function(k) { __stateNameMap[k] = k; });
131
+ function __resolveName(name) { return __stateNameMap[name] || name; }
132
+ ${coerceSrc}
133
+ ${defaultSrc}
134
+ ${updateSrc}
135
+ return __stateUpdateHandler;
136
+ `
137
+ )
138
+ const update = factory(stateSetters, stateTypes, stateValuesRef) as HandlerFn
139
+ return { update, writes }
140
+ }
141
+
142
+ describe('state-update with a missing setter', () => {
143
+ let warnSpy: jest.SpyInstance
144
+
145
+ beforeEach(() => {
146
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
147
+ })
148
+
149
+ afterEach(() => {
150
+ warnSpy.mockRestore()
151
+ })
152
+
153
+ it('warns and still resolves for a plain-value update', async () => {
154
+ const { update, writes } = await buildUpdateHandler({ known: 'string' })
155
+ const result = await update({ property: 'unknownProp', value: 'x' }, { __stateValues: {} })
156
+ expect(result.success).toBe(true)
157
+ expect(writes.unknownProp).toBeUndefined()
158
+ expect(warnSpy).toHaveBeenCalledTimes(1)
159
+ expect(String(warnSpy.mock.calls[0][0])).toContain('unknownProp')
160
+ expect(String(warnSpy.mock.calls[0][0])).toContain('known')
161
+ })
162
+
163
+ it('warns and still resolves for an object property-mode update', async () => {
164
+ const { update } = await buildUpdateHandler({ known: 'string' })
165
+ const result = await update(
166
+ { property: 'unknownObj', objectUpdateMode: 'property', objectPropertyPath: 'a', value: 1 },
167
+ { __stateValues: {} }
168
+ )
169
+ expect(result.success).toBe(true)
170
+ expect(warnSpy).toHaveBeenCalledTimes(1)
171
+ expect(String(warnSpy.mock.calls[0][0])).toContain('unknownObj')
172
+ })
173
+
174
+ it('does NOT warn when the setter exists', async () => {
175
+ const { update, writes } = await buildUpdateHandler({ known: 'string' })
176
+ await update({ property: 'known', value: 'hello' }, { __stateValues: {} })
177
+ expect(writes.known).toBe('hello')
178
+ expect(warnSpy).not.toHaveBeenCalled()
179
+ })
180
+ })
@@ -0,0 +1,13 @@
1
+ export interface AccountDeleteRouteOptions {
2
+ authUsersTableName?: string;
3
+ emailProvider?: string | null;
4
+ fromEmail?: string;
5
+ emailSecretEnvName?: string | null;
6
+ emailSubject?: string;
7
+ emailBodyHtml?: string;
8
+ siteName?: string;
9
+ deletedEmailPattern?: string;
10
+ }
11
+ export declare const accountDeleteRouteDependencies: (provider?: string) => Record<string, string>;
12
+ export declare const generateAccountDeleteRoute: (options?: AccountDeleteRouteOptions) => string;
13
+ //# sourceMappingURL=account-delete-route-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-delete-route-generator.d.ts","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,yBAAyB;IAExC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAI3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAID,eAAO,MAAM,8BAA8B,+CAAiC,CAAA;AAE5E,eAAO,MAAM,0BAA0B,aAAa,yBAAyB,KAAQ,MA6NpF,CAAA"}
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateAccountDeleteRoute = exports.accountDeleteRouteDependencies = void 0;
4
+ /**
5
+ * Generates the dedicated API route that backs the `account-delete-current`
6
+ * workflow node: `pages/api/account/delete-current.js`.
7
+ *
8
+ * The client node calls this route, which:
9
+ * 1. resolves the current user from the NextAuth session (never trusting a
10
+ * client-supplied id),
11
+ * 2. runs a single DB transaction that detaches + anonymises business/legal
12
+ * records (orders, withdrawal requests) so tracking survives, deletes the
13
+ * user's personal data (reviews, cart, favourites, chat history) and the
14
+ * user row, and
15
+ * 3. best-effort sends the farewell email (a failed send never fails the
16
+ * deletion).
17
+ *
18
+ * Every table statement is guarded by a runtime `information_schema` column
19
+ * check, so the route is a no-op for feature tables a given project never
20
+ * provisioned.
21
+ */
22
+ const pg_client_code_1 = require("./pg-client-code");
23
+ const transactional_email_code_1 = require("./transactional-email-code");
24
+ // Kept as a named alias (imported by workflow-project-plugin.ts) so the delete
25
+ // route's dependency wiring stays stable while the impl lives in the shared module.
26
+ exports.accountDeleteRouteDependencies = transactional_email_code_1.transactionalEmailDependencies;
27
+ const generateAccountDeleteRoute = (options = {}) => {
28
+ const authUsersTable = options.authUsersTableName || 'users';
29
+ const provider = options.emailProvider && transactional_email_code_1.SUPPORTED_EMAIL_PROVIDERS.has(options.emailProvider)
30
+ ? options.emailProvider
31
+ : null;
32
+ const deletedEmailPattern = options.deletedEmailPattern || 'deleted-user-{{userId}}@deleted.invalid';
33
+ return `${(0, pg_client_code_1.generatePgClientCode)()}
34
+ const { getToken } = require('next-auth/jwt');
35
+
36
+ const AUTH_USERS_TABLE = ${JSON.stringify(authUsersTable)};
37
+ const SITE_NAME = ${JSON.stringify(options.siteName || '')};
38
+ const DELETED_EMAIL_PATTERN = ${JSON.stringify(deletedEmailPattern)};
39
+ const EMAIL_PROVIDER = ${JSON.stringify(provider)};
40
+ const EMAIL_FROM = ${JSON.stringify(options.fromEmail || '')};
41
+ const EMAIL_SECRET_ENV_NAME = ${JSON.stringify(options.emailSecretEnvName || '')};
42
+ const EMAIL_SUBJECT = ${JSON.stringify(options.emailSubject || 'Your account has been deleted')};
43
+ const EMAIL_BODY_HTML = ${JSON.stringify(options.emailBodyHtml || '')};
44
+
45
+ // Personal data owned by the user — rows are DELETED (keyed by user_id). Messages
46
+ // are deleted before conversations so a conversation delete only cascades leftovers.
47
+ const DELETE_TABLES = [
48
+ 'teleport_product_reviews',
49
+ 'teleport_favourites',
50
+ 'teleport_cart',
51
+ 'teleport_ai_chat_messages',
52
+ 'teleport_ai_chat_conversations',
53
+ ];
54
+ // Business / legal records that must survive: the user link is nulled and any
55
+ // stored personal email is replaced with a synthetic, self-describing address.
56
+ const ANONYMIZE_TABLES = ['teleport_orders', 'teleport_withdrawal_requests'];
57
+ const EMAIL_COLUMNS = ['billing_email', 'shipping_email', 'customer_email', 'email'];
58
+
59
+ async function tableColumns(client, table) {
60
+ var r = await client.query(
61
+ "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1",
62
+ [table]
63
+ );
64
+ var cols = [];
65
+ for (var i = 0; i < r.rows.length; i++) { cols.push(r.rows[i].column_name); }
66
+ return cols;
67
+ }
68
+
69
+ async function anonymizeUserRows(client, table, userId, syntheticEmail) {
70
+ var cols = await tableColumns(client, table);
71
+ if (cols.indexOf('user_id') === -1) { return; }
72
+ var sets = ['user_id = NULL'];
73
+ var params = [userId];
74
+ for (var i = 0; i < EMAIL_COLUMNS.length; i++) {
75
+ if (cols.indexOf(EMAIL_COLUMNS[i]) !== -1) {
76
+ params.push(syntheticEmail);
77
+ sets.push(EMAIL_COLUMNS[i] + ' = $' + params.length);
78
+ }
79
+ }
80
+ await client.query('UPDATE ' + table + ' SET ' + sets.join(', ') + ' WHERE user_id = $1', params);
81
+ }
82
+
83
+ async function deleteUserRows(client, table, userId) {
84
+ var cols = await tableColumns(client, table);
85
+ if (cols.indexOf('user_id') === -1) { return; }
86
+ await client.query('DELETE FROM ' + table + ' WHERE user_id = $1', [userId]);
87
+ }
88
+
89
+ async function deleteRowsByEmail(client, table, column, email) {
90
+ if (!email) { return; }
91
+ var cols = await tableColumns(client, table);
92
+ if (cols.indexOf(column) === -1) { return; }
93
+ await client.query('DELETE FROM ' + table + ' WHERE ' + column + ' = $1', [email]);
94
+ }
95
+
96
+ // Invoices are a financial/tax record that must survive, but they store the
97
+ // customer's PII denormalised and link to the user only through order_id (no
98
+ // user_id). Scrub the PII in place, matching invoices whose order belongs to the
99
+ // user. MUST run BEFORE the orders row is anonymised (its user_id is still set).
100
+ var INVOICE_PII_NULL_COLUMNS = [
101
+ 'customer_name',
102
+ 'customer_address',
103
+ 'customer_city',
104
+ 'customer_state',
105
+ 'customer_zip',
106
+ 'customer_country',
107
+ 'customer_vat',
108
+ 'customer_phone',
109
+ ];
110
+
111
+ async function anonymizeInvoices(client, ordersTable, invoicesTable, userId, syntheticEmail) {
112
+ var invCols = await tableColumns(client, invoicesTable);
113
+ if (invCols.indexOf('order_id') === -1) { return; }
114
+ var orderCols = await tableColumns(client, ordersTable);
115
+ if (orderCols.indexOf('user_id') === -1) { return; }
116
+ var sets = [];
117
+ var params = [];
118
+ if (invCols.indexOf('customer_email') !== -1) {
119
+ params.push(syntheticEmail);
120
+ sets.push('customer_email = $' + params.length);
121
+ }
122
+ for (var i = 0; i < INVOICE_PII_NULL_COLUMNS.length; i++) {
123
+ if (invCols.indexOf(INVOICE_PII_NULL_COLUMNS[i]) !== -1) {
124
+ sets.push(INVOICE_PII_NULL_COLUMNS[i] + ' = NULL');
125
+ }
126
+ }
127
+ if (sets.length === 0) { return; }
128
+ params.push(userId);
129
+ await client.query(
130
+ 'UPDATE ' + invoicesTable + ' SET ' + sets.join(', ') +
131
+ ' WHERE order_id IN (SELECT id FROM ' + ordersTable + ' WHERE user_id = $' + params.length + ')',
132
+ params
133
+ );
134
+ }
135
+
136
+ ${(0, transactional_email_code_1.generateFillTemplateFn)()}
137
+
138
+ ${(0, transactional_email_code_1.generateProviderSendFunction)(provider)}
139
+
140
+ async function sendFarewellEmail(toEmail, tokenValues) {
141
+ if (!EMAIL_PROVIDER || !EMAIL_BODY_HTML || !toEmail) { return; }
142
+ var apiKey = EMAIL_SECRET_ENV_NAME ? process.env[EMAIL_SECRET_ENV_NAME] : '';
143
+ // Don't treat an unresolved deploy placeholder as a real credential.
144
+ if (apiKey && String(apiKey).indexOf('teleporthq.secrets.') === 0) { apiKey = ''; }
145
+ if (!apiKey) { console.warn('[account-delete] farewell email skipped: credential not set'); return; }
146
+ var from = EMAIL_FROM || process.env.EMAIL_FROM || '';
147
+ if (!from) { console.warn('[account-delete] farewell email skipped: sender not configured'); return; }
148
+ var subject = fillTemplate(EMAIL_SUBJECT, tokenValues);
149
+ var html = fillTemplate(EMAIL_BODY_HTML, tokenValues);
150
+ await __sendProviderEmail({ from: from, to: toEmail, subject: subject, html: html, apiKey: apiKey });
151
+ }
152
+
153
+ module.exports = async function handler(req, res) {
154
+ if (req.method !== 'POST') {
155
+ return res.status(405).json({ error: 'Method not allowed' });
156
+ }
157
+
158
+ // Identify the user from the session — never trust a client-supplied id.
159
+ var token = null;
160
+ try {
161
+ token = await getToken({ req: req, secret: process.env.NEXTAUTH_SECRET });
162
+ } catch (e) {
163
+ token = null;
164
+ }
165
+ var userId = token && (token.id || token.sub);
166
+ if (!userId) {
167
+ return res.status(401).json({ error: 'Unauthenticated' });
168
+ }
169
+
170
+ var client = getClient();
171
+ var userEmail = '';
172
+ var userName = '';
173
+ var userFound = false;
174
+ var failed = false;
175
+ // try/finally so the pg connection is ALWAYS closed — every early exit
176
+ // (not-found, error) leaves via the finally, never leaking a connection.
177
+ try {
178
+ await client.connect();
179
+
180
+ // Capture the email/name BEFORE deletion so the farewell email can still be sent.
181
+ var userRes = await client.query('SELECT * FROM ' + AUTH_USERS_TABLE + ' WHERE id = $1', [userId]);
182
+ if (userRes.rows.length) {
183
+ userFound = true;
184
+ userEmail = userRes.rows[0].email || '';
185
+ userName = userRes.rows[0].name || '';
186
+
187
+ var syntheticEmail = String(DELETED_EMAIL_PATTERN).split('{{userId}}').join(String(userId));
188
+
189
+ await client.query('BEGIN');
190
+ try {
191
+ // Scrub invoice PII FIRST — invoices link to the user only through
192
+ // order_id, so this must run while the orders row still carries user_id.
193
+ await anonymizeInvoices(client, 'teleport_orders', 'teleport_invoices', userId, syntheticEmail);
194
+ for (var a = 0; a < ANONYMIZE_TABLES.length; a++) {
195
+ await anonymizeUserRows(client, ANONYMIZE_TABLES[a], userId, syntheticEmail);
196
+ }
197
+ for (var d = 0; d < DELETE_TABLES.length; d++) {
198
+ await deleteUserRows(client, DELETE_TABLES[d], userId);
199
+ }
200
+ // password_reset_tokens has no user_id — it is keyed by the email string.
201
+ await deleteRowsByEmail(client, 'password_reset_tokens', 'email', userEmail);
202
+ await client.query('DELETE FROM ' + AUTH_USERS_TABLE + ' WHERE id = $1', [userId]);
203
+ await client.query('COMMIT');
204
+ } catch (txErr) {
205
+ try { await client.query('ROLLBACK'); } catch (e) {}
206
+ throw txErr;
207
+ }
208
+ }
209
+ } catch (err) {
210
+ failed = true;
211
+ console.error('[account-delete] failed:', err && err.message ? err.message : err);
212
+ } finally {
213
+ try { await client.end(); } catch (e) {}
214
+ }
215
+
216
+ if (failed) {
217
+ return res.status(500).json({ error: 'Account deletion failed' });
218
+ }
219
+ if (!userFound) {
220
+ return res.status(404).json({ error: 'Account not found' });
221
+ }
222
+
223
+ // Best-effort farewell email AFTER the successful commit (connection closed).
224
+ try {
225
+ var resolvedSiteName =
226
+ SITE_NAME ||
227
+ (req.headers && req.headers.host
228
+ ? String(req.headers.host).replace(/^www\\./, '').split(':')[0]
229
+ : '');
230
+ await sendFarewellEmail(userEmail, {
231
+ userName: userName || 'there',
232
+ userEmail: userEmail,
233
+ siteName: resolvedSiteName,
234
+ });
235
+ } catch (emailErr) {
236
+ console.error(
237
+ '[account-delete] farewell email failed:',
238
+ emailErr && emailErr.message ? emailErr.message : emailErr
239
+ );
240
+ }
241
+
242
+ return res.status(200).json({ success: true });
243
+ };
244
+ `;
245
+ };
246
+ exports.generateAccountDeleteRoute = generateAccountDeleteRoute;
247
+ //# sourceMappingURL=account-delete-route-generator.js.map