@teleporthq/teleport-plugin-next-workflows 0.44.0 → 0.44.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/_helpers/fake-store-database.ts +428 -0
- package/__tests__/_helpers/invoice-store.ts +382 -0
- package/__tests__/email-locale-senders.test.ts +3 -1
- package/__tests__/email-sender-name.test.ts +513 -0
- package/__tests__/graph-utils-topological-order.test.ts +155 -1
- package/__tests__/invoice-billing-details.test.ts +107 -1
- package/__tests__/invoice-data-access-replace.test.ts +236 -2
- package/__tests__/invoice-order-items-settle.test.ts +1 -1
- package/__tests__/invoice-payment-email.test.ts +191 -0
- package/__tests__/invoice-regenerate-email.test.ts +288 -0
- package/__tests__/invoice-regenerate-route.test.ts +9 -7
- package/__tests__/invoice-reservation.test.ts +1 -1
- package/__tests__/invoice-tender.test.ts +1 -1
- package/__tests__/segment-splitter-checkout-order.test.ts +306 -0
- package/__tests__/streaming-on-end-env-filter.test.ts +18 -18
- package/__tests__/streaming-on-end-order.test.ts +387 -0
- package/dist/cjs/account-delete-route-generator.d.ts +1 -0
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +2 -1
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts +1 -0
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +2 -1
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +9 -26
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/graph-utils.d.ts +31 -1
- package/dist/cjs/graph-utils.d.ts.map +1 -1
- package/dist/cjs/graph-utils.js +112 -10
- package/dist/cjs/graph-utils.js.map +1 -1
- package/dist/cjs/invoice/api-routes-code.d.ts +12 -3
- package/dist/cjs/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/cjs/invoice/api-routes-code.js +152 -21
- package/dist/cjs/invoice/api-routes-code.js.map +1 -1
- package/dist/cjs/invoice/data-access-code.d.ts.map +1 -1
- package/dist/cjs/invoice/data-access-code.js +133 -21
- package/dist/cjs/invoice/data-access-code.js.map +1 -1
- package/dist/cjs/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/cjs/invoice/email-sender-code.js +15 -25
- package/dist/cjs/invoice/email-sender-code.js.map +1 -1
- package/dist/cjs/invoice/index.js +1 -1
- package/dist/cjs/invoice/index.js.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-assembly-code.js +10 -6
- package/dist/cjs/invoice/invoice-assembly-code.js.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.js +3 -0
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/nodes/email/email-mailgun.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-mailgun.js +4 -2
- package/dist/cjs/nodes/email/email-mailgun.js.map +1 -1
- package/dist/cjs/nodes/email/email-postmark.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-postmark.js +5 -2
- package/dist/cjs/nodes/email/email-postmark.js.map +1 -1
- package/dist/cjs/nodes/email/email-resend.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-resend.js +5 -2
- package/dist/cjs/nodes/email/email-resend.js.map +1 -1
- package/dist/cjs/nodes/email/email-sendgrid.d.ts.map +1 -1
- package/dist/cjs/nodes/email/email-sendgrid.js +8 -2
- package/dist/cjs/nodes/email/email-sendgrid.js.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-remove.d.ts.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-remove.js +2 -0
- package/dist/cjs/nodes/storage/storage-local-remove.js.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-set.d.ts.map +1 -1
- package/dist/cjs/nodes/storage/storage-local-set.js +4 -0
- package/dist/cjs/nodes/storage/storage-local-set.js.map +1 -1
- package/dist/cjs/segment-splitter.d.ts.map +1 -1
- package/dist/cjs/segment-splitter.js +65 -27
- package/dist/cjs/segment-splitter.js.map +1 -1
- package/dist/cjs/transactional-email-code.d.ts.map +1 -1
- package/dist/cjs/transactional-email-code.js +32 -18
- package/dist/cjs/transactional-email-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +2 -0
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/account-delete-route-generator.d.ts +1 -0
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +2 -1
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts +1 -0
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +2 -1
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +9 -26
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/graph-utils.d.ts +31 -1
- package/dist/esm/graph-utils.d.ts.map +1 -1
- package/dist/esm/graph-utils.js +109 -9
- package/dist/esm/graph-utils.js.map +1 -1
- package/dist/esm/invoice/api-routes-code.d.ts +12 -3
- package/dist/esm/invoice/api-routes-code.d.ts.map +1 -1
- package/dist/esm/invoice/api-routes-code.js +152 -21
- package/dist/esm/invoice/api-routes-code.js.map +1 -1
- package/dist/esm/invoice/data-access-code.d.ts.map +1 -1
- package/dist/esm/invoice/data-access-code.js +133 -21
- package/dist/esm/invoice/data-access-code.js.map +1 -1
- package/dist/esm/invoice/email-sender-code.d.ts.map +1 -1
- package/dist/esm/invoice/email-sender-code.js +15 -25
- package/dist/esm/invoice/email-sender-code.js.map +1 -1
- package/dist/esm/invoice/index.js +1 -1
- package/dist/esm/invoice/index.js.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-assembly-code.js +10 -6
- package/dist/esm/invoice/invoice-assembly-code.js.map +1 -1
- package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-html-code.js +3 -0
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/nodes/email/email-mailgun.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-mailgun.js +4 -2
- package/dist/esm/nodes/email/email-mailgun.js.map +1 -1
- package/dist/esm/nodes/email/email-postmark.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-postmark.js +5 -2
- package/dist/esm/nodes/email/email-postmark.js.map +1 -1
- package/dist/esm/nodes/email/email-resend.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-resend.js +5 -2
- package/dist/esm/nodes/email/email-resend.js.map +1 -1
- package/dist/esm/nodes/email/email-sendgrid.d.ts.map +1 -1
- package/dist/esm/nodes/email/email-sendgrid.js +8 -2
- package/dist/esm/nodes/email/email-sendgrid.js.map +1 -1
- package/dist/esm/nodes/storage/storage-local-remove.d.ts.map +1 -1
- package/dist/esm/nodes/storage/storage-local-remove.js +2 -0
- package/dist/esm/nodes/storage/storage-local-remove.js.map +1 -1
- package/dist/esm/nodes/storage/storage-local-set.d.ts.map +1 -1
- package/dist/esm/nodes/storage/storage-local-set.js +4 -0
- package/dist/esm/nodes/storage/storage-local-set.js.map +1 -1
- package/dist/esm/segment-splitter.d.ts.map +1 -1
- package/dist/esm/segment-splitter.js +66 -28
- package/dist/esm/segment-splitter.js.map +1 -1
- package/dist/esm/transactional-email-code.d.ts.map +1 -1
- package/dist/esm/transactional-email-code.js +32 -18
- package/dist/esm/transactional-email-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +2 -0
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +3 -3
- package/src/account-delete-route-generator.ts +3 -1
- package/src/auth-generator.ts +3 -1
- package/src/executor-generator.ts +9 -26
- package/src/graph-utils.ts +143 -11
- package/src/invoice/api-routes-code.ts +166 -21
- package/src/invoice/data-access-code.ts +139 -21
- package/src/invoice/email-sender-code.ts +15 -25
- package/src/invoice/index.ts +1 -1
- package/src/invoice/invoice-assembly-code.ts +10 -6
- package/src/invoice/invoice-html-code.ts +3 -0
- package/src/nodes/email/email-mailgun.ts +11 -2
- package/src/nodes/email/email-postmark.ts +12 -2
- package/src/nodes/email/email-resend.ts +12 -2
- package/src/nodes/email/email-sendgrid.ts +15 -2
- package/src/nodes/storage/storage-local-remove.ts +4 -0
- package/src/nodes/storage/storage-local-set.ts +6 -0
- package/src/segment-splitter.ts +90 -36
- package/src/transactional-email-code.ts +32 -21
- package/src/workflow-project-plugin.ts +2 -0
|
@@ -0,0 +1,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
|
+
}
|