@teleporthq/teleport-plugin-next-workflows 0.43.75 → 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 (248) hide show
  1. package/__tests__/_helpers/fake-store-database.ts +428 -0
  2. package/__tests__/_helpers/invoice-store.ts +382 -0
  3. package/__tests__/booking-calendar-feed-route.test.ts +494 -0
  4. package/__tests__/custom-node-result-taken-branch.test.ts +107 -0
  5. package/__tests__/data-api-date-parser.test.ts +45 -0
  6. package/__tests__/email-locale-senders.test.ts +3 -1
  7. package/__tests__/email-sender-name.test.ts +513 -0
  8. package/__tests__/graph-utils-topological-order.test.ts +155 -1
  9. package/__tests__/invoice-billing-details.test.ts +384 -0
  10. package/__tests__/invoice-data-access-replace.test.ts +236 -2
  11. package/__tests__/invoice-order-items-settle.test.ts +1 -1
  12. package/__tests__/invoice-payment-email.test.ts +191 -0
  13. package/__tests__/invoice-regenerate-email.test.ts +288 -0
  14. package/__tests__/invoice-regenerate-route.test.ts +9 -7
  15. package/__tests__/invoice-reservation.test.ts +1 -1
  16. package/__tests__/invoice-tender.test.ts +1 -1
  17. package/__tests__/middleware-admin-panel-roles.test.ts +127 -0
  18. package/__tests__/page-arrival-remount.test.ts +247 -0
  19. package/__tests__/payment-drivers-module.test.ts +19 -0
  20. package/__tests__/raw-query-error-code.test.ts +47 -0
  21. package/__tests__/runtime-template-token-resolution.test.ts +16 -0
  22. package/__tests__/segment-reply-changed-keys.test.ts +31 -0
  23. package/__tests__/segment-route-skipped-branch.test.ts +101 -0
  24. package/__tests__/segment-splitter-checkout-order.test.ts +306 -0
  25. package/__tests__/streaming-on-end-env-filter.test.ts +18 -18
  26. package/__tests__/streaming-on-end-order.test.ts +387 -0
  27. package/dist/cjs/account-delete-route-generator.d.ts +1 -0
  28. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
  29. package/dist/cjs/account-delete-route-generator.js +2 -1
  30. package/dist/cjs/account-delete-route-generator.js.map +1 -1
  31. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  32. package/dist/cjs/api-route-generator.js +23 -13
  33. package/dist/cjs/api-route-generator.js.map +1 -1
  34. package/dist/cjs/auth-generator.d.ts +1 -0
  35. package/dist/cjs/auth-generator.d.ts.map +1 -1
  36. package/dist/cjs/auth-generator.js +77 -28
  37. package/dist/cjs/auth-generator.js.map +1 -1
  38. package/dist/cjs/booking-calendar-feed-route-generator.d.ts +28 -0
  39. package/dist/cjs/booking-calendar-feed-route-generator.d.ts.map +1 -0
  40. package/dist/cjs/booking-calendar-feed-route-generator.js +296 -0
  41. package/dist/cjs/booking-calendar-feed-route-generator.js.map +1 -0
  42. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  43. package/dist/cjs/data-api-route-generator.js +6 -1
  44. package/dist/cjs/data-api-route-generator.js.map +1 -1
  45. package/dist/cjs/executor-generator.d.ts.map +1 -1
  46. package/dist/cjs/executor-generator.js +39 -28
  47. package/dist/cjs/executor-generator.js.map +1 -1
  48. package/dist/cjs/graph-utils.d.ts +31 -1
  49. package/dist/cjs/graph-utils.d.ts.map +1 -1
  50. package/dist/cjs/graph-utils.js +112 -10
  51. package/dist/cjs/graph-utils.js.map +1 -1
  52. package/dist/cjs/index.d.ts +1 -1
  53. package/dist/cjs/index.d.ts.map +1 -1
  54. package/dist/cjs/index.js +2 -1
  55. package/dist/cjs/index.js.map +1 -1
  56. package/dist/cjs/invoice/api-routes-code.d.ts +12 -3
  57. package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
  58. package/dist/cjs/invoice/api-routes-code.js +152 -21
  59. package/dist/cjs/invoice/api-routes-code.js.map +1 -1
  60. package/dist/cjs/invoice/data-access-code.d.ts.map +1 -1
  61. package/dist/cjs/invoice/data-access-code.js +133 -21
  62. package/dist/cjs/invoice/data-access-code.js.map +1 -1
  63. package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
  64. package/dist/cjs/invoice/email-sender-code.js +15 -25
  65. package/dist/cjs/invoice/email-sender-code.js.map +1 -1
  66. package/dist/cjs/invoice/index.js +1 -1
  67. package/dist/cjs/invoice/index.js.map +1 -1
  68. package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
  69. package/dist/cjs/invoice/invoice-assembly-code.js +50 -6
  70. package/dist/cjs/invoice/invoice-assembly-code.js.map +1 -1
  71. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  72. package/dist/cjs/invoice/invoice-html-code.js +13 -2
  73. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  74. package/dist/cjs/nodes/data/data-raw-query.d.ts.map +1 -1
  75. package/dist/cjs/nodes/data/data-raw-query.js +11 -1
  76. package/dist/cjs/nodes/data/data-raw-query.js.map +1 -1
  77. package/dist/cjs/nodes/email/email-mailgun.d.ts.map +1 -1
  78. package/dist/cjs/nodes/email/email-mailgun.js +4 -2
  79. package/dist/cjs/nodes/email/email-mailgun.js.map +1 -1
  80. package/dist/cjs/nodes/email/email-postmark.d.ts.map +1 -1
  81. package/dist/cjs/nodes/email/email-postmark.js +5 -2
  82. package/dist/cjs/nodes/email/email-postmark.js.map +1 -1
  83. package/dist/cjs/nodes/email/email-resend.d.ts.map +1 -1
  84. package/dist/cjs/nodes/email/email-resend.js +5 -2
  85. package/dist/cjs/nodes/email/email-resend.js.map +1 -1
  86. package/dist/cjs/nodes/email/email-sendgrid.d.ts.map +1 -1
  87. package/dist/cjs/nodes/email/email-sendgrid.js +8 -2
  88. package/dist/cjs/nodes/email/email-sendgrid.js.map +1 -1
  89. package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
  90. package/dist/cjs/nodes/general/general-custom-js.js +11 -0
  91. package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
  92. package/dist/cjs/nodes/storage/storage-local-remove.d.ts.map +1 -1
  93. package/dist/cjs/nodes/storage/storage-local-remove.js +2 -0
  94. package/dist/cjs/nodes/storage/storage-local-remove.js.map +1 -1
  95. package/dist/cjs/nodes/storage/storage-local-set.d.ts.map +1 -1
  96. package/dist/cjs/nodes/storage/storage-local-set.js +4 -0
  97. package/dist/cjs/nodes/storage/storage-local-set.js.map +1 -1
  98. package/dist/cjs/page-arrival.d.ts +10 -0
  99. package/dist/cjs/page-arrival.d.ts.map +1 -0
  100. package/dist/cjs/page-arrival.js +86 -0
  101. package/dist/cjs/page-arrival.js.map +1 -0
  102. package/dist/cjs/payments/index.d.ts +10 -4
  103. package/dist/cjs/payments/index.d.ts.map +1 -1
  104. package/dist/cjs/payments/index.js +18 -7
  105. package/dist/cjs/payments/index.js.map +1 -1
  106. package/dist/cjs/segment-splitter.d.ts.map +1 -1
  107. package/dist/cjs/segment-splitter.js +65 -27
  108. package/dist/cjs/segment-splitter.js.map +1 -1
  109. package/dist/cjs/transactional-email-code.d.ts.map +1 -1
  110. package/dist/cjs/transactional-email-code.js +32 -18
  111. package/dist/cjs/transactional-email-code.js.map +1 -1
  112. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  113. package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
  114. package/dist/cjs/workflow-component-plugin.js +35 -149
  115. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  116. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  117. package/dist/cjs/workflow-project-plugin.js +16 -4
  118. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  119. package/dist/cjs/workflow-scope.d.ts +19 -0
  120. package/dist/cjs/workflow-scope.d.ts.map +1 -0
  121. package/dist/cjs/workflow-scope.js +147 -0
  122. package/dist/cjs/workflow-scope.js.map +1 -0
  123. package/dist/esm/account-delete-route-generator.d.ts +1 -0
  124. package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
  125. package/dist/esm/account-delete-route-generator.js +2 -1
  126. package/dist/esm/account-delete-route-generator.js.map +1 -1
  127. package/dist/esm/api-route-generator.d.ts.map +1 -1
  128. package/dist/esm/api-route-generator.js +23 -13
  129. package/dist/esm/api-route-generator.js.map +1 -1
  130. package/dist/esm/auth-generator.d.ts +1 -0
  131. package/dist/esm/auth-generator.d.ts.map +1 -1
  132. package/dist/esm/auth-generator.js +77 -28
  133. package/dist/esm/auth-generator.js.map +1 -1
  134. package/dist/esm/booking-calendar-feed-route-generator.d.ts +28 -0
  135. package/dist/esm/booking-calendar-feed-route-generator.d.ts.map +1 -0
  136. package/dist/esm/booking-calendar-feed-route-generator.js +291 -0
  137. package/dist/esm/booking-calendar-feed-route-generator.js.map +1 -0
  138. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  139. package/dist/esm/data-api-route-generator.js +6 -1
  140. package/dist/esm/data-api-route-generator.js.map +1 -1
  141. package/dist/esm/executor-generator.d.ts.map +1 -1
  142. package/dist/esm/executor-generator.js +39 -28
  143. package/dist/esm/executor-generator.js.map +1 -1
  144. package/dist/esm/graph-utils.d.ts +31 -1
  145. package/dist/esm/graph-utils.d.ts.map +1 -1
  146. package/dist/esm/graph-utils.js +109 -9
  147. package/dist/esm/graph-utils.js.map +1 -1
  148. package/dist/esm/index.d.ts +1 -1
  149. package/dist/esm/index.d.ts.map +1 -1
  150. package/dist/esm/index.js +1 -1
  151. package/dist/esm/index.js.map +1 -1
  152. package/dist/esm/invoice/api-routes-code.d.ts +12 -3
  153. package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
  154. package/dist/esm/invoice/api-routes-code.js +152 -21
  155. package/dist/esm/invoice/api-routes-code.js.map +1 -1
  156. package/dist/esm/invoice/data-access-code.d.ts.map +1 -1
  157. package/dist/esm/invoice/data-access-code.js +133 -21
  158. package/dist/esm/invoice/data-access-code.js.map +1 -1
  159. package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
  160. package/dist/esm/invoice/email-sender-code.js +15 -25
  161. package/dist/esm/invoice/email-sender-code.js.map +1 -1
  162. package/dist/esm/invoice/index.js +1 -1
  163. package/dist/esm/invoice/index.js.map +1 -1
  164. package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
  165. package/dist/esm/invoice/invoice-assembly-code.js +50 -6
  166. package/dist/esm/invoice/invoice-assembly-code.js.map +1 -1
  167. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  168. package/dist/esm/invoice/invoice-html-code.js +13 -2
  169. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  170. package/dist/esm/nodes/data/data-raw-query.d.ts.map +1 -1
  171. package/dist/esm/nodes/data/data-raw-query.js +11 -1
  172. package/dist/esm/nodes/data/data-raw-query.js.map +1 -1
  173. package/dist/esm/nodes/email/email-mailgun.d.ts.map +1 -1
  174. package/dist/esm/nodes/email/email-mailgun.js +4 -2
  175. package/dist/esm/nodes/email/email-mailgun.js.map +1 -1
  176. package/dist/esm/nodes/email/email-postmark.d.ts.map +1 -1
  177. package/dist/esm/nodes/email/email-postmark.js +5 -2
  178. package/dist/esm/nodes/email/email-postmark.js.map +1 -1
  179. package/dist/esm/nodes/email/email-resend.d.ts.map +1 -1
  180. package/dist/esm/nodes/email/email-resend.js +5 -2
  181. package/dist/esm/nodes/email/email-resend.js.map +1 -1
  182. package/dist/esm/nodes/email/email-sendgrid.d.ts.map +1 -1
  183. package/dist/esm/nodes/email/email-sendgrid.js +8 -2
  184. package/dist/esm/nodes/email/email-sendgrid.js.map +1 -1
  185. package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
  186. package/dist/esm/nodes/general/general-custom-js.js +11 -0
  187. package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
  188. package/dist/esm/nodes/storage/storage-local-remove.d.ts.map +1 -1
  189. package/dist/esm/nodes/storage/storage-local-remove.js +2 -0
  190. package/dist/esm/nodes/storage/storage-local-remove.js.map +1 -1
  191. package/dist/esm/nodes/storage/storage-local-set.d.ts.map +1 -1
  192. package/dist/esm/nodes/storage/storage-local-set.js +4 -0
  193. package/dist/esm/nodes/storage/storage-local-set.js.map +1 -1
  194. package/dist/esm/page-arrival.d.ts +10 -0
  195. package/dist/esm/page-arrival.d.ts.map +1 -0
  196. package/dist/esm/page-arrival.js +81 -0
  197. package/dist/esm/page-arrival.js.map +1 -0
  198. package/dist/esm/payments/index.d.ts +10 -4
  199. package/dist/esm/payments/index.d.ts.map +1 -1
  200. package/dist/esm/payments/index.js +16 -6
  201. package/dist/esm/payments/index.js.map +1 -1
  202. package/dist/esm/segment-splitter.d.ts.map +1 -1
  203. package/dist/esm/segment-splitter.js +66 -28
  204. package/dist/esm/segment-splitter.js.map +1 -1
  205. package/dist/esm/transactional-email-code.d.ts.map +1 -1
  206. package/dist/esm/transactional-email-code.js +32 -18
  207. package/dist/esm/transactional-email-code.js.map +1 -1
  208. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  209. package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
  210. package/dist/esm/workflow-component-plugin.js +32 -146
  211. package/dist/esm/workflow-component-plugin.js.map +1 -1
  212. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  213. package/dist/esm/workflow-project-plugin.js +16 -4
  214. package/dist/esm/workflow-project-plugin.js.map +1 -1
  215. package/dist/esm/workflow-scope.d.ts +19 -0
  216. package/dist/esm/workflow-scope.d.ts.map +1 -0
  217. package/dist/esm/workflow-scope.js +142 -0
  218. package/dist/esm/workflow-scope.js.map +1 -0
  219. package/package.json +4 -4
  220. package/src/account-delete-route-generator.ts +3 -1
  221. package/src/api-route-generator.ts +23 -13
  222. package/src/auth-generator.ts +83 -26
  223. package/src/booking-calendar-feed-route-generator.ts +293 -0
  224. package/src/data-api-route-generator.ts +6 -1
  225. package/src/executor-generator.ts +39 -28
  226. package/src/graph-utils.ts +143 -11
  227. package/src/index.ts +1 -0
  228. package/src/invoice/api-routes-code.ts +166 -21
  229. package/src/invoice/data-access-code.ts +139 -21
  230. package/src/invoice/email-sender-code.ts +15 -25
  231. package/src/invoice/index.ts +1 -1
  232. package/src/invoice/invoice-assembly-code.ts +50 -6
  233. package/src/invoice/invoice-html-code.ts +13 -2
  234. package/src/nodes/data/data-raw-query.ts +11 -1
  235. package/src/nodes/email/email-mailgun.ts +11 -2
  236. package/src/nodes/email/email-postmark.ts +12 -2
  237. package/src/nodes/email/email-resend.ts +12 -2
  238. package/src/nodes/email/email-sendgrid.ts +15 -2
  239. package/src/nodes/general/general-custom-js.ts +10 -0
  240. package/src/nodes/storage/storage-local-remove.ts +4 -0
  241. package/src/nodes/storage/storage-local-set.ts +6 -0
  242. package/src/page-arrival.ts +115 -0
  243. package/src/payments/index.ts +18 -5
  244. package/src/segment-splitter.ts +90 -36
  245. package/src/transactional-email-code.ts +32 -21
  246. package/src/workflow-component-plugin.ts +43 -188
  247. package/src/workflow-project-plugin.ts +17 -4
  248. package/src/workflow-scope.ts +184 -0
@@ -0,0 +1,428 @@
1
+ type Row = Record<string, unknown>
2
+
3
+ interface QueryResult {
4
+ rows: Row[]
5
+ rowCount: number
6
+ }
7
+
8
+ interface Session {
9
+ inTransaction: boolean
10
+ undo: Array<() => void>
11
+ savepoints: Map<string, number>
12
+ held: Set<string>
13
+ }
14
+
15
+ interface Lock {
16
+ owner: Session
17
+ released: Promise<void>
18
+ release: () => void
19
+ }
20
+
21
+ /** The columns a second row may not repeat, beside `id`. */
22
+ const UNIQUE_COLUMNS: Record<string, string[]> = {
23
+ teleport_events: ['dedupe_key'],
24
+ }
25
+
26
+ const newSession = (): Session => ({
27
+ inTransaction: false,
28
+ undo: [],
29
+ savepoints: new Map(),
30
+ held: new Set(),
31
+ })
32
+
33
+ const uniqueViolation = (table: string, column: string): Error =>
34
+ Object.assign(
35
+ new Error(`duplicate key value violates unique constraint "${table}_${column}_key"`),
36
+ { code: '23505' }
37
+ )
38
+
39
+ /**
40
+ * A Postgres stand-in for the statements the generated invoice code sends: the
41
+ * store's tables in memory, their UNIQUE keys, transactions that roll back,
42
+ * and row locks — a transaction's UPDATE of a row holds it until COMMIT or
43
+ * ROLLBACK, and another UPDATE of that row waits for it and then reads the row
44
+ * as committed, as Postgres does under READ COMMITTED. Every statement it does
45
+ * not know fails loudly, so a change in the generated SQL fails a test instead
46
+ * of passing it vacuously.
47
+ *
48
+ * `pg` is the module to hand the generated code: its `Pool` (the invoice data
49
+ * access) and its `Client` (the sent-email ledger, the generate route's order
50
+ * mirror) all reach these same tables.
51
+ */
52
+ export class FakeStoreDatabase {
53
+ readonly tables: Record<string, Row[]> = {
54
+ teleport_invoices: [],
55
+ teleport_invoice_items: [],
56
+ teleport_orders: [],
57
+ teleport_order_items: [],
58
+ teleport_events: [],
59
+ teleport_sent_emails: [],
60
+ }
61
+
62
+ readonly statements: string[] = []
63
+
64
+ readonly pg = this.createPgModule()
65
+
66
+ private readonly locks = new Map<string, Lock>()
67
+
68
+ // A SERIAL primary key, for the rows inserted without an id.
69
+ private serial = 0
70
+
71
+ private failures: Array<{ statement: RegExp; message: string }> = []
72
+
73
+ private pauses: Array<{ statement: RegExp; arrive: () => void; released: Promise<void> }> = []
74
+
75
+ /** The next statement matching `statement` fails with `message`, as an outage would. */
76
+ failNext(statement: RegExp, message: string): void {
77
+ this.failures.push({ statement, message })
78
+ }
79
+
80
+ /**
81
+ * The next statement matching `statement` waits, before it touches any row,
82
+ * until `release()`; `reached` resolves once it waits. How a spec lets
83
+ * another request overtake this one at an exact point.
84
+ */
85
+ pauseNext(statement: RegExp): { reached: Promise<void>; release: () => void } {
86
+ let release: () => void = () => undefined
87
+ let arrive: () => void = () => undefined
88
+ const released = new Promise<void>((resolve) => {
89
+ release = resolve
90
+ })
91
+ const reached = new Promise<void>((resolve) => {
92
+ arrive = resolve
93
+ })
94
+ this.pauses.push({ statement, arrive, released })
95
+ return { reached, release }
96
+ }
97
+
98
+ insert(table: string, row: Row): Row {
99
+ const stored = { ...row }
100
+ this.rows(table).push(stored)
101
+ return stored
102
+ }
103
+
104
+ row(table: string, id: unknown): Row | undefined {
105
+ return this.rows(table).find((candidate) => String(candidate.id) === String(id))
106
+ }
107
+
108
+ private rows(table: string): Row[] {
109
+ if (!this.tables[table]) {
110
+ throw new Error(`fake database: relation "${table}" does not exist`)
111
+ }
112
+ return this.tables[table]
113
+ }
114
+
115
+ private createPgModule() {
116
+ const queryIn =
117
+ (session: Session) =>
118
+ (text: string, values: unknown[] = []) =>
119
+ this.query(session, text, values)
120
+ const endSession = (session: Session) => this.endSession(session)
121
+
122
+ class Pool {
123
+ query(text: string, values?: unknown[]) {
124
+ return queryIn(newSession())(text, values)
125
+ }
126
+
127
+ async connect() {
128
+ const session = newSession()
129
+ return {
130
+ query: queryIn(session),
131
+ release: () => endSession(session),
132
+ }
133
+ }
134
+ }
135
+
136
+ class Client {
137
+ private readonly session = newSession()
138
+
139
+ async connect(): Promise<void> {
140
+ // Nothing to open.
141
+ }
142
+
143
+ query(text: string, values?: unknown[]) {
144
+ return queryIn(this.session)(text, values)
145
+ }
146
+
147
+ async end(): Promise<void> {
148
+ endSession(this.session)
149
+ }
150
+ }
151
+
152
+ return { Pool, Client }
153
+ }
154
+
155
+ private endSession(session: Session): void {
156
+ if (session.inTransaction) {
157
+ this.finishTransaction(session, false)
158
+ }
159
+ }
160
+
161
+ private async query(session: Session, text: string, values: unknown[]): Promise<QueryResult> {
162
+ // Every statement is a round-trip: other requests run in between.
163
+ await new Promise((resolve) => setImmediate(resolve))
164
+ const sql = text.replace(/\s+/g, ' ').trim()
165
+ this.statements.push(sql)
166
+ const failure = this.failures.find((candidate) => candidate.statement.test(sql))
167
+ if (failure) {
168
+ this.failures = this.failures.filter((candidate) => candidate !== failure)
169
+ throw new Error(failure.message)
170
+ }
171
+ const pause = this.pauses.find((candidate) => candidate.statement.test(sql))
172
+ if (pause) {
173
+ this.pauses = this.pauses.filter((candidate) => candidate !== pause)
174
+ pause.arrive()
175
+ await pause.released
176
+ }
177
+ const autocommit = !session.inTransaction
178
+ try {
179
+ return await this.run(session, sql, values)
180
+ } finally {
181
+ if (autocommit && !session.inTransaction) {
182
+ this.releaseLocks(session)
183
+ }
184
+ }
185
+ }
186
+
187
+ private async run(session: Session, sql: string, values: unknown[]): Promise<QueryResult> {
188
+ if (sql === 'BEGIN') {
189
+ session.inTransaction = true
190
+ return { rows: [], rowCount: 0 }
191
+ }
192
+ if (sql === 'COMMIT' || sql === 'ROLLBACK') {
193
+ this.finishTransaction(session, sql === 'COMMIT')
194
+ return { rows: [], rowCount: 0 }
195
+ }
196
+ let match = /^SAVEPOINT (\w+)$/.exec(sql)
197
+ if (match) {
198
+ session.savepoints.set(match[1], session.undo.length)
199
+ return { rows: [], rowCount: 0 }
200
+ }
201
+ match = /^RELEASE SAVEPOINT (\w+)$/.exec(sql)
202
+ if (match) {
203
+ session.savepoints.delete(match[1])
204
+ return { rows: [], rowCount: 0 }
205
+ }
206
+ match = /^ROLLBACK TO SAVEPOINT (\w+)$/.exec(sql)
207
+ if (match) {
208
+ const mark = session.savepoints.get(match[1]) ?? session.undo.length
209
+ session.undo
210
+ .splice(mark)
211
+ .reverse()
212
+ .forEach((undo) => undo())
213
+ return { rows: [], rowCount: 0 }
214
+ }
215
+ if (sql === 'SELECT pg_advisory_xact_lock(hashtext($1))') {
216
+ await this.lock(session, `advisory:${String(values[0])}`)
217
+ return { rows: [{}], rowCount: 1 }
218
+ }
219
+
220
+ match =
221
+ /^SELECT \* FROM (\w+) WHERE (\w+)(?:::text)? = \$1(?: ORDER BY [\w ,]+?)?(?: LIMIT (\d+))?$/.exec(
222
+ sql
223
+ )
224
+ if (match) {
225
+ const [, table, column, limit] = match
226
+ const found = this.rows(table)
227
+ .filter((row) => row[column] != null && String(row[column]) === String(values[0]))
228
+ .map((row) => ({ ...row }))
229
+ const rows = limit ? found.slice(0, Number(limit)) : found
230
+ return { rows, rowCount: rows.length }
231
+ }
232
+
233
+ match = /^SELECT MAX\(.+\) AS last_number FROM (\w+) WHERE .+$/.exec(sql)
234
+ if (match) {
235
+ const prefix = String(values[0])
236
+ const issued = this.rows(match[1])
237
+ .map((row) => String(row.invoice_number || ''))
238
+ .filter(
239
+ (invoiceNumber) =>
240
+ invoiceNumber.startsWith(prefix) &&
241
+ /^[0-9]{1,15}$/.test(invoiceNumber.slice(prefix.length))
242
+ )
243
+ .map((invoiceNumber) => Number(invoiceNumber.slice(prefix.length)))
244
+ return { rows: [{ last_number: issued.length ? Math.max(...issued) : null }], rowCount: 1 }
245
+ }
246
+
247
+ match =
248
+ /^UPDATE teleport_orders SET invoice_id = \$1, invoice_number = \$2, invoice_pdf_url = \$3, updated_at = NOW\(\) WHERE id = \$4 AND \(invoice_id IS NULL OR invoice_id::text = \$5\)$/.exec(
249
+ sql
250
+ )
251
+ if (match) {
252
+ return this.updateRow(session, 'teleport_orders', values[3], (order) => {
253
+ if (order.invoice_id != null && String(order.invoice_id) !== String(values[4])) {
254
+ return null
255
+ }
256
+ return { invoice_id: values[0], invoice_number: values[1], invoice_pdf_url: values[2] }
257
+ })
258
+ }
259
+
260
+ match =
261
+ /^UPDATE teleport_orders SET invoice_id = COALESCE\(invoice_id, \$1\), invoice_number = COALESCE\(invoice_number, \$2\), invoice_pdf_url = COALESCE\(NULLIF\(invoice_pdf_url, ''\), NULLIF\(\$3, ''\)\), updated_at = NOW\(\) WHERE id = \$4$/.exec(
262
+ sql
263
+ )
264
+ if (match) {
265
+ return this.updateRow(session, 'teleport_orders', values[3], (order) => ({
266
+ invoice_id: order.invoice_id ?? values[0],
267
+ invoice_number: order.invoice_number ?? values[1],
268
+ invoice_pdf_url: order.invoice_pdf_url || values[2] || null,
269
+ }))
270
+ }
271
+
272
+ // claimStaleInvoice: a reservation still without its PDF, untouched since `$3`.
273
+ match =
274
+ /^UPDATE (\w+) SET updated_at = \$1 WHERE id::text = \$2 AND pdf_size_bytes IS NULL AND updated_at < \$3 RETURNING id$/.exec(
275
+ sql
276
+ )
277
+ if (match) {
278
+ return this.updateRow(session, match[1], values[1], (row) =>
279
+ row.pdf_size_bytes == null && String(row.updated_at) < String(values[2])
280
+ ? { updated_at: values[0] }
281
+ : null
282
+ )
283
+ }
284
+
285
+ match = /^UPDATE (\w+) SET (.+) WHERE id = \$(\d+)( AND pdf_size_bytes IS NULL)?$/.exec(sql)
286
+ if (match) {
287
+ const [, table, assignments, idParam, onlyWithoutPdf] = match
288
+ const fields: Row = {}
289
+ for (const assignment of assignments.split(', ')) {
290
+ const column = /^"?(\w+)"? = \$(\d+)$/.exec(assignment)
291
+ if (!column) {
292
+ throw new Error(`fake database: unsupported assignment "${assignment}"`)
293
+ }
294
+ fields[column[1]] = values[Number(column[2]) - 1]
295
+ }
296
+ return this.updateRow(session, table, values[Number(idParam) - 1], (row) =>
297
+ onlyWithoutPdf && row.pdf_size_bytes != null ? null : fields
298
+ )
299
+ }
300
+
301
+ match = /^DELETE FROM (\w+) WHERE (\w+) = \$1$/.exec(sql)
302
+ if (match) {
303
+ const [, table, column] = match
304
+ const rows = this.rows(table)
305
+ const removed = rows.filter((row) => String(row[column]) === String(values[0]))
306
+ this.tables[table] = rows.filter((row) => !removed.includes(row))
307
+ this.remember(session, () => {
308
+ this.tables[table].push(...removed)
309
+ })
310
+ return { rows: [], rowCount: removed.length }
311
+ }
312
+
313
+ match =
314
+ /^INSERT INTO "?(\w+)"? \((.+?)\) VALUES \((.+?)\)(?: ON CONFLICT \((\w+)\) DO NOTHING)?( RETURNING \*)?$/.exec(
315
+ sql
316
+ )
317
+ if (match) {
318
+ const [, table, columnList, placeholderList, conflictColumn, returning] = match
319
+ const columns = columnList.split(', ').map((column) => column.replace(/"/g, ''))
320
+ const placeholders = placeholderList.split(', ')
321
+ const row: Row = {}
322
+ columns.forEach((column, index) => {
323
+ const param = /^\$(\d+)$/.exec(placeholders[index])
324
+ if (!param) {
325
+ throw new Error(`fake database: unsupported value "${placeholders[index]}"`)
326
+ }
327
+ row[column] = values[Number(param[1]) - 1]
328
+ })
329
+ if (row.id === undefined) {
330
+ row.id = ++this.serial
331
+ }
332
+ for (const column of ['id', ...(UNIQUE_COLUMNS[table] || [])]) {
333
+ const taken = this.rows(table).some(
334
+ (existing) => row[column] != null && String(existing[column]) === String(row[column])
335
+ )
336
+ if (taken) {
337
+ if (conflictColumn === column) {
338
+ return { rows: [], rowCount: 0 }
339
+ }
340
+ throw uniqueViolation(table, column)
341
+ }
342
+ }
343
+ this.rows(table).push(row)
344
+ this.remember(session, () => {
345
+ this.tables[table] = this.tables[table].filter((existing) => existing !== row)
346
+ })
347
+ return { rows: returning ? [{ ...row }] : [], rowCount: 1 }
348
+ }
349
+
350
+ throw new Error(`fake database: unsupported statement: ${sql}`)
351
+ }
352
+
353
+ /**
354
+ * One row's UPDATE: waits for the row lock, then lets `change` read the row
355
+ * as it is now and answer the new values, or null when the row does not match.
356
+ */
357
+ private async updateRow(
358
+ session: Session,
359
+ table: string,
360
+ id: unknown,
361
+ change: (row: Row) => Row | null
362
+ ): Promise<QueryResult> {
363
+ const target = this.row(table, id)
364
+ if (!target) {
365
+ return { rows: [], rowCount: 0 }
366
+ }
367
+ await this.lock(session, `${table}:${String(id)}`)
368
+ const fields = change(target)
369
+ if (!fields) {
370
+ return { rows: [], rowCount: 0 }
371
+ }
372
+ const before: Row = {}
373
+ for (const column of Object.keys(fields)) {
374
+ before[column] = target[column]
375
+ }
376
+ Object.assign(target, fields)
377
+ this.remember(session, () => {
378
+ Object.assign(target, before)
379
+ })
380
+ return { rows: [{ id: target.id }], rowCount: 1 }
381
+ }
382
+
383
+ private remember(session: Session, undo: () => void): void {
384
+ if (session.inTransaction) {
385
+ session.undo.push(undo)
386
+ }
387
+ }
388
+
389
+ private async lock(session: Session, key: string): Promise<void> {
390
+ for (;;) {
391
+ const held = this.locks.get(key)
392
+ if (!held) {
393
+ let release: () => void = () => undefined
394
+ const released = new Promise<void>((resolve) => {
395
+ release = resolve
396
+ })
397
+ this.locks.set(key, { owner: session, released, release })
398
+ session.held.add(key)
399
+ return
400
+ }
401
+ if (held.owner === session) {
402
+ return
403
+ }
404
+ await held.released
405
+ }
406
+ }
407
+
408
+ private finishTransaction(session: Session, commit: boolean): void {
409
+ if (!commit) {
410
+ session.undo.reverse().forEach((undo) => undo())
411
+ }
412
+ session.undo = []
413
+ session.savepoints.clear()
414
+ session.inTransaction = false
415
+ this.releaseLocks(session)
416
+ }
417
+
418
+ private releaseLocks(session: Session): void {
419
+ for (const key of session.held) {
420
+ const held = this.locks.get(key)
421
+ if (held && held.owner === session) {
422
+ this.locks.delete(key)
423
+ held.release()
424
+ }
425
+ }
426
+ session.held.clear()
427
+ }
428
+ }