@oaiz/sdk 0.0.0-stage → 0.1.4131

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/dist/test.mjs ADDED
@@ -0,0 +1,2495 @@
1
+ import { join, resolve } from "node:path";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import { createHash, randomBytes } from "node:crypto";
4
+
5
+ // src/app/testmode/auth.ts
6
+ const accessTokenLifetimeSeconds = 3600;
7
+ const noStore = { "cache-control": "no-store" };
8
+ const pageTitle = "Sign in as a test member";
9
+ function createTestAuth(members) {
10
+ const codes = /* @__PURE__ */ new Map();
11
+ const accessTokens = /* @__PURE__ */ new Map();
12
+ const refreshTokens = /* @__PURE__ */ new Map();
13
+ function newSession(member, clientId, scope) {
14
+ return {
15
+ member,
16
+ clientId,
17
+ scope,
18
+ accessTokens: /* @__PURE__ */ new Set(),
19
+ refreshToken: ""
20
+ };
21
+ }
22
+ function rotate(session) {
23
+ refreshTokens.delete(session.refreshToken);
24
+ const accessToken = randomToken();
25
+ session.accessTokens.add(accessToken);
26
+ accessTokens.set(accessToken, session);
27
+ session.refreshToken = randomToken();
28
+ refreshTokens.set(session.refreshToken, session);
29
+ return accessToken;
30
+ }
31
+ function end(session) {
32
+ for (const token of session.accessTokens) accessTokens.delete(token);
33
+ session.accessTokens.clear();
34
+ refreshTokens.delete(session.refreshToken);
35
+ }
36
+ function tokenAnswer(session, accessToken) {
37
+ return json$1(200, {
38
+ access_token: accessToken,
39
+ token_type: "Bearer",
40
+ expires_in: accessTokenLifetimeSeconds,
41
+ refresh_token: session.refreshToken,
42
+ scope: session.scope
43
+ }, noStore);
44
+ }
45
+ function discovery(request) {
46
+ const issuer = `${new URL(request.url).origin}/__oaiz-test/oauth`;
47
+ return json$1(200, {
48
+ issuer,
49
+ authorization_endpoint: `${issuer}/authorize`,
50
+ token_endpoint: `${issuer}/token`,
51
+ userinfo_endpoint: `${issuer}/userinfo`,
52
+ revocation_endpoint: `${issuer}/revoke`,
53
+ response_types_supported: ["code"],
54
+ grant_types_supported: ["authorization_code", "refresh_token"],
55
+ code_challenge_methods_supported: ["S256"]
56
+ });
57
+ }
58
+ function authorizePage(request) {
59
+ const parameters = new URL(request.url).searchParams;
60
+ const read = readAuthorizationRequest(parameters, new URL(request.url).origin);
61
+ if (read instanceof Response) return read;
62
+ const hidden = [
63
+ ["response_type", "code"],
64
+ ["client_id", read.clientId],
65
+ ["redirect_uri", read.redirectUri],
66
+ ["scope", read.scope],
67
+ ["code_challenge", read.challenge],
68
+ ["code_challenge_method", "S256"],
69
+ ...read.state === null ? [] : [["state", read.state]]
70
+ ].map(([name, value]) => `<input type="hidden" name="${name}" value="${escapeHTML(value)}">`).join("");
71
+ const seeded = members();
72
+ const forms = seeded.length === 0 ? "<p>No members can sign in. Add them to tests/members.json.</p>" : seeded.map((member) => `<form method="post" action="/__oaiz-test/oauth/authorize">${hidden}<button type="submit" name="member" value="${escapeHTML(member.id)}">${escapeHTML(member.name)}</button></form>`).join("\n");
73
+ const body = `<!doctype html>
74
+ <html lang="en">
75
+ <head><meta charset="utf-8"><title>${pageTitle}</title></head>
76
+ <body><main><h1>${pageTitle}</h1>
77
+ ${forms}
78
+ </main></body>
79
+ </html>
80
+ `;
81
+ return new Response(body, {
82
+ status: 200,
83
+ headers: {
84
+ "content-type": "text/html; charset=utf-8",
85
+ ...noStore
86
+ }
87
+ });
88
+ }
89
+ async function approve(request) {
90
+ const form = new URLSearchParams(await request.text());
91
+ const read = readAuthorizationRequest(form, new URL(request.url).origin);
92
+ if (read instanceof Response) return read;
93
+ const member = members().find((candidate) => candidate.id === form.get("member"));
94
+ if (member === void 0) return oauthError(400, "invalid_request", "member is not in tests/members.json");
95
+ const code = randomToken();
96
+ codes.set(code, {
97
+ member,
98
+ clientId: read.clientId,
99
+ redirectUri: read.redirectUri,
100
+ challenge: read.challenge,
101
+ scope: read.scope
102
+ });
103
+ const target = new URL(read.redirectUri);
104
+ target.searchParams.set("code", code);
105
+ if (read.state !== null) target.searchParams.set("state", read.state);
106
+ target.searchParams.set("iss", `${new URL(request.url).origin}/__oaiz-test/oauth`);
107
+ return new Response(null, {
108
+ status: 303,
109
+ headers: { location: target.toString() }
110
+ });
111
+ }
112
+ async function token(request) {
113
+ const form = new URLSearchParams(await request.text());
114
+ const clientId = form.get("client_id") ?? "";
115
+ switch (form.get("grant_type")) {
116
+ case "authorization_code": {
117
+ const code = form.get("code") ?? "";
118
+ const grant = codes.get(code);
119
+ if (grant === void 0 || grant.clientId !== clientId || grant.redirectUri !== form.get("redirect_uri") || s256(form.get("code_verifier") ?? "") !== grant.challenge) return oauthError(400, "invalid_grant", "the code is unknown, used, or not this request's");
120
+ codes.delete(code);
121
+ const session = newSession(grant.member, grant.clientId, grant.scope);
122
+ return tokenAnswer(session, rotate(session));
123
+ }
124
+ case "refresh_token": {
125
+ const session = refreshTokens.get(form.get("refresh_token") ?? "");
126
+ if (session === void 0 || session.clientId !== clientId) return oauthError(400, "invalid_grant", "the refresh token is unknown, rotated, or revoked");
127
+ return tokenAnswer(session, rotate(session));
128
+ }
129
+ default: return oauthError(400, "unsupported_grant_type", "grant_type must be authorization_code or refresh_token");
130
+ }
131
+ }
132
+ function authenticate(request) {
133
+ const presented = bearerToken(request.headers.get("authorization"));
134
+ if (presented === void 0) return json$1(401, {
135
+ message: "Authorization header not provided",
136
+ slug: "no-authorization-header"
137
+ }, { "www-authenticate": "Bearer" });
138
+ const session = accessTokens.get(presented);
139
+ if (session === void 0) return json$1(401, {
140
+ message: "authentication: Invalid token",
141
+ slug: "invalid-token"
142
+ }, { "www-authenticate": "Bearer error=\"invalid_token\"" });
143
+ return session;
144
+ }
145
+ function userinfo(request) {
146
+ const session = authenticate(request);
147
+ if (session instanceof Response) return session;
148
+ const { member } = session;
149
+ return json$1(200, {
150
+ sub: member.id,
151
+ name: member.name,
152
+ email: member.email,
153
+ email_verified: true
154
+ });
155
+ }
156
+ async function revoke(request) {
157
+ const form = new URLSearchParams(await request.text());
158
+ const presented = form.get("token") ?? "";
159
+ if (presented === "") return oauthError(400, "invalid_request", "token is required");
160
+ const session = refreshTokens.get(presented) ?? accessTokens.get(presented);
161
+ if (session !== void 0 && session.clientId === form.get("client_id")) end(session);
162
+ return new Response(null, {
163
+ status: 200,
164
+ headers: noStore
165
+ });
166
+ }
167
+ return {
168
+ async handle(request, route) {
169
+ const method = request.method;
170
+ if (route === ".well-known/openid-configuration" && method === "GET") return discovery(request);
171
+ if (route === "authorize" && method === "GET") return authorizePage(request);
172
+ if (route === "authorize" && method === "POST") return approve(request);
173
+ if (route === "token" && method === "POST") return token(request);
174
+ if (route === "userinfo" && (method === "GET" || method === "POST")) return userinfo(request);
175
+ if (route === "revoke" && method === "POST") return revoke(request);
176
+ },
177
+ issueToken(memberId, clientId) {
178
+ const member = members().find((candidate) => candidate.id === memberId);
179
+ if (member === void 0) throw new Error(`tests/members.json has no member ${memberId}`);
180
+ return rotate(newSession(member, clientId, "openid email profile"));
181
+ },
182
+ authenticate
183
+ };
184
+ }
185
+ function readAuthorizationRequest(parameters, origin) {
186
+ if (parameters.get("response_type") !== "code") return oauthError(400, "invalid_request", "response_type must be code");
187
+ const clientId = parameters.get("client_id") ?? "";
188
+ if (clientId === "") return oauthError(400, "invalid_request", "client_id is required");
189
+ const redirectUri = parameters.get("redirect_uri") ?? "";
190
+ if (!URL.canParse(redirectUri) || new URL(redirectUri).origin !== origin) return oauthError(400, "invalid_request", `redirect_uri must be a page on ${origin}`);
191
+ const challenge = parameters.get("code_challenge") ?? "";
192
+ if (challenge === "" || parameters.get("code_challenge_method") !== "S256") return oauthError(400, "invalid_request", "an S256 code_challenge is required");
193
+ return {
194
+ clientId,
195
+ redirectUri,
196
+ state: parameters.get("state"),
197
+ challenge,
198
+ scope: parameters.get("scope") ?? "openid"
199
+ };
200
+ }
201
+ function bearerToken(header) {
202
+ const separator = header?.indexOf(" ") ?? -1;
203
+ if (header === null || separator < 0) return void 0;
204
+ const token = header.slice(separator + 1);
205
+ return header.slice(0, separator).toLowerCase() === "bearer" && token !== "" ? token : void 0;
206
+ }
207
+ function s256(verifier) {
208
+ return createHash("sha256").update(verifier).digest("base64url");
209
+ }
210
+ function randomToken() {
211
+ return randomBytes(32).toString("base64url");
212
+ }
213
+ function oauthError(status, error, description) {
214
+ return json$1(status, {
215
+ error,
216
+ error_description: description
217
+ }, noStore);
218
+ }
219
+ function json$1(status, body, headers = {}) {
220
+ return new Response(JSON.stringify(body), {
221
+ status,
222
+ headers: {
223
+ "content-type": "application/json; charset=utf-8",
224
+ ...headers
225
+ }
226
+ });
227
+ }
228
+ function escapeHTML(value) {
229
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
230
+ }
231
+
232
+ // src/app/testmode/rows/errors.ts
233
+ const slugTableRequestInvalid = "storage-table-request-invalid";
234
+ const slugTableNotFound = "storage-table-not-found";
235
+ const slugStatementTimeout = "storage-statement-timeout";
236
+ const slugCustomerSQLFailed = "storage-customer-sql-failed";
237
+ const slugCustomerSQLPermissionDenied = "storage-customer-sql-permission-denied";
238
+ const slugBoundedTransactionFailed = "storage-bounded-transaction-failed";
239
+ const slugFunctionNotFound = "storage-function-not-found";
240
+ const slugFunctionCallInvalid = "storage-function-call-invalid";
241
+ const slugFunctionResultTooLarge = "storage-function-result-too-large";
242
+ const slugDatabaseEditRequired = "storage-database-edit-required";
243
+ var RowsRefusal = class extends Error {
244
+ constructor(status, slug, message, errors) {
245
+ super(message);
246
+ this.status = status;
247
+ this.slug = slug;
248
+ this.errors = errors;
249
+ }
250
+ body() {
251
+ return this.errors ? {
252
+ message: this.message,
253
+ slug: this.slug,
254
+ errors: this.errors
255
+ } : {
256
+ message: this.message,
257
+ slug: this.slug
258
+ };
259
+ }
260
+ };
261
+ function incorrectInput(message, slug) {
262
+ return new RowsRefusal(400, slug, `incorrect-input: ${message}`);
263
+ }
264
+ function forbidden(message, slug) {
265
+ return new RowsRefusal(403, slug, `forbidden: ${message}`);
266
+ }
267
+ function notFound(message, slug) {
268
+ return new RowsRefusal(404, slug, `not-found: ${message}`);
269
+ }
270
+ function externalService(message, slug) {
271
+ return new RowsRefusal(500, slug, `external-service: ${message}`);
272
+ }
273
+ function tableRequestError(kind, message) {
274
+ return new RowsRefusal(400, kind === "table-not-found" ? slugTableNotFound : slugTableRequestInvalid, `incorrect-input: ${message}`);
275
+ }
276
+ function tableNotFoundError(table) {
277
+ return tableRequestError("table-not-found", `the table ${table} does not exist`);
278
+ }
279
+ function columnNotFoundError(table, column) {
280
+ return tableRequestError("column-not-found", `column ${table}.${column} does not exist`);
281
+ }
282
+ function embedPathNotFoundError(path) {
283
+ return tableRequestError("embed-path-not-found", `${path} is not an embedded table of this request; embed it in the select`);
284
+ }
285
+ function invalidBodyError(message) {
286
+ return tableRequestError("invalid-body", message);
287
+ }
288
+ function payloadColumnNotFoundError(table, column) {
289
+ return tableRequestError("payload-column-not-found", `the table ${table} has no column ${column}`);
290
+ }
291
+ function filterRequiredError(kind) {
292
+ return tableRequestError("filter-required", `${kind.toUpperCase()} requires a WHERE clause`);
293
+ }
294
+ function limitError(message) {
295
+ return tableRequestError("limit-exceeded", message);
296
+ }
297
+ function singularMismatchError(rows) {
298
+ return tableRequestError("singular-mismatch", `single requires exactly one row, and the result has ${rows}`);
299
+ }
300
+ function statementFailure(sqlState, message, hint) {
301
+ let text = `${message} (sqlstate ${sqlState})`;
302
+ if (hint) text += `\nHint: ${hint}`;
303
+ if (sqlState === "42501") return forbidden(text, slugCustomerSQLPermissionDenied);
304
+ return incorrectInput(text, slugCustomerSQLFailed);
305
+ }
306
+ const statementTimeoutLimit = "30s";
307
+ function statementTimeoutError() {
308
+ return new RowsRefusal(503, slugStatementTimeout, `statement-timeout: the statement exceeded the statement timeout of ${"30s"}`);
309
+ }
310
+ function boundedTxFailure(message, sqlState) {
311
+ return externalService(`${message} (sqlstate ${sqlState})`, slugBoundedTransactionFailed);
312
+ }
313
+ function functionNotFoundError(name) {
314
+ return notFound(`the function ${name} does not exist in the database`, slugFunctionNotFound);
315
+ }
316
+ function functionCallInvalidError(message) {
317
+ return incorrectInput(message, slugFunctionCallInvalid);
318
+ }
319
+ function functionResultTooLargeError(maxRows) {
320
+ return incorrectInput(`the function returned more than ${maxRows} rows; return fewer rows`, slugFunctionResultTooLarge);
321
+ }
322
+ function databaseEditRequiredError() {
323
+ return forbidden("editing this database requires edit access", slugDatabaseEditRequired);
324
+ }
325
+
326
+ // src/app/testmode/database.ts
327
+ const emptyDatabaseSQL = `CREATE ROLE "oaiz_test_read" NOLOGIN;
328
+ CREATE ROLE "oaiz_test_write" NOLOGIN;
329
+ CREATE ROLE "oaiz_test_owner" NOLOGIN;
330
+ CREATE SCHEMA "data";
331
+ CREATE SCHEMA "oaiz_private";
332
+ GRANT USAGE ON SCHEMA "data" TO "oaiz_test_read", "oaiz_test_write", "oaiz_test_owner";
333
+ GRANT USAGE ON SCHEMA "oaiz_private" TO "oaiz_test_read", "oaiz_test_write", "oaiz_test_owner";`;
334
+ const int8 = 20;
335
+ const vectorIndexesSQL = `SELECT 1 FROM pg_catalog.pg_class c
336
+ JOIN pg_catalog.pg_am a ON a.oid = c.relam
337
+ JOIN pg_catalog.pg_proc p ON p.oid = a.amhandler
338
+ WHERE a.amtype = 'i'
339
+ AND p.prosrc NOT IN ('bthandler', 'hashhandler', 'gisthandler', 'ginhandler', 'spghandler', 'brinhandler', 'hnswhandler')
340
+ LIMIT 1`;
341
+ function requireSupportedIndexes(indexes) {
342
+ if (indexes.length > 0) throw incorrectInput("IVFFlat indexes are refused; use HNSW for a supported vector index", "storage-vector-index-refused");
343
+ }
344
+ async function managedExtensions() {
345
+ const [{ btree_gin }, { btree_gist }, { citext }, { fuzzystrmatch }, { hstore }, { intarray }, { ltree }, { pg_trgm }, { pgcrypto }, { unaccent }, { uuid_ossp }, { vector }] = await Promise.all([
346
+ import("@electric-sql/pglite/contrib/btree_gin"),
347
+ import("@electric-sql/pglite/contrib/btree_gist"),
348
+ import("@electric-sql/pglite/contrib/citext"),
349
+ import("@electric-sql/pglite/contrib/fuzzystrmatch"),
350
+ import("@electric-sql/pglite/contrib/hstore"),
351
+ import("@electric-sql/pglite/contrib/intarray"),
352
+ import("@electric-sql/pglite/contrib/ltree"),
353
+ import("@electric-sql/pglite/contrib/pg_trgm"),
354
+ import("@electric-sql/pglite/contrib/pgcrypto"),
355
+ import("@electric-sql/pglite/contrib/unaccent"),
356
+ import("@electric-sql/pglite/contrib/uuid_ossp"),
357
+ import("@electric-sql/pglite-pgvector")
358
+ ]);
359
+ return {
360
+ btree_gin,
361
+ btree_gist,
362
+ citext,
363
+ fuzzystrmatch,
364
+ hstore,
365
+ intarray,
366
+ ltree,
367
+ pg_trgm,
368
+ pgcrypto,
369
+ unaccent,
370
+ uuid_ossp,
371
+ vector
372
+ };
373
+ }
374
+ async function openTestDatabase(schemaSQL, what) {
375
+ const { PGlite } = await import("@electric-sql/pglite");
376
+ const pg = await PGlite.create({ extensions: await managedExtensions() });
377
+ await pg.exec("SET TimeZone TO 'UTC'");
378
+ try {
379
+ await pg.exec(schemaSQL.trim() === "" ? emptyDatabaseSQL : schemaSQL);
380
+ requireSupportedIndexes((await pg.query(vectorIndexesSQL)).rows);
381
+ } catch (error) {
382
+ await pg.close();
383
+ if (error instanceof RowsRefusal) throw error;
384
+ const reason = error.message;
385
+ throw new Error(`${what} does not run on the test database: ${reason}`, { cause: error });
386
+ }
387
+ const roles = (await pg.query("SELECT rolname FROM pg_catalog.pg_roles WHERE rolname IN ('oaiz_test_read', 'oaiz_test_write')")).rows.map((row) => row.rolname);
388
+ const missing = ["oaiz_test_read", "oaiz_test_write"].find((role) => !roles.includes(role));
389
+ if (missing !== void 0) {
390
+ await pg.close();
391
+ throw new Error(`${what} creates no role ${missing}, so it is not the output of \`oaiz db test-schema\`. Write the file with \`oaiz db test-schema <database-id> -o ${what}\`; a hand-written CREATE TABLE file does not work.`);
392
+ }
393
+ const asText = (value) => value;
394
+ const serializers = Object.fromEntries(Object.keys(pg.serializers).map((type) => [type, asText]));
395
+ const session = {
396
+ async query(sql, args) {
397
+ return (await pg.query(sql, args, {
398
+ rowMode: "array",
399
+ parsers: { [int8]: (value) => value },
400
+ serializers
401
+ })).rows;
402
+ },
403
+ async exec(sql) {
404
+ await pg.exec(sql);
405
+ }
406
+ };
407
+ let queue = Promise.resolve();
408
+ let failedReset;
409
+ const serialized = (fn) => {
410
+ const run = queue.then(fn);
411
+ queue = run.catch(() => void 0);
412
+ return run;
413
+ };
414
+ const exclusive = (fn) => serialized(() => {
415
+ if (failedReset !== void 0) throw failedReset;
416
+ return fn(session);
417
+ });
418
+ return {
419
+ exclusive,
420
+ reset: (seedSQL) => exclusive(async (session) => {
421
+ await session.exec("BEGIN");
422
+ try {
423
+ await resetRows(session, seedSQL);
424
+ requireSupportedIndexes(await session.query(vectorIndexesSQL, []));
425
+ await session.exec("COMMIT");
426
+ } catch (error) {
427
+ failedReset = new Error("the test database was discarded because its reset failed", { cause: error });
428
+ await session.exec("ROLLBACK");
429
+ throw error;
430
+ }
431
+ }),
432
+ close: () => serialized(() => pg.close())
433
+ };
434
+ }
435
+ async function resetRows(session, seedSQL) {
436
+ const every = await session.query("SELECT format('data.%I', tablename) FROM pg_catalog.pg_tables WHERE schemaname = 'data' ORDER BY tablename", []);
437
+ if (every.length > 0) await session.query(`TRUNCATE ${every.map(([name]) => name).join(", ")} RESTART IDENTITY CASCADE`, []);
438
+ if (seedSQL.trim() !== "") await session.exec(seedSQL);
439
+ }
440
+
441
+ // src/app/testmode/json.ts
442
+ var JsonNumber = class {
443
+ constructor(text) {
444
+ this.text = text;
445
+ }
446
+ };
447
+ var JsonSyntaxError = class extends Error {};
448
+ function parseJson(text) {
449
+ const reader = new JsonReader(text);
450
+ const value = reader.value();
451
+ reader.space();
452
+ if (!reader.atEnd()) throw reader.unexpected("after top-level value");
453
+ return value;
454
+ }
455
+ function parseFirstJson(text) {
456
+ const reader = new JsonReader(text);
457
+ reader.space();
458
+ if (reader.atEnd()) throw new JsonSyntaxError("EOF");
459
+ return reader.value();
460
+ }
461
+ var JsonReader = class {
462
+ constructor(text) {
463
+ this.text = text;
464
+ this.position = 0;
465
+ }
466
+ atEnd() {
467
+ return this.position >= this.text.length;
468
+ }
469
+ space() {
470
+ while (!this.atEnd() && " \r\n".includes(this.text[this.position])) this.position++;
471
+ }
472
+ unexpected(context) {
473
+ if (this.atEnd()) return new JsonSyntaxError("unexpected EOF");
474
+ return new JsonSyntaxError(`invalid character '${this.text[this.position]}' ${context}`);
475
+ }
476
+ value() {
477
+ this.space();
478
+ const char = this.text[this.position];
479
+ if (char === "{") return this.object();
480
+ if (char === "[") return this.array();
481
+ if (char === "\"") return this.string();
482
+ if (char === "-" || char >= "0" && char <= "9") return this.number();
483
+ for (const [word, value] of [
484
+ ["true", true],
485
+ ["false", false],
486
+ ["null", null]
487
+ ]) if (this.text.startsWith(word, this.position)) {
488
+ this.position += word.length;
489
+ return value;
490
+ }
491
+ throw this.unexpected("looking for beginning of value");
492
+ }
493
+ object() {
494
+ this.position++;
495
+ const object = /* @__PURE__ */ new Map();
496
+ this.space();
497
+ if (this.text[this.position] === "}") {
498
+ this.position++;
499
+ return object;
500
+ }
501
+ for (;;) {
502
+ this.space();
503
+ if (this.text[this.position] !== "\"") throw this.unexpected("looking for beginning of object key string");
504
+ const key = this.string();
505
+ this.space();
506
+ if (this.text[this.position] !== ":") throw this.unexpected("after object key");
507
+ this.position++;
508
+ object.set(key, this.value());
509
+ this.space();
510
+ const next = this.text[this.position];
511
+ this.position++;
512
+ if (next === "}") return object;
513
+ if (next !== ",") {
514
+ this.position--;
515
+ throw this.unexpected("after object key:value pair");
516
+ }
517
+ }
518
+ }
519
+ array() {
520
+ this.position++;
521
+ const array = [];
522
+ this.space();
523
+ if (this.text[this.position] === "]") {
524
+ this.position++;
525
+ return array;
526
+ }
527
+ for (;;) {
528
+ array.push(this.value());
529
+ this.space();
530
+ const next = this.text[this.position];
531
+ this.position++;
532
+ if (next === "]") return array;
533
+ if (next !== ",") {
534
+ this.position--;
535
+ throw this.unexpected("after array element");
536
+ }
537
+ }
538
+ }
539
+ string() {
540
+ const start = this.position;
541
+ this.position++;
542
+ for (;;) {
543
+ if (this.atEnd()) throw new JsonSyntaxError("unexpected EOF");
544
+ const char = this.text[this.position];
545
+ if (char === "\"") break;
546
+ if (char < " ") throw this.unexpected("in string literal");
547
+ this.position += char === "\\" ? 2 : 1;
548
+ }
549
+ this.position++;
550
+ try {
551
+ return JSON.parse(this.text.slice(start, this.position));
552
+ } catch {
553
+ this.position = start;
554
+ throw this.unexpected("in string escape code");
555
+ }
556
+ }
557
+ number() {
558
+ const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(this.text.slice(this.position));
559
+ if (!match) {
560
+ this.position++;
561
+ throw this.unexpected("in numeric literal");
562
+ }
563
+ this.position += match[0].length;
564
+ return new JsonNumber(match[0]);
565
+ }
566
+ };
567
+ function stringifyJson(value) {
568
+ if (value === null || value === void 0) return "null";
569
+ if (value instanceof JsonNumber) return value.text;
570
+ switch (typeof value) {
571
+ case "boolean": return value ? "true" : "false";
572
+ case "bigint": return value.toString();
573
+ case "number":
574
+ if (!Number.isFinite(value)) throw new TypeError(`${value} is not a JSON number`);
575
+ return JSON.stringify(value);
576
+ case "string": return stringifyString(value);
577
+ }
578
+ if (value instanceof Date) return stringifyString(value.toISOString());
579
+ if (Array.isArray(value)) return `[${value.map((element) => stringifyJson(element)).join(",")}]`;
580
+ return `{${(value instanceof Map ? [...value.entries()] : Object.entries(value)).filter(([, member]) => member !== void 0).sort(([a], [b]) => compareCodePoints(a, b)).map(([key, member]) => `${stringifyString(key)}:${stringifyJson(member)}`).join(",")}}`;
581
+ }
582
+ function stringifyString(value) {
583
+ return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (char) => {
584
+ return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
585
+ });
586
+ }
587
+ function compareCodePoints(a, b) {
588
+ const left = Array.from(a);
589
+ const right = Array.from(b);
590
+ for (let index = 0; index < Math.min(left.length, right.length); index++) {
591
+ const difference = left[index].codePointAt(0) - right[index].codePointAt(0);
592
+ if (difference !== 0) return difference;
593
+ }
594
+ return left.length - right.length;
595
+ }
596
+
597
+ // src/app/testmode/members.ts
598
+ const defaultGrant = "edit";
599
+ function parseMembers(text, what) {
600
+ let document;
601
+ try {
602
+ document = JSON.parse(text);
603
+ } catch (error) {
604
+ throw new Error(`${what} is not JSON: ${error.message}`, { cause: error });
605
+ }
606
+ const root = object(document, what, membersShape, [
607
+ "teams",
608
+ "members",
609
+ "grants"
610
+ ]);
611
+ const teams = array(root.teams ?? [], `${what} teams`).map((team, index) => {
612
+ const fields = object(team, `${what} teams[${index}]`, teamShape, ["id", "name"]);
613
+ return {
614
+ id: nonEmpty(fields.id, `${what} teams[${index}].id`),
615
+ name: nonEmpty(fields.name, `${what} teams[${index}].name`)
616
+ };
617
+ });
618
+ const members = array(root.members ?? [], `${what} members`).map((member, index) => {
619
+ const where = `${what} members[${index}]`;
620
+ const fields = object(member, where, memberShape, [
621
+ "id",
622
+ "name",
623
+ "email",
624
+ "teams",
625
+ "role"
626
+ ]);
627
+ const read = {
628
+ id: nonEmpty(fields.id, `${where}.id`),
629
+ name: nonEmpty(fields.name, `${where}.name`),
630
+ email: nonEmpty(fields.email, `${where}.email`)
631
+ };
632
+ if (fields.teams !== void 0) read.teams = array(fields.teams, `${where}.teams`).map((team, at) => nonEmpty(team, `${where}.teams[${at}]`));
633
+ if (fields.role !== void 0) {
634
+ if (fields.role !== "member" && fields.role !== "administrator") throw new Error(`${where}.role is "member" or "administrator"`);
635
+ read.role = fields.role;
636
+ }
637
+ return read;
638
+ });
639
+ const grants = {};
640
+ for (const [database, grant] of Object.entries(object(root.grants ?? {}, `${what} grants`, grantsShape))) {
641
+ if (grant !== "view" && grant !== "edit") throw new Error(`${what} grants.${database} is "view" or "edit"`);
642
+ grants[database.toLowerCase()] = grant;
643
+ }
644
+ return {
645
+ teams,
646
+ members,
647
+ grants
648
+ };
649
+ }
650
+ const teamShape = "{ \"id\": \"<uuid>\", \"name\": \"Sales\" }";
651
+ const memberShape = "{ \"id\": \"<uuid>\", \"name\": \"Ada Lovelace\", \"email\": \"ada@example.test\", \"teams\": [\"<team uuid>\"], \"role\": \"member\" }";
652
+ const grantsShape = "{ \"<database-id>\": \"view\" }";
653
+ const membersShape = `{ "teams": [${teamShape}], "members": [${memberShape}], "grants": ${grantsShape} }`;
654
+ function object(value, what, shape, fields) {
655
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${what} is not an object such as ${shape}`);
656
+ for (const key of Object.keys(value)) if (fields !== void 0 && !fields.includes(key)) throw new Error(`${what} has the unknown field ${key}`);
657
+ return value;
658
+ }
659
+ function array(value, what) {
660
+ if (!Array.isArray(value)) throw new Error(`${what} is not an array`);
661
+ return value;
662
+ }
663
+ function nonEmpty(value, what) {
664
+ if (typeof value !== "string" || value === "") throw new Error(`${what} is not a text`);
665
+ return value;
666
+ }
667
+
668
+ // src/app/testmode/rows/table.ts
669
+ const maxTableInValues = 1e3;
670
+ const maxTableNodeRows = 1e3;
671
+ const maxTableDocumentRows = maxTableNodeRows * maxTableNodeRows;
672
+ const maxTableAliasBytes = 63;
673
+ function emptyFilter() {
674
+ return {
675
+ or: false,
676
+ negated: false,
677
+ terms: []
678
+ };
679
+ }
680
+ function validateTableAlias(alias) {
681
+ if (new TextEncoder().encode(alias).length > maxTableAliasBytes || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(alias)) throw tableRequestError("invalid", "the alias must be a letter or underscore followed by letters, digits or underscores, at most 63 bytes");
682
+ }
683
+
684
+ // src/app/testmode/rows/catalog.ts
685
+ const tableCatalogQuery = `WITH RECURSIVE reached(oid, steps) AS (
686
+ SELECT c.oid, 0
687
+ FROM pg_class c
688
+ WHERE c.relname = $1::text AND c.relnamespace = to_regnamespace('data') AND c.relkind IN ('r', 'p', 'v')
689
+ UNION
690
+ SELECT other.oid, r.steps + 1
691
+ FROM reached r
692
+ JOIN pg_constraint con ON con.contype = 'f' AND con.conparentid = 0 AND r.oid IN (con.conrelid, con.confrelid)
693
+ JOIN pg_class other ON other.oid = CASE WHEN con.conrelid = r.oid THEN con.confrelid ELSE con.conrelid END
694
+ WHERE r.steps < $2::integer
695
+ AND other.relnamespace = to_regnamespace('data') AND other.relkind IN ('r', 'p')
696
+ )
697
+ SELECT coalesce(json_agg(json_build_object(
698
+ 'name', c.relname,
699
+ 'kind', CASE WHEN c.relkind = 'v' THEN 'view' ELSE 'table' END,
700
+ 'columns', (
701
+ SELECT coalesce(json_agg(json_build_object(
702
+ 'name', a.attname,
703
+ 'type', format_type(a.atttypid, a.atttypmod)
704
+ ) ORDER BY a.attnum), '[]')
705
+ FROM pg_attribute a
706
+ WHERE a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped
707
+ ),
708
+ 'constraints', (
709
+ SELECT coalesce(json_agg(json_build_object(
710
+ 'name', con.conname,
711
+ 'kind', con.contype,
712
+ 'columns', (
713
+ SELECT json_agg(ka.attname ORDER BY k.ord)
714
+ FROM unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord)
715
+ JOIN pg_attribute ka ON ka.attrelid = con.conrelid AND ka.attnum = k.attnum
716
+ ),
717
+ 'referencedTable', rc.relname,
718
+ 'referencedColumns', (
719
+ SELECT json_agg(fa.attname ORDER BY f.ord)
720
+ FROM unnest(con.confkey) WITH ORDINALITY AS f(attnum, ord)
721
+ JOIN pg_attribute fa ON fa.attrelid = con.confrelid AND fa.attnum = f.attnum
722
+ )
723
+ ) ORDER BY con.conname), '[]')
724
+ FROM pg_constraint con
725
+ LEFT JOIN pg_class rc ON rc.oid = con.confrelid
726
+ WHERE con.conrelid = c.oid
727
+ AND con.contype IN ('p', 'u', 'f')
728
+ AND (con.contype <> 'f' OR (rc.relnamespace = c.relnamespace AND con.conparentid = 0))
729
+ )
730
+ ) ORDER BY c.relname), '[]')::text
731
+ FROM pg_class c
732
+ WHERE c.oid IN (SELECT oid FROM reached)`;
733
+ function tableCatalogArguments(table, depth) {
734
+ return [table.includes("\0") ? "" : table.replace(/\p{Cs}/gu, "�"), String(depth)];
735
+ }
736
+ function catalogDepth(select, depth = 0) {
737
+ if (depth === 3) return depth;
738
+ let deepest = depth;
739
+ for (const item of select) if (item.embed !== void 0) deepest = Math.max(deepest, catalogDepth(item.embed.select, depth + 1));
740
+ return deepest;
741
+ }
742
+ function decodeTableCatalog(document) {
743
+ const rows = JSON.parse(document);
744
+ return new TableCatalog(rows.map((row) => {
745
+ const table = {
746
+ name: row.name,
747
+ kind: row.kind === "view" ? "view" : "table",
748
+ columns: row.columns,
749
+ primaryKey: [],
750
+ uniqueKeys: [],
751
+ foreignKeys: []
752
+ };
753
+ for (const constraint of row.constraints) {
754
+ if (constraint.kind === "p") table.primaryKey = constraint.columns ?? [];
755
+ if (constraint.kind === "u") table.uniqueKeys.push(constraint.columns ?? []);
756
+ if (constraint.kind === "f") table.foreignKeys.push({
757
+ name: constraint.name,
758
+ columns: constraint.columns ?? [],
759
+ referencedTable: constraint.referencedTable ?? "",
760
+ referencedColumns: constraint.referencedColumns ?? []
761
+ });
762
+ }
763
+ return table;
764
+ }));
765
+ }
766
+ var TableCatalog = class {
767
+ constructor(tables) {
768
+ this.tables = tables;
769
+ }
770
+ table(name) {
771
+ return this.tables.find((table) => table.name === name);
772
+ }
773
+ relationship(origin, target, hint) {
774
+ const matches = this.relationshipsFrom(origin).filter((relationship) => relationshipMatches(relationship, target, hint));
775
+ if (matches.length === 1) return matches[0];
776
+ if (matches.length === 0) throw tableRequestError("relationship-not-found", `no foreign key relates the tables ${origin} and ${target}`);
777
+ const choices = matches.map((match) => `${match.foreignTable}!${match.constraint}`);
778
+ throw tableRequestError("relationship-ambiguous", `more than one foreign key relates the tables ${origin} and ${target}; name one in the select as ${choices.join(" or ")}`);
779
+ }
780
+ relationshipsFrom(origin) {
781
+ const relationships = [];
782
+ for (const holder of this.tables) for (const foreignKey of holder.foreignKeys) {
783
+ const oneToOne = uniqueSet(holder, foreignKey.columns);
784
+ if (holder.name === origin) relationships.push({
785
+ table: origin,
786
+ foreignTable: foreignKey.referencedTable,
787
+ constraint: foreignKey.name,
788
+ cardinality: oneToOne ? "one-to-one" : "many-to-one",
789
+ columns: foreignKey.columns,
790
+ foreignColumns: foreignKey.referencedColumns
791
+ });
792
+ if (foreignKey.referencedTable === origin) relationships.push({
793
+ table: origin,
794
+ foreignTable: holder.name,
795
+ constraint: foreignKey.name,
796
+ cardinality: oneToOne ? "one-to-one" : "one-to-many",
797
+ columns: foreignKey.referencedColumns,
798
+ foreignColumns: foreignKey.columns
799
+ });
800
+ }
801
+ return relationships;
802
+ }
803
+ };
804
+ function toOne(relationship) {
805
+ return relationship.cardinality !== "one-to-many";
806
+ }
807
+ function catalogColumn(table, name) {
808
+ return table.columns.find((column) => column.name === name);
809
+ }
810
+ function uniqueSet(table, columns) {
811
+ return [table.primaryKey, ...table.uniqueKeys].some((key) => sameColumnSet(key, columns));
812
+ }
813
+ function sameColumnSet(a, b) {
814
+ if (a.length === 0 || a.length !== b.length) return false;
815
+ const sortedB = [...b].sort();
816
+ return [...a].sort().every((column, index) => column === sortedB[index]);
817
+ }
818
+ function relationshipMatches(relationship, target, hint) {
819
+ const singleColumn = (name) => relationship.columns.length === 1 && relationship.columns[0] === name;
820
+ const singleForeignColumn = (name) => relationship.foreignColumns.length === 1 && relationship.foreignColumns[0] === name;
821
+ if (relationship.table === relationship.foreignTable) {
822
+ if (hint === "") return target === relationship.foreignTable && relationship.cardinality === "one-to-many" || singleColumn(target) && relationship.cardinality === "many-to-one";
823
+ return target === relationship.foreignTable && relationship.cardinality === "one-to-many" && singleForeignColumn(hint);
824
+ }
825
+ if (hint === "") return target === relationship.foreignTable || target === relationship.constraint || singleColumn(target);
826
+ return target === relationship.foreignTable && (hint === relationship.constraint || singleColumn(hint) || singleForeignColumn(hint));
827
+ }
828
+
829
+ // src/app/testmode/rows/resolve.ts
830
+ function isAggregated(node) {
831
+ return node.columns.some((column) => column.aggregate !== void 0);
832
+ }
833
+ function groupColumns(node) {
834
+ const columns = node.columns.filter((column) => column.aggregate === void 0);
835
+ return [...new Set(columns.map((column) => column.name))];
836
+ }
837
+ function resolveTableRead(read, catalog) {
838
+ if (read.limit !== void 0 && read.limit < 0) throw tableRequestError("invalid", "the limit must not be negative");
839
+ if (read.offset < BigInt(0)) throw tableRequestError("invalid", "the offset must not be negative");
840
+ if (read.select.some((item) => item.aggregate !== void 0)) {
841
+ if (read.select.some((item) => item.star === true || item.embed !== void 0)) throw tableRequestError("unsupported", "an aggregate cannot be read beside * or an embed");
842
+ }
843
+ const table = catalog.table(read.table);
844
+ if (table === void 0) throw tableNotFoundError(read.table);
845
+ const resolver = new TableReadResolver(catalog, true);
846
+ const root = resolver.node(table, read.select, 0);
847
+ root.filter = resolver.filter(table, read.filter);
848
+ for (const embedFilter of read.embedFilters) resolver.attachEmbedFilter(root, embedFilter);
849
+ requireUniqueAggregatedKeys(root);
850
+ const order = isAggregated(root) ? resolveAggregatedOrder(table, read.order, groupColumns(root)) : resolveTableOrder(table, read.order);
851
+ const limit = read.limit === void 0 ? maxTableNodeRows : Math.min(read.limit, maxTableNodeRows);
852
+ requireBoundedFanOut(root, limit);
853
+ return {
854
+ root,
855
+ order,
856
+ limit,
857
+ offset: read.offset,
858
+ count: read.count,
859
+ singular: read.singular
860
+ };
861
+ }
862
+ function requireBoundedFanOut(root, rows) {
863
+ let repeatedRows = 0;
864
+ const visit = (node, path, rows, repeated) => {
865
+ for (const embed of node.embeds) {
866
+ if (!embed.projected) continue;
867
+ const embedPath = path === "" ? embed.name : `${path}.${embed.name}`;
868
+ let childRows = rows;
869
+ let childRepeated = repeated;
870
+ if (embed.relationship.cardinality === "many-to-one") childRepeated = true;
871
+ if (embed.relationship.cardinality === "one-to-many") {
872
+ childRows = rows * maxTableNodeRows;
873
+ if (repeated) {
874
+ repeatedRows += childRows;
875
+ if (repeatedRows > 1e6) throw limitError(`the embeds under repeated rows can return more than ${maxTableDocumentRows} rows, at ${embedPath}`);
876
+ }
877
+ }
878
+ visit(embed.node, embedPath, childRows, childRepeated);
879
+ }
880
+ };
881
+ visit(root, "", rows, false);
882
+ }
883
+ var TableReadResolver = class {
884
+ constructor(catalog, aggregates) {
885
+ this.catalog = catalog;
886
+ this.aggregates = aggregates;
887
+ this.embeds = 0;
888
+ this.conditions = 0;
889
+ }
890
+ node(table, items, depth) {
891
+ const node = {
892
+ table: table.name,
893
+ columns: [],
894
+ embeds: [],
895
+ filter: emptyFilter()
896
+ };
897
+ for (const item of items.length === 0 ? [{ star: true }] : items) {
898
+ requireOneSelectForm(item);
899
+ if (item.star) for (const column of table.columns) node.columns.push({
900
+ name: column.name,
901
+ alias: ""
902
+ });
903
+ else if (item.embed) node.embeds.push(this.embed(table, item.embed, depth + 1));
904
+ else if (item.aggregate !== void 0) node.columns.push(this.aggregate(table, item, depth));
905
+ else {
906
+ const column = item.column ?? "";
907
+ if (catalogColumn(table, column) === void 0) throw columnNotFoundError(table.name, column);
908
+ if (item.alias) validateTableAlias(item.alias);
909
+ node.columns.push({
910
+ name: column,
911
+ alias: item.alias ?? ""
912
+ });
913
+ }
914
+ }
915
+ return node;
916
+ }
917
+ aggregate(table, item, depth) {
918
+ if (!this.aggregates) throw tableRequestError("unsupported", "a write cannot return an aggregate");
919
+ if (depth > 0) throw tableRequestError("unsupported", "an embed cannot read an aggregate");
920
+ const column = item.column ?? "";
921
+ if (column !== "" && catalogColumn(table, column) === void 0) throw columnNotFoundError(table.name, column);
922
+ const aggregate = item.aggregate;
923
+ const alias = item.alias || aggregate;
924
+ validateTableAlias(alias);
925
+ return {
926
+ name: column,
927
+ alias,
928
+ aggregate
929
+ };
930
+ }
931
+ embed(parent, embed, depth) {
932
+ if (depth > 3) throw limitError(`embeds nest deeper than ${3} levels`);
933
+ this.embeds++;
934
+ if (this.embeds > 12) throw limitError(`the request embeds more than ${12} relations`);
935
+ if (embed.relation === "") throw tableRequestError("invalid", "an embed must name a relation");
936
+ if (embed.alias !== "") validateTableAlias(embed.alias);
937
+ const relationship = this.catalog.relationship(parent.name, embed.relation, embed.hint);
938
+ const table = this.catalog.table(relationship.foreignTable);
939
+ if (table === void 0) throw tableNotFoundError(relationship.foreignTable);
940
+ const projected = embed.select.length > 0;
941
+ return {
942
+ name: embed.alias !== "" ? embed.alias : embed.relation,
943
+ relationship,
944
+ inner: embed.inner,
945
+ projected,
946
+ node: projected ? this.node(table, embed.select, depth) : {
947
+ table: table.name,
948
+ columns: [],
949
+ embeds: [],
950
+ filter: emptyFilter()
951
+ },
952
+ relation: embed.relation
953
+ };
954
+ }
955
+ attachEmbedFilter(root, embedFilter) {
956
+ if (embedFilter.path.length === 0) throw tableRequestError("invalid", "an embedded filter must name an embed");
957
+ let node = root;
958
+ for (const name of embedFilter.path) {
959
+ const embed = node.embeds.find((candidate) => candidate.name === name) ?? node.embeds.find((candidate) => candidate.relation === name);
960
+ if (embed === void 0) throw embedPathNotFoundError(name);
961
+ node = embed.node;
962
+ }
963
+ const table = this.catalog.table(node.table);
964
+ if (table === void 0) throw tableNotFoundError(node.table);
965
+ this.condition(table, embedFilter.condition);
966
+ node.filter.terms.push({ condition: embedFilter.condition });
967
+ }
968
+ filter(table, filter) {
969
+ this.group(table, filter, 0);
970
+ return filter;
971
+ }
972
+ group(table, group, depth) {
973
+ if (depth > 64) throw limitError(`logic groups nest deeper than ${64} levels`);
974
+ if (group.terms.length === 0 && depth > 0) throw tableRequestError("invalid", "a logic group must hold at least one condition");
975
+ for (const term of group.terms) if (term.condition && !term.group) this.condition(table, term.condition);
976
+ else if (term.group && !term.condition) this.group(table, term.group, depth + 1);
977
+ else throw tableRequestError("invalid", "a filter term must be one condition or one group");
978
+ }
979
+ condition(table, condition) {
980
+ this.conditions++;
981
+ if (this.conditions > 64) throw limitError(`the request has more than ${64} filter conditions`);
982
+ if (catalogColumn(table, condition.column) === void 0) throw columnNotFoundError(table.name, condition.column);
983
+ if (condition.operator === "in" && condition.values.length > 1e3) throw limitError(`an in list holds more than ${maxTableInValues} values`);
984
+ }
985
+ };
986
+ function requireOneSelectForm(item) {
987
+ const named = (item.column ?? "") !== "" || item.aggregate === "count";
988
+ if ([
989
+ item.star === true,
990
+ named,
991
+ item.embed !== void 0
992
+ ].filter(Boolean).length !== 1 || ((item.alias ?? "") !== "" || item.aggregate !== void 0) && !named) throw tableRequestError("invalid", "a select item must be *, one column, one aggregate or one embed");
993
+ }
994
+ function requireUniqueAggregatedKeys(node) {
995
+ if (!isAggregated(node)) return;
996
+ const keys = /* @__PURE__ */ new Set();
997
+ for (const column of node.columns) {
998
+ const key = column.alias || column.name;
999
+ if (keys.has(key)) throw tableRequestError("invalid", `the select reads more than one value into ${JSON.stringify(key)}; give one an alias`);
1000
+ keys.add(key);
1001
+ }
1002
+ }
1003
+ function resolveAggregatedOrder(table, order, groups) {
1004
+ const resolved = resolveTableOrder({
1005
+ ...table,
1006
+ primaryKey: []
1007
+ }, order);
1008
+ if (resolved.some((term) => !groups.includes(term.column))) throw tableRequestError("unsupported", "an aggregated read can order only by a column it groups by");
1009
+ for (const column of groups) if (!resolved.some((term) => term.column === column)) resolved.push({
1010
+ column,
1011
+ descending: false,
1012
+ nulls: ""
1013
+ });
1014
+ return resolved;
1015
+ }
1016
+ function resolveTableOrder(table, order) {
1017
+ if (order.length > 16) throw limitError(`the order has more than ${16} terms`);
1018
+ const resolved = [];
1019
+ for (const term of order) {
1020
+ if (catalogColumn(table, term.column) === void 0) throw columnNotFoundError(table.name, term.column);
1021
+ resolved.push(term);
1022
+ }
1023
+ for (const column of table.primaryKey) if (!resolved.some((term) => term.column === column)) resolved.push({
1024
+ column,
1025
+ descending: false,
1026
+ nulls: ""
1027
+ });
1028
+ return resolved;
1029
+ }
1030
+ function resolveTableWrite(write, catalog) {
1031
+ const table = catalog.table(write.table);
1032
+ if (table === void 0) throw tableNotFoundError(write.table);
1033
+ if (table.kind === "view") throw tableRequestError("unsupported", `data.${table.name} is a view, and views are read only`);
1034
+ const resolved = {
1035
+ table: table.name,
1036
+ kind: write.kind,
1037
+ groups: [],
1038
+ conflictColumns: [],
1039
+ resolution: "",
1040
+ mergeColumns: [],
1041
+ filter: emptyFilter(),
1042
+ representation: void 0,
1043
+ count: write.count,
1044
+ singular: write.singular
1045
+ };
1046
+ let representationRows = maxTableDocumentRows;
1047
+ if (write.kind === "insert") {
1048
+ const payload = resolveTableInsertPayload(write, table);
1049
+ resolved.groups = payload.groups;
1050
+ resolved.mergeColumns = payload.merged;
1051
+ representationRows = payload.objects;
1052
+ [resolved.conflictColumns, resolved.resolution] = resolveTableConflict(write, table);
1053
+ } else if (write.kind === "update") resolved.groups = resolveTableUpdatePayload(write, table);
1054
+ const resolver = new TableReadResolver(catalog, false);
1055
+ if (write.kind !== "insert") {
1056
+ if (write.filter.terms.length === 0) throw filterRequiredError(write.kind);
1057
+ resolved.filter = resolver.filter(table, write.filter);
1058
+ }
1059
+ const representation = resolver.node(table, write.select, 0);
1060
+ for (const embedFilter of write.embedFilters) resolver.attachEmbedFilter(representation, embedFilter);
1061
+ if (write.representation) {
1062
+ requireBoundedFanOut(representation, representationRows);
1063
+ resolved.representation = representation;
1064
+ }
1065
+ return resolved;
1066
+ }
1067
+ function writeObjects(body) {
1068
+ return body.map((object) => {
1069
+ if (object === null) throw invalidBodyError("Empty or invalid json");
1070
+ return {
1071
+ keys: [...object.keys()],
1072
+ raw: stringifyJson(object)
1073
+ };
1074
+ });
1075
+ }
1076
+ function sharedKeys(objects) {
1077
+ const first = new Set(objects[0]?.keys ?? []);
1078
+ for (const object of objects.slice(1)) {
1079
+ const keys = new Set(object.keys);
1080
+ if (keys.size !== first.size || [...keys].some((key) => !first.has(key))) throw invalidBodyError("All object keys must match");
1081
+ }
1082
+ return first;
1083
+ }
1084
+ function resolveTablePayloadColumns(write, table, shared) {
1085
+ const payload = /* @__PURE__ */ new Set();
1086
+ const names = write.columns ?? [...shared].sort();
1087
+ for (const name of names) {
1088
+ if (catalogColumn(table, name) === void 0) throw payloadColumnNotFoundError(table.name, name);
1089
+ payload.add(name);
1090
+ }
1091
+ return payload;
1092
+ }
1093
+ function catalogColumnsIn(table, names) {
1094
+ return table.columns.filter((column) => names(column.name));
1095
+ }
1096
+ function resolveTableInsertPayload(write, table) {
1097
+ const objects = writeObjects(write.body ?? []);
1098
+ const grouped = write.missingDefault && write.columns !== void 0;
1099
+ const payload = resolveTablePayloadColumns(write, table, write.columns === void 0 ? sharedKeys(objects) : /* @__PURE__ */ new Set());
1100
+ const columns = catalogColumnsIn(table, (name) => payload.has(name));
1101
+ const merged = columns.map((column) => column.name);
1102
+ if (!grouped || objects.length === 0) return {
1103
+ groups: [{
1104
+ columns,
1105
+ rows: `[${objects.map((object) => object.raw).join(",")}]`
1106
+ }],
1107
+ merged,
1108
+ objects: objects.length
1109
+ };
1110
+ const listed = write.columns;
1111
+ const sets = /* @__PURE__ */ new Map();
1112
+ for (const object of objects) {
1113
+ const signature = listed.map((name) => object.keys.includes(name) ? "1" : "0").join("");
1114
+ if (!sets.has(signature)) {
1115
+ if (sets.size === 16) throw limitError(`the rows have more than ${16} different key sets`);
1116
+ sets.set(signature, []);
1117
+ }
1118
+ sets.get(signature).push(object);
1119
+ }
1120
+ return {
1121
+ groups: [...sets].map(([signature, members]) => ({
1122
+ columns: catalogColumnsIn(table, (name) => {
1123
+ const position = listed.indexOf(name);
1124
+ return position >= 0 && signature[position] === "1";
1125
+ }),
1126
+ rows: `[${members.map((object) => object.raw).join(",")}]`
1127
+ })),
1128
+ merged,
1129
+ objects: objects.length
1130
+ };
1131
+ }
1132
+ function resolveTableUpdatePayload(write, table) {
1133
+ const [object] = writeObjects([write.body ?? null]);
1134
+ const payload = resolveTablePayloadColumns(write, table, new Set(object.keys));
1135
+ const columns = catalogColumnsIn(table, (name) => payload.has(name));
1136
+ if (columns.length === 0) throw invalidBodyError("the payload sets no column");
1137
+ return [{
1138
+ columns,
1139
+ rows: object.raw
1140
+ }];
1141
+ }
1142
+ function resolveTableConflict(write, table) {
1143
+ for (const name of write.onConflict) if (catalogColumn(table, name) === void 0) throw columnNotFoundError(table.name, name);
1144
+ if (write.resolution === "") return [[], ""];
1145
+ const target = write.onConflict.length > 0 ? write.onConflict : table.primaryKey;
1146
+ if (target.length === 0) return [[], ""];
1147
+ return [[...target], write.resolution];
1148
+ }
1149
+
1150
+ // src/app/testmode/rows/compile.ts
1151
+ const tableSourceName = `"oaiz_source"`;
1152
+ const tableMutationName = `"oaiz_mutation"`;
1153
+ const tablePayloadAlias = `"r"`;
1154
+ function compileRead(read) {
1155
+ const builder = new TableStatementBuilder();
1156
+ const root = builder.node(read.root);
1157
+ const where = whereOf(root);
1158
+ const page = builder.page(root, read);
1159
+ let total = "NULL::bigint";
1160
+ if (read.count === "exact") {
1161
+ const rows = where === "" ? root.from : `${root.from} WHERE ${where}`;
1162
+ if (!isAggregated(read.root)) total = `(SELECT count(*) FROM ${rows})`;
1163
+ else if (root.groups.length === 0) total = "1::bigint";
1164
+ else total = `(SELECT count(*) FROM (SELECT 1 FROM ${rows} GROUP BY ${root.groups.join(", ")}) AS "g")`;
1165
+ }
1166
+ return builder.statement(`WITH ${tableSourceName} AS (${page}) SELECT ${total}, count(*), ${documentBody(read.singular)} FROM ${tableSourceName}`);
1167
+ }
1168
+ function compileWrite(write) {
1169
+ const builder = new TableStatementBuilder();
1170
+ const target = builder.nextAlias();
1171
+ const table = `${quoteQualified("data", write.table)} AS ${target}`;
1172
+ const expressions = [];
1173
+ if (write.kind === "insert") {
1174
+ const returning = ` RETURNING ${target}.*`;
1175
+ if (write.groups.length === 1) expressions.push(`${tableMutationName} AS (${builder.insert(table, write, write.groups[0])}${returning})`);
1176
+ else {
1177
+ const parts = write.groups.map((group, index) => {
1178
+ const name = quoteTableIdentifier(`oaiz_write${index}`);
1179
+ expressions.push(`${name} AS (${builder.insert(table, write, group)}${returning})`);
1180
+ return `SELECT * FROM ${name}`;
1181
+ });
1182
+ expressions.push(`${tableMutationName} AS (${parts.join(" UNION ALL ")})`);
1183
+ }
1184
+ } else {
1185
+ const statement = write.kind === "update" ? builder.update(table, write) : `DELETE FROM ${table}`;
1186
+ const where = builder.filter(target, write.filter);
1187
+ expressions.push(`${tableMutationName} AS (${statement} WHERE ${where} RETURNING ${target}.*)`);
1188
+ }
1189
+ if (write.representation === void 0) {
1190
+ const total = write.count === "exact" ? "count(*)" : "NULL::bigint";
1191
+ return builder.statement(`WITH ${expressions.join(", ")} SELECT ${total}, count(*), NULL::text FROM ${tableMutationName}`);
1192
+ }
1193
+ const root = builder.node(write.representation);
1194
+ root.from = `${tableMutationName} AS ${root.alias}`;
1195
+ expressions.push(`${tableSourceName} AS (${selectSQL(root)})`);
1196
+ const changed = `(SELECT count(*) FROM ${tableMutationName})`;
1197
+ const total = write.count === "exact" ? changed : "NULL::bigint";
1198
+ return builder.statement(`WITH ${expressions.join(", ")} SELECT ${total}, ${changed}, ${documentBody(write.singular)} FROM ${tableSourceName}`);
1199
+ }
1200
+ function documentBody(singular) {
1201
+ if (singular) return `coalesce(json_agg(${tableSourceName}.*)->0, 'null')::text`;
1202
+ return `coalesce(json_agg(${tableSourceName}.*), '[]')::text`;
1203
+ }
1204
+ function payloadRecord(columns) {
1205
+ const definitions = columns.map((column) => `${quoteTableIdentifier(column.name)} ${column.type}`);
1206
+ return `${tablePayloadAlias}(${definitions.join(", ")})`;
1207
+ }
1208
+ function whereOf(node) {
1209
+ return [...node.conditions, ...node.exists].join(" AND ");
1210
+ }
1211
+ function selectSQL(node) {
1212
+ let rows = `SELECT ${node.items.join(", ")} FROM ${node.from}`;
1213
+ for (const join of node.joins) rows += ` ${join}`;
1214
+ const where = whereOf(node);
1215
+ if (where !== "") rows += ` WHERE ${where}`;
1216
+ if (node.groups.length > 0) rows += ` GROUP BY ${node.groups.join(", ")}`;
1217
+ return rows;
1218
+ }
1219
+ const comparisonOperators = {
1220
+ eq: "=",
1221
+ neq: "<>",
1222
+ gt: ">",
1223
+ gte: ">=",
1224
+ lt: "<",
1225
+ lte: "<=",
1226
+ like: "LIKE",
1227
+ ilike: "ILIKE"
1228
+ };
1229
+ const isTests = {
1230
+ null: "IS NULL",
1231
+ true: "IS TRUE",
1232
+ false: "IS FALSE"
1233
+ };
1234
+ const aggregateFunctions = {
1235
+ avg: "avg",
1236
+ count: "count",
1237
+ max: "max",
1238
+ min: "min",
1239
+ sum: "sum"
1240
+ };
1241
+ function columnItem(alias, column) {
1242
+ let item = `${alias}.${quoteTableIdentifier(column.name)}`;
1243
+ if (column.aggregate !== void 0) {
1244
+ const argument = column.name === "" ? "*" : item;
1245
+ item = `${aggregateFunctions[column.aggregate]}(${argument})`;
1246
+ }
1247
+ return column.alias !== "" ? `${item} AS ${quoteTableIdentifier(column.alias)}` : item;
1248
+ }
1249
+ var TableStatementBuilder = class {
1250
+ constructor() {
1251
+ this.args = [];
1252
+ this.aliases = 0;
1253
+ }
1254
+ statement(sql) {
1255
+ return {
1256
+ sql,
1257
+ args: this.args
1258
+ };
1259
+ }
1260
+ nextAlias() {
1261
+ return quoteTableIdentifier(`t${this.aliases++}`);
1262
+ }
1263
+ param(value) {
1264
+ this.args.push(String(value));
1265
+ return `$${this.args.length}`;
1266
+ }
1267
+ page(root, read) {
1268
+ let page = selectSQL(root);
1269
+ if (read.order.length > 0) page += ` ORDER BY ${read.order.map((term) => orderTerm(root.alias, term)).join(", ")}`;
1270
+ page += ` LIMIT ${this.param(read.limit)}`;
1271
+ page += ` OFFSET ${this.param(read.offset)}`;
1272
+ return page;
1273
+ }
1274
+ node(node) {
1275
+ const index = this.aliases;
1276
+ const alias = this.nextAlias();
1277
+ const compiled = {
1278
+ index,
1279
+ alias,
1280
+ from: `${quoteQualified("data", node.table)} AS ${alias}`,
1281
+ items: node.columns.map((column) => columnItem(alias, column)),
1282
+ joins: [],
1283
+ conditions: [],
1284
+ exists: [],
1285
+ groups: isAggregated(node) ? groupColumns(node).map((column) => `${alias}.${quoteTableIdentifier(column)}`) : []
1286
+ };
1287
+ const filter = this.filter(alias, node.filter);
1288
+ if (filter !== "") compiled.conditions.push(filter);
1289
+ for (const embed of node.embeds) this.embed(compiled, embed);
1290
+ return compiled;
1291
+ }
1292
+ embed(parent, embed) {
1293
+ if (!embed.inner && !embed.projected) return;
1294
+ const child = this.node(embed.node);
1295
+ const relationship = embed.relationship;
1296
+ child.conditions = [...relationship.columns.map((column, index) => `${child.alias}.${quoteTableIdentifier(relationship.foreignColumns[index])} = ${parent.alias}.${quoteTableIdentifier(column)}`), ...child.conditions];
1297
+ const where = whereOf(child);
1298
+ if (embed.inner) parent.exists.push(`EXISTS (SELECT 1 FROM ${child.from} WHERE ${where})`);
1299
+ if (!embed.projected) return;
1300
+ const row = quoteTableIdentifier(`s${child.index}`);
1301
+ const joined = quoteTableIdentifier(`e${child.index}`);
1302
+ let rows = `SELECT ${child.items.join(", ")} FROM ${child.from}`;
1303
+ for (const join of child.joins) rows += ` ${join}`;
1304
+ rows += ` WHERE ${where}`;
1305
+ const name = quoteTableIdentifier(embed.name);
1306
+ if (toOne(relationship)) {
1307
+ parent.joins.push(`LEFT JOIN LATERAL (SELECT to_json(${row}.*) AS "j" FROM (${rows}) AS ${row}) AS ${joined} ON TRUE`);
1308
+ parent.items.push(`${joined}."j" AS ${name}`);
1309
+ return;
1310
+ }
1311
+ rows += ` LIMIT ${maxTableNodeRows}`;
1312
+ parent.joins.push(`LEFT JOIN LATERAL (SELECT json_agg(${row}.*) AS "j" FROM (${rows}) AS ${row}) AS ${joined} ON TRUE`);
1313
+ parent.items.push(`coalesce(${joined}."j", '[]') AS ${name}`);
1314
+ }
1315
+ filter(alias, group) {
1316
+ if (group.terms.length === 0) return "";
1317
+ const parts = group.terms.map((term) => term.group ? this.filter(alias, term.group) : this.condition(alias, term.condition));
1318
+ const body = parts.join(group.or ? " OR " : " AND ");
1319
+ if (group.negated) return `NOT (${body})`;
1320
+ return parts.length > 1 ? `(${body})` : body;
1321
+ }
1322
+ condition(alias, condition) {
1323
+ const column = `${alias}.${quoteTableIdentifier(condition.column)}`;
1324
+ let expression;
1325
+ if (condition.operator === "is") expression = `${column} ${isTests[condition.is]}`;
1326
+ else if (condition.operator === "in") expression = `${column} = ANY(${this.param(tableArrayLiteral(condition.values))})`;
1327
+ else expression = `${column} ${comparisonOperators[condition.operator]} ${this.param(condition.value)}`;
1328
+ return condition.negated ? `NOT (${expression})` : expression;
1329
+ }
1330
+ insert(table, write, group) {
1331
+ let statement = `INSERT INTO ${table}`;
1332
+ if (group.columns.length === 0) statement += ` SELECT FROM json_array_elements(${this.param(group.rows)}::json)`;
1333
+ else {
1334
+ const names = group.columns.map((column) => quoteTableIdentifier(column.name));
1335
+ const values = group.columns.map((column) => `${tablePayloadAlias}.${quoteTableIdentifier(column.name)}`);
1336
+ statement += ` (${names.join(", ")}) SELECT ${values.join(", ")} FROM json_to_recordset(${this.param(group.rows)}::json) AS ${payloadRecord(group.columns)}`;
1337
+ }
1338
+ if (write.resolution !== "") {
1339
+ statement += ` ON CONFLICT (${write.conflictColumns.map(quoteTableIdentifier).join(", ")})`;
1340
+ const assignments = write.resolution === "merge" ? write.mergeColumns.map((column) => `${quoteTableIdentifier(column)} = EXCLUDED.${quoteTableIdentifier(column)}`) : [];
1341
+ statement += assignments.length > 0 ? ` DO UPDATE SET ${assignments.join(", ")}` : " DO NOTHING";
1342
+ }
1343
+ return statement;
1344
+ }
1345
+ update(table, write) {
1346
+ const group = write.groups[0];
1347
+ const assignments = group.columns.map((column) => {
1348
+ const name = quoteTableIdentifier(column.name);
1349
+ return `${name} = ${tablePayloadAlias}.${name}`;
1350
+ });
1351
+ const from = ` FROM json_to_record(${this.param(group.rows)}::json) AS ${payloadRecord(group.columns)}`;
1352
+ return `UPDATE ${table} SET ${assignments.join(", ")}${from}`;
1353
+ }
1354
+ };
1355
+ function orderTerm(alias, term) {
1356
+ let rendered = `${alias}.${quoteTableIdentifier(term.column)} ${term.descending ? "DESC" : "ASC"}`;
1357
+ if (term.nulls === "first") rendered += " NULLS FIRST";
1358
+ if (term.nulls === "last") rendered += " NULLS LAST";
1359
+ return rendered;
1360
+ }
1361
+ function tableArrayLiteral(values) {
1362
+ return `{${values.map((value) => `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`).join(",")}}`;
1363
+ }
1364
+ function quoteTableIdentifier(name) {
1365
+ return `"${name.replaceAll("\0", "").replaceAll("\"", "\"\"")}"`;
1366
+ }
1367
+ function quoteQualified(schema, name) {
1368
+ return `${quoteTableIdentifier(schema)}.${quoteTableIdentifier(name)}`;
1369
+ }
1370
+
1371
+ // src/app/testmode/rows/execute.ts
1372
+ const testOrganisation = "7e570000-0000-4000-8000-00000000000a";
1373
+ const testPhysicalDatabase = "oaiz_test";
1374
+ const customerStatementClasses = /* @__PURE__ */ new Set([
1375
+ "0A",
1376
+ "20",
1377
+ "21",
1378
+ "22",
1379
+ "23",
1380
+ "25",
1381
+ "2D",
1382
+ "2F",
1383
+ "42",
1384
+ "44",
1385
+ "54",
1386
+ "P0"
1387
+ ]);
1388
+ const singularGuardSQL = `SELECT 1 / CASE
1389
+ WHEN pg_catalog.current_setting('oaiz.table_singular_guard', true) = 'ok' THEN 1
1390
+ ELSE 0 END`;
1391
+ function runRowsRequest(database, caller, request) {
1392
+ const readOnly = request.read !== void 0;
1393
+ return database.exclusive(async (session) => {
1394
+ await step(session, `begin isolation level read committed ${readOnly ? "read only" : "read write"}`, [], "failed to prepare the bounded transaction");
1395
+ try {
1396
+ await prelude(session, caller);
1397
+ const scope = request.read ? request.read : request.write;
1398
+ const [[document]] = await step(session, tableCatalogQuery, tableCatalogArguments(scope.table, catalogDepth(scope.select)), "query failed");
1399
+ const catalog = decodeTableCatalog(document);
1400
+ const result = request.read ? await readDocument(session, compileRead(resolveTableRead(request.read, catalog)), request.read.singular) : await writeDocument(session, compileWrite(resolveTableWrite(request.write, catalog)), request.write.singular);
1401
+ await step(session, "COMMIT", [], readOnly ? "failed to commit the bounded transaction" : "failed to complete the bounded transaction");
1402
+ return result;
1403
+ } catch (error) {
1404
+ await session.query("ROLLBACK", []).catch(() => void 0);
1405
+ throw error;
1406
+ }
1407
+ });
1408
+ }
1409
+ async function prelude(session, caller) {
1410
+ await step(session, "SELECT set_config('client_encoding', 'UTF8', true), set_config('standard_conforming_strings', 'on', true), set_config('hnsw.iterative_scan', 'strict_order', true)", [], "failed to pin the transaction's parser settings");
1411
+ const claims = [
1412
+ ["oaiz.organisation_id", testOrganisation],
1413
+ ["oaiz.storage_database", testPhysicalDatabase],
1414
+ ["oaiz.subject", caller.subject],
1415
+ ["oaiz.organisation_role", caller.organisationRole],
1416
+ ["oaiz.teams", caller.teams.join(",")],
1417
+ ["oaiz.capability", caller.capability],
1418
+ ["oaiz.app", caller.app]
1419
+ ];
1420
+ await step(session, `SELECT ${claims.map((_, index) => `set_config($${2 * index + 1}, $${2 * index + 2}, true)`).join(", ")}`, claims.flat(), "failed to publish the identity claims");
1421
+ await step(session, `SET LOCAL ROLE "${caller.capability === "view" ? "oaiz_test_read" : "oaiz_test_write"}"`, [], "failed to assume the managed data role");
1422
+ await step(session, "SELECT set_config('statement_timeout', $1, true)", ["30s"], "statement failed");
1423
+ await step(session, "SET LOCAL search_path TO pg_catalog, extensions, pg_temp", [], "statement failed");
1424
+ }
1425
+ async function readDocument(session, statement, singular) {
1426
+ let sql = statement.sql;
1427
+ if (singular) sql = `SELECT document.total, document.returned, document.body, pg_catalog.set_config('oaiz.table_singular_guard', CASE WHEN document.returned = 1 THEN 'ok' ELSE 'mismatch' END, true) FROM (\n${sql}\n) AS document(total, returned, body)`;
1428
+ const result = await documentStatement(session, sql, statement.args);
1429
+ if (singular) try {
1430
+ await session.query(singularGuardSQL, []);
1431
+ } catch (error) {
1432
+ if (sqlStateOf(error) === "22012" && result.returned !== 1) throw singularMismatchError(result.returned);
1433
+ throw documentFailure(error);
1434
+ }
1435
+ return result;
1436
+ }
1437
+ async function writeDocument(session, statement, singular) {
1438
+ const result = await documentStatement(session, statement.sql, statement.args);
1439
+ if (singular && result.returned !== 1) throw singularMismatchError(result.returned);
1440
+ return result;
1441
+ }
1442
+ async function documentStatement(session, sql, args) {
1443
+ let rows;
1444
+ try {
1445
+ rows = await session.query(sql, args);
1446
+ } catch (error) {
1447
+ throw documentFailure(error);
1448
+ }
1449
+ const [total, returned, body] = rows[0];
1450
+ return {
1451
+ total,
1452
+ returned: Number(returned),
1453
+ body
1454
+ };
1455
+ }
1456
+ function documentFailure(error) {
1457
+ const sqlState = sqlStateOf(error);
1458
+ if (sqlState === void 0) return error;
1459
+ if (sqlState === "57014") return statementTimeoutError();
1460
+ const failure = error;
1461
+ if (customerStatementClasses.has(sqlState.slice(0, 2))) return statementFailure(sqlState, failure.message, failure.hint);
1462
+ return boundedTxFailure("query failed", sqlState);
1463
+ }
1464
+ async function step(session, sql, args, message) {
1465
+ try {
1466
+ return await session.query(sql, args);
1467
+ } catch (error) {
1468
+ const sqlState = sqlStateOf(error);
1469
+ if (sqlState === void 0) throw error;
1470
+ throw boundedTxFailure(message, sqlState);
1471
+ }
1472
+ }
1473
+ function sqlStateOf(error) {
1474
+ if (!(error instanceof Error) || !("severity" in error)) return void 0;
1475
+ const code = error.code;
1476
+ return typeof code === "string" && code.length === 5 ? code : void 0;
1477
+ }
1478
+
1479
+ // src/app/testmode/rows/functions.ts
1480
+ const maxFunctionCallRows = 1e3;
1481
+ const maxFunctionCallArgs = 100;
1482
+ const maxObjectNameBytes = 63;
1483
+ const functionNamePattern = /^[A-Za-z][A-Za-z0-9_]*$/;
1484
+ const reservedPrefixes = ["oaiz_", "pg_"];
1485
+ const functionCatalogQuery = `SELECT coalesce(pg_catalog.json_agg(pg_catalog.json_build_object(
1486
+ 'name', p.proname,
1487
+ 'volatility', p.provolatile,
1488
+ 'returnsSet', p.proretset,
1489
+ 'returns', CASE
1490
+ WHEN p.prorettype IN ('pg_catalog.trigger'::pg_catalog.regtype, 'pg_catalog.event_trigger'::pg_catalog.regtype) THEN 'trigger'
1491
+ WHEN p.prorettype = 'pg_catalog.void'::pg_catalog.regtype THEN 'void'
1492
+ WHEN p.proargmodes && ARRAY['o', 'b', 't']::pg_catalog."char"[] THEN 'row'
1493
+ WHEN base.typtype = 'c' THEN 'row'
1494
+ WHEN base.typtype = 'p' THEN 'unsupported'
1495
+ ELSE 'scalar' END,
1496
+ 'inputs', coalesce((
1497
+ SELECT pg_catalog.json_agg(pg_catalog.json_build_object(
1498
+ 'name', coalesce(i.name, ''),
1499
+ 'mode', i.mode,
1500
+ 'type', pg_catalog.format_type(i.type, NULL),
1501
+ 'pseudo', t.typtype = 'p',
1502
+ 'hasDefault', i.position > p.pronargs - p.pronargdefaults) ORDER BY i.position)
1503
+ FROM (
1504
+ SELECT a.type, p.proargnames[a.ordinality] AS name, coalesce(p.proargmodes[a.ordinality], 'i') AS mode,
1505
+ pg_catalog.row_number() OVER (ORDER BY a.ordinality) AS position
1506
+ FROM pg_catalog.unnest(coalesce(p.proallargtypes, p.proargtypes::pg_catalog.oid[])) WITH ORDINALITY AS a(type, ordinality)
1507
+ WHERE coalesce(p.proargmodes[a.ordinality], 'i') IN ('i', 'b', 'v')
1508
+ ) AS i
1509
+ JOIN pg_catalog.pg_type t ON t.oid = i.type), '[]'::pg_catalog.json)) ORDER BY p.oid), '[]'::pg_catalog.json)::pg_catalog.text
1510
+ FROM pg_catalog.pg_proc p
1511
+ CROSS JOIN LATERAL (
1512
+ WITH RECURSIVE chain(typtype, typbasetype, depth) AS (
1513
+ SELECT t.typtype, t.typbasetype, 0 FROM pg_catalog.pg_type t WHERE t.oid = p.prorettype
1514
+ UNION ALL
1515
+ SELECT t.typtype, t.typbasetype, chain.depth + 1
1516
+ FROM chain JOIN pg_catalog.pg_type t ON t.oid = chain.typbasetype
1517
+ WHERE chain.typtype = 'd'
1518
+ )
1519
+ SELECT chain.typtype FROM chain ORDER BY chain.depth DESC LIMIT 1
1520
+ ) AS base
1521
+ WHERE p.pronamespace = 'data'::pg_catalog.regnamespace AND p.proname = $1 AND p.prokind = 'f'`;
1522
+ async function runFunctionCall(database, caller, call) {
1523
+ const args = call.args ?? /* @__PURE__ */ new Map();
1524
+ return database.exclusive(async (session) => {
1525
+ await step(session, "begin isolation level read committed read write", [], "failed to prepare the bounded transaction");
1526
+ try {
1527
+ await prelude(session, caller);
1528
+ const [[document]] = await step(session, functionCatalogQuery, [call.name], "query failed");
1529
+ const resolved = resolveFunctionCall(call.name, args, JSON.parse(document), caller);
1530
+ if (resolved.readOnly) await step(session, "SET TRANSACTION READ ONLY", [], "statement failed");
1531
+ const statement = compileFunctionCall(resolved);
1532
+ const result = await documentStatement(session, statement.sql, statement.args);
1533
+ if (result.returned > maxFunctionCallRows) throw functionResultTooLargeError(maxFunctionCallRows);
1534
+ await step(session, "COMMIT", [], resolved.readOnly ? "failed to commit the bounded transaction" : "failed to complete the bounded transaction");
1535
+ return result.body;
1536
+ } catch (error) {
1537
+ await session.query("ROLLBACK", []).catch(() => void 0);
1538
+ throw error;
1539
+ }
1540
+ });
1541
+ }
1542
+ function validateFunctionCall(call) {
1543
+ if (new TextEncoder().encode(call.name).length > maxObjectNameBytes) throw functionCallInvalidError("the function name is longer than a PostgreSQL identifier");
1544
+ const folded = call.name.toLowerCase();
1545
+ if (!functionNamePattern.test(call.name) || reservedPrefixes.some((prefix) => folded.startsWith(prefix))) throw functionNotFoundError(call.name);
1546
+ if ((call.args?.size ?? 0) > maxFunctionCallArgs) throw functionCallInvalidError(`a function call passes at most ${maxFunctionCallArgs} arguments`);
1547
+ }
1548
+ function resolveFunctionCall(name, args, entries, caller) {
1549
+ if (entries.length === 0) throw functionNotFoundError(name);
1550
+ if (entries.length > 1) throw functionCallInvalidError(`the database holds more than one function named ${name}`);
1551
+ const [entry] = entries;
1552
+ if (entry.returns === "trigger") throw functionNotFoundError(name);
1553
+ if (entry.returns === "unsupported") throw functionCallInvalidError(`the function ${name} returns a type a call cannot read`);
1554
+ const resolved = {
1555
+ name: entry.name,
1556
+ args: [],
1557
+ argValues: "",
1558
+ returnsSet: entry.returnsSet,
1559
+ returns: entry.returns,
1560
+ readOnly: entry.volatility !== "v"
1561
+ };
1562
+ const taken = /* @__PURE__ */ new Set();
1563
+ for (const input of entry.inputs) {
1564
+ if (input.name === "") throw functionCallInvalidError(`the function ${name} has an input without a name`);
1565
+ if (input.mode === "v") throw functionCallInvalidError(`the function ${name} takes VARIADIC input ${input.name}`);
1566
+ if (input.pseudo) throw functionCallInvalidError(`the function ${name} takes input ${input.name} of pseudo-type ${input.type}`);
1567
+ taken.add(input.name);
1568
+ if (args.has(input.name)) resolved.args.push({
1569
+ name: input.name,
1570
+ type: input.type
1571
+ });
1572
+ else if (!input.hasDefault) throw functionCallInvalidError(`the function ${name} requires argument ${input.name}`);
1573
+ }
1574
+ const unknown = [...args.keys()].filter((arg) => !taken.has(arg)).sort(compareCodePoints);
1575
+ if (unknown.length !== 0) throw functionCallInvalidError(`the function ${name} has no argument ${unknown[0]}`);
1576
+ if (!resolved.readOnly && caller.capability !== "edit") throw databaseEditRequiredError();
1577
+ if (resolved.args.length !== 0) resolved.argValues = stringifyJson(new Map(resolved.args.map((arg) => [arg.name, args.get(arg.name)])));
1578
+ return resolved;
1579
+ }
1580
+ function compileFunctionCall(call) {
1581
+ const fn = `${quoteIdentifier("data")}.${quoteIdentifier(call.name)}`;
1582
+ let source;
1583
+ let args = [];
1584
+ if (call.args.length !== 0) {
1585
+ const columns = call.args.map((arg) => `${quoteIdentifier(arg.name)} ${arg.type}`);
1586
+ const named = call.args.map((arg) => `${quoteIdentifier(arg.name)} => r.${quoteIdentifier(arg.name)}`);
1587
+ source = `pg_catalog.json_to_record($1::pg_catalog.json) AS r(${columns.join(", ")}), LATERAL ${fn}(${named.join(", ")})`;
1588
+ args = [call.argValues];
1589
+ } else source = `${fn}()`;
1590
+ let item;
1591
+ let inner;
1592
+ switch (call.returns) {
1593
+ case "row":
1594
+ [item, inner] = ["pg_catalog.to_json(o.*)", `SELECT f.* FROM ${source} AS f`];
1595
+ break;
1596
+ case "void":
1597
+ [item, inner] = ["NULL::pg_catalog.json", `SELECT FROM ${source} AS f`];
1598
+ break;
1599
+ default: [item, inner] = ["pg_catalog.to_json(o.v)", `SELECT f.v FROM ${source} AS f(v)`];
1600
+ }
1601
+ return {
1602
+ sql: `SELECT NULL::pg_catalog.int8, pg_catalog.count(*), ${call.returnsSet ? "coalesce(pg_catalog.json_agg(result.v), '[]'::pg_catalog.json)" : "(pg_catalog.json_agg(result.v) -> 0)"}::pg_catalog.text FROM (\nSELECT ${item} AS v FROM (\n${inner}\nLIMIT 1001\n) AS o\n) AS result`,
1603
+ args
1604
+ };
1605
+ }
1606
+ function quoteIdentifier(name) {
1607
+ return `"${name.replaceAll("\0", "").replaceAll("\"", "\"\"")}"`;
1608
+ }
1609
+
1610
+ // src/app/testmode/rows/request.ts
1611
+ function decodeRowsRequest(operation, body) {
1612
+ const value = readBody(body);
1613
+ switch (operation) {
1614
+ case "select": return { read: selectRowsRead(decodeSelect(value)) };
1615
+ case "insert": {
1616
+ const input = decodeInsert(value);
1617
+ return { write: insertRowsWrite(input.table, input.rows, input.defaultToNull, input.select, input.count, input.single) };
1618
+ }
1619
+ case "upsert": return { write: upsertRowsWrite(decodeUpsert(value)) };
1620
+ case "update": return { write: updateRowsWrite(decodeUpdate(value)) };
1621
+ case "delete": return { write: deleteRowsWrite(decodeDelete(value)) };
1622
+ }
1623
+ }
1624
+ function readBody(body) {
1625
+ try {
1626
+ return parseFirstJson(body);
1627
+ } catch (error) {
1628
+ if (!(error instanceof JsonSyntaxError)) throw error;
1629
+ throw new RowsRefusal(400, "invalid-input", error.message);
1630
+ }
1631
+ }
1632
+ function invalidJson(field, message) {
1633
+ return new RowsRefusal(400, "invalid-input", "Invalid JSON input", [{
1634
+ field,
1635
+ message
1636
+ }]);
1637
+ }
1638
+ function kindOf(value) {
1639
+ if (typeof value === "boolean") return "bool";
1640
+ if (typeof value === "string") return "string";
1641
+ if (value instanceof JsonNumber) return "number";
1642
+ if (Array.isArray(value)) return "array";
1643
+ return "object";
1644
+ }
1645
+ function typeError(goType, value, path, found = kindOf(value)) {
1646
+ if (path === "") return invalidJson("json", `Invalid JSON format: json: cannot unmarshal ${found} into Go value of type ${goType}`);
1647
+ const field = path.toLowerCase();
1648
+ return invalidJson(field, `Expected ${goType} for field ${field}, but got ${found}`);
1649
+ }
1650
+ function decodeStruct(value, path, goType, fields, target) {
1651
+ if (value === null) return;
1652
+ if (!(value instanceof Map)) throw typeError(goType, value, path);
1653
+ const names = Object.keys(fields);
1654
+ for (const [key, member] of value) {
1655
+ const name = names.includes(key) ? key : names.find((field) => field.toLowerCase() === key.toLowerCase());
1656
+ if (name === void 0) throw invalidJson("json", `Invalid JSON format: json: unknown field ${JSON.stringify(key)}`);
1657
+ const spec = fields[name];
1658
+ const memberPath = path === "" ? name : `${path}.${name}`;
1659
+ if (member === null) {
1660
+ if (spec.nilable) target[name] = void 0;
1661
+ continue;
1662
+ }
1663
+ target[name] = spec.decode(member, memberPath);
1664
+ }
1665
+ }
1666
+ const field = (decode, nilable = false) => ({
1667
+ decode,
1668
+ nilable
1669
+ });
1670
+ function decodeString(goType = "string") {
1671
+ return (value, path) => {
1672
+ if (typeof value !== "string") throw typeError(goType, value, path);
1673
+ return value;
1674
+ };
1675
+ }
1676
+ const decodeBool = (value, path) => {
1677
+ if (typeof value !== "boolean") throw typeError("bool", value, path);
1678
+ return value;
1679
+ };
1680
+ const int64Min = BigInt("-9223372036854775808");
1681
+ const int64Max = BigInt("9223372036854775807");
1682
+ const decodeInt = (value, path) => {
1683
+ if (!(value instanceof JsonNumber)) throw typeError("int", value, path);
1684
+ if (!/^-?\d+$/.test(value.text)) throw typeError("int", value, path, `number ${value.text}`);
1685
+ const parsed = BigInt(value.text);
1686
+ if (parsed < int64Min || parsed > int64Max) throw typeError("int", value, path, `number ${value.text}`);
1687
+ return parsed;
1688
+ };
1689
+ function decodeSlice(goType, element, zero) {
1690
+ return (value, path) => {
1691
+ if (!Array.isArray(value)) throw typeError(goType, value, path);
1692
+ return value.map((item) => item === null ? zero : element(item, path));
1693
+ };
1694
+ }
1695
+ const decodeObject = (value, path) => {
1696
+ if (!(value instanceof Map)) throw typeError("map[string]interface {}", value, path);
1697
+ return value;
1698
+ };
1699
+ const decodeFilter = (value, path) => {
1700
+ const filter = {
1701
+ column: "",
1702
+ operator: "",
1703
+ not: false
1704
+ };
1705
+ decodeStruct(value, path, "dto.StorageRowFilter", filterFields, filter);
1706
+ return filter;
1707
+ };
1708
+ const decodeFilters = decodeSlice("[]dto.StorageRowFilter", decodeFilter, void 0);
1709
+ const filterFields = {
1710
+ column: field(decodeString()),
1711
+ operator: field(decodeString("dto.StorageRowFilterOperator")),
1712
+ value: field(decodeString(), true),
1713
+ values: field(decodeSlice("[]string", decodeString(), ""), true),
1714
+ not: field(decodeBool),
1715
+ or: field((value, path) => decodeFilters(value, path), true),
1716
+ and: field((value, path) => decodeFilters(value, path), true)
1717
+ };
1718
+ const decodeOrder = (value, path) => {
1719
+ const order = { column: "" };
1720
+ decodeStruct(value, path, "dto.StorageRowOrder", {
1721
+ column: field(decodeString()),
1722
+ ascending: field(decodeBool, true),
1723
+ nullsFirst: field(decodeBool, true)
1724
+ }, order);
1725
+ return order;
1726
+ };
1727
+ function filterList(filters) {
1728
+ return filters?.map((filter) => filter ?? {
1729
+ column: "",
1730
+ operator: "",
1731
+ not: false
1732
+ });
1733
+ }
1734
+ const countField = field(decodeString("dto.StorageRowCount"));
1735
+ const bodyType = "dto.wire";
1736
+ function decodeSelect(value) {
1737
+ const input = {
1738
+ table: "",
1739
+ select: "",
1740
+ offset: BigInt(0),
1741
+ count: "",
1742
+ single: false
1743
+ };
1744
+ decodeStruct(value, "", bodyType, {
1745
+ table: field(decodeString()),
1746
+ select: field(decodeString()),
1747
+ filters: field(decodeFilters, true),
1748
+ order: field(decodeSlice("[]dto.StorageRowOrder", decodeOrder, { column: "" }), true),
1749
+ limit: field(decodeInt, true),
1750
+ offset: field(decodeInt),
1751
+ count: countField,
1752
+ single: field(decodeBool)
1753
+ }, input);
1754
+ input.filters = filterList(input.filters);
1755
+ requireFields("selectstoragedatabaserowsinput", [["table", input.table !== ""]]);
1756
+ return input;
1757
+ }
1758
+ const writeFields = {
1759
+ table: field(decodeString()),
1760
+ select: field(decodeString(), true),
1761
+ count: countField,
1762
+ single: field(decodeBool)
1763
+ };
1764
+ const rowsField = field(decodeSlice("[]map[string]interface {}", decodeObject, null), true);
1765
+ function decodeInsert(value) {
1766
+ const input = {
1767
+ table: "",
1768
+ count: "",
1769
+ single: false
1770
+ };
1771
+ decodeStruct(value, "", bodyType, {
1772
+ ...writeFields,
1773
+ rows: rowsField,
1774
+ defaultToNull: field(decodeBool, true)
1775
+ }, input);
1776
+ requireFields("insertstoragedatabaserowsinput", [["table", input.table !== ""], ["rows", input.rows !== void 0]]);
1777
+ return input;
1778
+ }
1779
+ function decodeUpsert(value) {
1780
+ const input = {
1781
+ table: "",
1782
+ count: "",
1783
+ single: false,
1784
+ ignoreDuplicates: false
1785
+ };
1786
+ decodeStruct(value, "", bodyType, {
1787
+ ...writeFields,
1788
+ rows: rowsField,
1789
+ onConflict: field(decodeSlice("[]string", decodeString(), ""), true),
1790
+ ignoreDuplicates: field(decodeBool),
1791
+ defaultToNull: field(decodeBool, true)
1792
+ }, input);
1793
+ requireFields("upsertstoragedatabaserowsinput", [["table", input.table !== ""], ["rows", input.rows !== void 0]]);
1794
+ return input;
1795
+ }
1796
+ function decodeUpdate(value) {
1797
+ const input = {
1798
+ table: "",
1799
+ count: "",
1800
+ single: false
1801
+ };
1802
+ decodeStruct(value, "", bodyType, {
1803
+ ...writeFields,
1804
+ values: field(decodeObject, true),
1805
+ filters: field(decodeFilters, true)
1806
+ }, input);
1807
+ input.filters = filterList(input.filters);
1808
+ requireFields("updatestoragedatabaserowsinput", [
1809
+ ["table", input.table !== ""],
1810
+ ["values", input.values !== void 0],
1811
+ ["filters", input.filters !== void 0]
1812
+ ]);
1813
+ return input;
1814
+ }
1815
+ function decodeDelete(value) {
1816
+ const input = {
1817
+ table: "",
1818
+ count: "",
1819
+ single: false
1820
+ };
1821
+ decodeStruct(value, "", bodyType, {
1822
+ ...writeFields,
1823
+ filters: field(decodeFilters, true)
1824
+ }, input);
1825
+ input.filters = filterList(input.filters);
1826
+ requireFields("deletestoragedatabaserowsinput", [["table", input.table !== ""], ["filters", input.filters !== void 0]]);
1827
+ return input;
1828
+ }
1829
+ function decodeFunctionCall(body) {
1830
+ const input = { name: "" };
1831
+ decodeStruct(readBody(body), "", bodyType, {
1832
+ name: field(decodeString()),
1833
+ args: field(decodeObject, true)
1834
+ }, input);
1835
+ requireFields("callstoragedatabasefunctioninput", [["name", input.name !== ""]]);
1836
+ validateFunctionCall(input);
1837
+ return input;
1838
+ }
1839
+ function requireFields(type, checks) {
1840
+ const errors = checks.filter(([, present]) => !present).map(([name]) => ({
1841
+ field: `${type}.${name}`,
1842
+ message: `${type}.${name} is required`
1843
+ }));
1844
+ if (errors.length > 0) throw new RowsRefusal(400, "validation-error", "Validation failed", errors);
1845
+ }
1846
+ const rowOperators = [
1847
+ "eq",
1848
+ "neq",
1849
+ "gt",
1850
+ "gte",
1851
+ "lt",
1852
+ "lte",
1853
+ "like",
1854
+ "ilike",
1855
+ "in",
1856
+ "is"
1857
+ ];
1858
+ const rowIsValues = [
1859
+ "null",
1860
+ "true",
1861
+ "false"
1862
+ ];
1863
+ function invalidRowsRequest(message) {
1864
+ return incorrectInput(message, slugTableRequestInvalid);
1865
+ }
1866
+ function selectRowsRead(input) {
1867
+ const selection = parseRowSelect(input.select);
1868
+ const [filter, embedFilters] = rowFilters(input.filters ?? []);
1869
+ return {
1870
+ table: input.table,
1871
+ select: selection,
1872
+ filter,
1873
+ embedFilters,
1874
+ order: rowOrder(input.order ?? []),
1875
+ limit: input.limit === void 0 ? void 0 : Number(input.limit),
1876
+ offset: input.offset,
1877
+ count: rowCount(input.count),
1878
+ singular: input.single
1879
+ };
1880
+ }
1881
+ function insertRowsWrite(table, rows, defaultToNull, returning, count, single) {
1882
+ let columns;
1883
+ for (const row of rows) for (const name of row?.keys() ?? []) {
1884
+ columns ??= [];
1885
+ if (!columns.includes(name)) columns.push(name);
1886
+ }
1887
+ columns?.sort(compareCodePoints);
1888
+ return withRowsReturning({
1889
+ table,
1890
+ kind: "insert",
1891
+ body: rows,
1892
+ columns,
1893
+ onConflict: [],
1894
+ resolution: "",
1895
+ missingDefault: defaultToNull === false,
1896
+ filter: emptyFilter(),
1897
+ embedFilters: [],
1898
+ representation: false,
1899
+ select: [],
1900
+ count: "",
1901
+ singular: single
1902
+ }, returning, count);
1903
+ }
1904
+ function upsertRowsWrite(input) {
1905
+ const write = insertRowsWrite(input.table, input.rows, input.defaultToNull, input.select, input.count, input.single);
1906
+ write.onConflict = input.onConflict ?? [];
1907
+ write.resolution = input.ignoreDuplicates ? "ignore" : "merge";
1908
+ return write;
1909
+ }
1910
+ function updateRowsWrite(input) {
1911
+ const [filter, embedFilters] = rowFilters(input.filters);
1912
+ return withRowsReturning({
1913
+ table: input.table,
1914
+ kind: "update",
1915
+ body: input.values,
1916
+ columns: void 0,
1917
+ onConflict: [],
1918
+ resolution: "",
1919
+ missingDefault: false,
1920
+ filter,
1921
+ embedFilters,
1922
+ representation: false,
1923
+ select: [],
1924
+ count: "",
1925
+ singular: input.single
1926
+ }, input.select, input.count);
1927
+ }
1928
+ function deleteRowsWrite(input) {
1929
+ const [filter, embedFilters] = rowFilters(input.filters);
1930
+ return withRowsReturning({
1931
+ table: input.table,
1932
+ kind: "delete",
1933
+ body: void 0,
1934
+ columns: void 0,
1935
+ onConflict: [],
1936
+ resolution: "",
1937
+ missingDefault: false,
1938
+ filter,
1939
+ embedFilters,
1940
+ representation: false,
1941
+ select: [],
1942
+ count: "",
1943
+ singular: input.single
1944
+ }, input.select, input.count);
1945
+ }
1946
+ function withRowsReturning(write, returning, count) {
1947
+ write.count = rowCount(count);
1948
+ if (returning === void 0) return write;
1949
+ write.representation = true;
1950
+ write.select = parseRowSelect(returning);
1951
+ return write;
1952
+ }
1953
+ function rowCount(count) {
1954
+ if (count === "" || count === "exact") return count;
1955
+ throw invalidRowsRequest("count must be exact");
1956
+ }
1957
+ function rowOrder(order) {
1958
+ return order.map((requested) => ({
1959
+ column: requested.column,
1960
+ descending: requested.ascending === false,
1961
+ nulls: requested.nullsFirst === void 0 ? "" : requested.nullsFirst ? "first" : "last"
1962
+ }));
1963
+ }
1964
+ function rowFilters(filters) {
1965
+ const group = emptyFilter();
1966
+ const embedFilters = [];
1967
+ for (const filter of filters) {
1968
+ if (filter.or !== void 0 || filter.and !== void 0) {
1969
+ group.terms.push(rowFilterGroup(filter, 1));
1970
+ continue;
1971
+ }
1972
+ const path = parseRowColumnPath(filter.column);
1973
+ const condition = rowCondition(filter, path[path.length - 1]);
1974
+ if (path.length > 1) {
1975
+ embedFilters.push({
1976
+ path: path.slice(0, -1),
1977
+ condition
1978
+ });
1979
+ continue;
1980
+ }
1981
+ group.terms.push({ condition });
1982
+ }
1983
+ return [group, embedFilters];
1984
+ }
1985
+ function rowFilterGroup(filter, depth) {
1986
+ if (depth > 64) throw invalidRowsRequest(`filter groups nest deeper than ${64} levels`);
1987
+ if (filter.or !== void 0 === (filter.and !== void 0) || filter.column !== "" || filter.operator !== "" || filter.value !== void 0 || filter.values !== void 0) throw invalidRowsRequest("a filter group sets or or and, and nothing else but not");
1988
+ const members = filterList(filter.or ?? filter.and);
1989
+ const group = {
1990
+ or: filter.or !== void 0,
1991
+ negated: filter.not,
1992
+ terms: []
1993
+ };
1994
+ if (members.length === 0) throw invalidRowsRequest("a filter group needs at least one filter");
1995
+ for (const member of members) {
1996
+ if (member.or !== void 0 || member.and !== void 0) {
1997
+ group.terms.push(rowFilterGroup(member, depth + 1));
1998
+ continue;
1999
+ }
2000
+ group.terms.push({ condition: rowCondition(member, member.column) });
2001
+ }
2002
+ return { group };
2003
+ }
2004
+ function rowCondition(filter, column) {
2005
+ if (column === "") throw invalidRowsRequest("a filter needs a column");
2006
+ const operator = rowOperators.find((candidate) => candidate === filter.operator);
2007
+ if (operator === void 0) throw invalidRowsRequest(`the filter on ${column} needs an operator`);
2008
+ const condition = {
2009
+ column,
2010
+ operator,
2011
+ negated: filter.not,
2012
+ value: "",
2013
+ values: [],
2014
+ is: ""
2015
+ };
2016
+ if (operator === "in") {
2017
+ if (filter.values === void 0 || filter.value !== void 0) throw invalidRowsRequest(`in on ${column} takes values and no value`);
2018
+ condition.values = filter.values;
2019
+ return condition;
2020
+ }
2021
+ if (filter.values !== void 0) throw invalidRowsRequest("only in takes values");
2022
+ if (operator === "is") {
2023
+ const test = rowIsValues.find((candidate) => candidate === (filter.value ?? "null"));
2024
+ if (test === void 0) throw invalidRowsRequest(`is on ${column} takes null, true or false`);
2025
+ condition.is = test;
2026
+ return condition;
2027
+ }
2028
+ if (filter.value === void 0) throw invalidRowsRequest(`the filter on ${column} needs a value`);
2029
+ condition.value = filter.value;
2030
+ return condition;
2031
+ }
2032
+ function parseRowColumnPath(column) {
2033
+ const parser = new RowSelectParser(column, "filter column");
2034
+ const path = [];
2035
+ for (;;) {
2036
+ path.push(parser.fieldName());
2037
+ if (parser.atEnd()) return path;
2038
+ if (!parser.consume(".")) throw parser.fail("expecting '.' or end of input");
2039
+ }
2040
+ }
2041
+ function parseRowSelect(value) {
2042
+ if (value.trim() === "") return [];
2043
+ const parser = new RowSelectParser(value, "select");
2044
+ const items = parser.selectList();
2045
+ if (!parser.atEnd()) throw parser.fail("expecting ',' or end of input");
2046
+ return items;
2047
+ }
2048
+ const rowAggregates = /* @__PURE__ */ new Map([
2049
+ ["avg", "avg"],
2050
+ ["count", "count"],
2051
+ ["max", "max"],
2052
+ ["min", "min"],
2053
+ ["sum", "sum"]
2054
+ ]);
2055
+ const identifierRune = /^[\p{L}\p{Nd}_$ ]$/u;
2056
+ var RowSelectParser = class {
2057
+ constructor(text, what) {
2058
+ this.text = text;
2059
+ this.what = what;
2060
+ this.position = 0;
2061
+ this.depth = 0;
2062
+ this.input = Array.from(text);
2063
+ }
2064
+ atEnd() {
2065
+ return this.position >= this.input.length;
2066
+ }
2067
+ peek() {
2068
+ return this.atEnd() ? "" : this.input[this.position];
2069
+ }
2070
+ hasPrefix(prefix) {
2071
+ return this.prefixEnd(prefix) >= 0;
2072
+ }
2073
+ consume(prefix) {
2074
+ const end = this.prefixEnd(prefix);
2075
+ if (end >= 0) this.position = end;
2076
+ return end >= 0;
2077
+ }
2078
+ prefixEnd(prefix) {
2079
+ let position = this.position;
2080
+ for (const char of Array.from(prefix)) {
2081
+ if (position >= this.input.length || this.input[position] !== char) return -1;
2082
+ position++;
2083
+ }
2084
+ return position;
2085
+ }
2086
+ skipSpace() {
2087
+ while (this.peek() === " " || this.peek() === " ") this.position++;
2088
+ }
2089
+ fail(reason) {
2090
+ const found = this.atEnd() ? "end of input" : `'${this.peek()}'`;
2091
+ return invalidRowsRequest(`the ${this.what} ${JSON.stringify(this.text)} has an unexpected ${found} at position ${this.position + 1}, ${reason}`);
2092
+ }
2093
+ fieldName() {
2094
+ if (this.peek() === "\"") return this.quoted();
2095
+ const parts = [];
2096
+ for (;;) {
2097
+ const start = this.position;
2098
+ while (!this.atEnd() && identifierRune.test(this.peek())) this.position++;
2099
+ const part = this.input.slice(start, this.position).join("").trim();
2100
+ if (part === "") throw this.fail("expecting a name");
2101
+ parts.push(part);
2102
+ if (!this.hasPrefix("-") || this.hasPrefix("->")) return parts.join("-");
2103
+ this.position++;
2104
+ }
2105
+ }
2106
+ quoted() {
2107
+ this.position++;
2108
+ let value = "";
2109
+ while (!this.atEnd()) {
2110
+ const char = this.peek();
2111
+ this.position++;
2112
+ if (char === "\"") return value;
2113
+ if (char === "\\") {
2114
+ if (this.atEnd()) throw this.fail("expecting a character after '\\'");
2115
+ value += this.peek();
2116
+ this.position++;
2117
+ } else value += char;
2118
+ }
2119
+ throw this.fail("expecting a closing '\"'");
2120
+ }
2121
+ selectList() {
2122
+ const items = [];
2123
+ this.skipSpace();
2124
+ if (this.atEnd() || this.peek() === ")") return items;
2125
+ for (;;) {
2126
+ items.push(this.selectItem());
2127
+ this.skipSpace();
2128
+ if (!this.consume(",")) return items;
2129
+ this.skipSpace();
2130
+ }
2131
+ }
2132
+ selectItem() {
2133
+ if (this.hasPrefix("...")) throw invalidRowsRequest("spreading an embedded table is not supported");
2134
+ if (this.consume("*")) {
2135
+ if (!this.atItemEnd()) throw this.fail("expecting ',', ')' or end of input");
2136
+ return { star: true };
2137
+ }
2138
+ const start = this.position;
2139
+ const aggregate = this.aggregate();
2140
+ if (aggregate !== void 0) return aggregate;
2141
+ this.position = start;
2142
+ const embed = this.embed();
2143
+ if (embed !== void 0) return { embed };
2144
+ this.position = start;
2145
+ return this.columnItem();
2146
+ }
2147
+ aggregate() {
2148
+ const alias = this.alias();
2149
+ const quoted = this.peek() === "\"";
2150
+ if (!quoted && !identifierRune.test(this.peek())) return void 0;
2151
+ const name = this.fieldName();
2152
+ const item = { alias };
2153
+ if (!quoted && name === "count" && this.consume("()")) item.aggregate = "count";
2154
+ else if (this.consume(".")) {
2155
+ const quotedFunction = this.peek() === "\"";
2156
+ const aggregate = rowAggregates.get(this.fieldName());
2157
+ if (quotedFunction || aggregate === void 0 || !this.consume("()")) throw invalidRowsRequest("only the aggregates avg(), count(), max(), min() and sum() are supported");
2158
+ item.column = name;
2159
+ item.aggregate = aggregate;
2160
+ } else return;
2161
+ if (this.hasPrefix("::")) throw invalidRowsRequest("casts are not supported");
2162
+ if (!this.atItemEnd()) throw this.fail("expecting ',', ')' or end of input");
2163
+ return item;
2164
+ }
2165
+ atItemEnd() {
2166
+ this.skipSpace();
2167
+ return this.atEnd() || this.peek() === "," || this.peek() === ")";
2168
+ }
2169
+ alias() {
2170
+ const start = this.position;
2171
+ try {
2172
+ const name = this.fieldName();
2173
+ if (this.hasPrefix(":") && !this.hasPrefix("::")) {
2174
+ this.position++;
2175
+ return name;
2176
+ }
2177
+ } catch (error) {
2178
+ if (!(error instanceof RowsRefusal)) throw error;
2179
+ }
2180
+ this.position = start;
2181
+ return "";
2182
+ }
2183
+ embed() {
2184
+ const alias = this.alias();
2185
+ if (this.peek() !== "\"" && !identifierRune.test(this.peek())) return void 0;
2186
+ const embed = {
2187
+ relation: this.fieldName(),
2188
+ alias,
2189
+ hint: "",
2190
+ inner: false,
2191
+ select: []
2192
+ };
2193
+ let hinted = false;
2194
+ let joined = false;
2195
+ while (this.consume("!")) {
2196
+ const parameter = this.fieldName();
2197
+ if ((parameter === "inner" || parameter === "left") && !joined) {
2198
+ embed.inner = parameter === "inner";
2199
+ joined = true;
2200
+ } else if (parameter !== "inner" && parameter !== "left" && !hinted) {
2201
+ embed.hint = parameter;
2202
+ hinted = true;
2203
+ }
2204
+ }
2205
+ if (!this.consume("(")) return void 0;
2206
+ this.depth++;
2207
+ if (this.depth > 3) throw invalidRowsRequest(`embeds nest deeper than ${3} levels`);
2208
+ const items = this.selectList();
2209
+ if (!this.consume(")")) throw this.fail("expecting ',' or ')'");
2210
+ this.depth--;
2211
+ embed.select = items;
2212
+ return embed;
2213
+ }
2214
+ columnItem() {
2215
+ const alias = this.alias();
2216
+ const column = this.fieldName();
2217
+ if (this.hasPrefix("->")) throw invalidRowsRequest("JSON paths are not supported");
2218
+ if (this.hasPrefix("::")) throw invalidRowsRequest("casts are not supported");
2219
+ if (!this.atItemEnd()) throw this.fail("expecting ',', ')' or end of input");
2220
+ return {
2221
+ column,
2222
+ alias
2223
+ };
2224
+ }
2225
+ };
2226
+
2227
+ // src/app/testmode/server.ts
2228
+ const oauthPrefix = `/__oaiz-test/oauth/`;
2229
+ const rowsRoute = /^\/__oaiz-test\/storage\/databases\/([^/]+)\/rows\/(select|insert|upsert|update|delete)$/;
2230
+ const functionCallRoute = /^\/__oaiz-test\/storage\/databases\/([^/]+)\/functions\/call$/;
2231
+ const unsupported = {
2232
+ slug: "test-mode-unsupported",
2233
+ message: "OAIZ test mode runs sign-in, row requests and function calls only. Tools, events, workflow runs, model calls, embeddings and chat need OAIZ."
2234
+ };
2235
+ const databaseNotFound = {
2236
+ message: "the managed database was not found",
2237
+ slug: "storage-database-access-denied"
2238
+ };
2239
+ async function createTestServer(options) {
2240
+ let files = await readTestFiles(options.directory);
2241
+ const open = /* @__PURE__ */ new Map();
2242
+ const build = (id, database) => {
2243
+ const built = (async () => {
2244
+ const opened = await openTestDatabase(database.schemaSQL, databaseFile(id, ".sql"));
2245
+ try {
2246
+ await seed(opened, id, database.seedSQL);
2247
+ } catch (error) {
2248
+ await opened.close();
2249
+ throw error;
2250
+ }
2251
+ return opened;
2252
+ })();
2253
+ const entry = {
2254
+ schemaSQL: database.schemaSQL,
2255
+ database: built
2256
+ };
2257
+ built.catch(() => {
2258
+ if (open.get(id) === entry) open.delete(id);
2259
+ });
2260
+ open.set(id, entry);
2261
+ return entry;
2262
+ };
2263
+ const databaseFor = (id) => {
2264
+ const database = files.databases.get(id);
2265
+ if (database === void 0) return void 0;
2266
+ return (open.get(id) ?? build(id, database)).database;
2267
+ };
2268
+ const auth = createTestAuth(() => files.members.members);
2269
+ async function storageRequest(request, id, decode, run) {
2270
+ const signedIn = auth.authenticate(request);
2271
+ if (signedIn instanceof Response) return signedIn;
2272
+ const caller = {
2273
+ subject: signedIn.member.id,
2274
+ organisationRole: signedIn.member.role ?? "member",
2275
+ teams: signedIn.member.teams ?? [],
2276
+ capability: files.members.grants[id] ?? "edit",
2277
+ app: signedIn.clientId
2278
+ };
2279
+ try {
2280
+ const decoded = decode(await request.text());
2281
+ const database = databaseFor(id);
2282
+ if (database === void 0) return json(404, databaseNotFound);
2283
+ return new Response(await run(await database, caller, decoded), {
2284
+ status: 200,
2285
+ headers: { "content-type": "application/json; charset=utf-8" }
2286
+ });
2287
+ } catch (error) {
2288
+ if (error instanceof RowsRefusal) return json(error.status, error.body());
2289
+ throw error;
2290
+ }
2291
+ }
2292
+ const rows = (request, id, operation) => storageRequest(request, id, (body) => decodeRowsRequest(operation, body), async (database, caller, decoded) => rowsBody(await runRowsRequest(database, caller, decoded), (decoded.read ?? decoded.write).singular));
2293
+ const callFunction = (request, id) => storageRequest(request, id, decodeFunctionCall, async (database, caller, call) => functionCallBody(await runFunctionCall(database, caller, call)));
2294
+ async function reset() {
2295
+ files = await readTestFiles(options.directory);
2296
+ const waiting = [];
2297
+ for (const [id, entry] of [...open]) {
2298
+ const database = files.databases.get(id);
2299
+ if (database !== void 0 && database.schemaSQL === entry.schemaSQL) {
2300
+ waiting.push(entry.database.then((opened) => seed(opened, id, database.seedSQL)).catch(async (error) => {
2301
+ if (open.get(id) === entry) open.delete(id);
2302
+ await closeQuietly(entry);
2303
+ throw error;
2304
+ }));
2305
+ continue;
2306
+ }
2307
+ open.delete(id);
2308
+ waiting.push(closeQuietly(entry));
2309
+ if (database !== void 0) waiting.push(build(id, database).database);
2310
+ }
2311
+ await Promise.all(waiting);
2312
+ }
2313
+ return {
2314
+ async handle(request) {
2315
+ const path = new URL(request.url).pathname;
2316
+ const route = rowsRoute.exec(path);
2317
+ const functionRoute = route ? null : functionCallRoute.exec(path);
2318
+ if (route || functionRoute) {
2319
+ if (request.method !== "POST") return new Response("404 page not found", {
2320
+ status: 404,
2321
+ headers: { "content-type": "text/plain" }
2322
+ });
2323
+ if (route) return rows(request, databaseId(route[1]), route[2]);
2324
+ return callFunction(request, databaseId(functionRoute[1]));
2325
+ }
2326
+ if (path.startsWith(oauthPrefix)) {
2327
+ const answer = await auth.handle(request, path.slice(oauthPrefix.length));
2328
+ if (answer !== void 0) return answer;
2329
+ }
2330
+ return json(501, unsupported);
2331
+ },
2332
+ issueToken: (memberId, clientId) => auth.issueToken(memberId, clientId),
2333
+ reset,
2334
+ async close() {
2335
+ const entries = [...open.values()];
2336
+ open.clear();
2337
+ await Promise.all(entries.map(closeQuietly));
2338
+ }
2339
+ };
2340
+ }
2341
+ function databaseId(segment) {
2342
+ return decodeURIComponent(segment).toLowerCase();
2343
+ }
2344
+ function closeQuietly(entry) {
2345
+ return entry.database.then((opened) => opened.close(), () => void 0);
2346
+ }
2347
+ function databaseFile(id, suffix) {
2348
+ return `tests/databases/${id}${suffix}`;
2349
+ }
2350
+ async function seed(database, id, seedSQL) {
2351
+ try {
2352
+ await database.reset(seedSQL);
2353
+ } catch (error) {
2354
+ if (error instanceof RowsRefusal) throw error;
2355
+ throw new Error(`${databaseFile(id, ".seed.sql")} does not run on its test database: ${error.message}`, { cause: error });
2356
+ }
2357
+ }
2358
+ async function readTestFiles(directory) {
2359
+ const membersText = await readOptional(join(directory, "members.json"));
2360
+ const members = membersText === void 0 ? {
2361
+ teams: [],
2362
+ members: [],
2363
+ grants: {}
2364
+ } : parseMembers(membersText, "tests/members.json");
2365
+ const folder = join(directory, "databases");
2366
+ let names;
2367
+ try {
2368
+ names = await readdir(folder);
2369
+ } catch (error) {
2370
+ if (error.code !== "ENOENT") throw error;
2371
+ names = [];
2372
+ }
2373
+ const schemas = /* @__PURE__ */ new Map();
2374
+ const seeds = /* @__PURE__ */ new Map();
2375
+ for (const name of names.sort()) {
2376
+ const lower = name.toLowerCase();
2377
+ if (lower.endsWith(".seed.sql")) seeds.set(lower.slice(0, -9), await readFile(join(folder, name), "utf8"));
2378
+ else if (lower.endsWith(".sql")) schemas.set(lower.slice(0, -4), await readFile(join(folder, name), "utf8"));
2379
+ }
2380
+ const databases = /* @__PURE__ */ new Map();
2381
+ for (const [id, schemaSQL] of schemas) databases.set(id, {
2382
+ schemaSQL,
2383
+ seedSQL: seeds.get(id) ?? ""
2384
+ });
2385
+ for (const id of seeds.keys()) if (!schemas.has(id)) throw new Error(`${databaseFile(id, ".seed.sql")} has no schema file ${databaseFile(id, ".sql")}`);
2386
+ return {
2387
+ members,
2388
+ databases
2389
+ };
2390
+ }
2391
+ async function readOptional(path) {
2392
+ try {
2393
+ return await readFile(path, "utf8");
2394
+ } catch (error) {
2395
+ if (error.code === "ENOENT") return void 0;
2396
+ throw error;
2397
+ }
2398
+ }
2399
+ function rowsBody(result, single) {
2400
+ let rows = [];
2401
+ if (result.body !== null) {
2402
+ const document = parseJson(result.body);
2403
+ if (single) rows = document === null ? [] : [document];
2404
+ else rows = document;
2405
+ }
2406
+ const count = result.total === null ? "" : `,"count":${result.total}`;
2407
+ return `{"rows":${stringifyJson(rows)}${count}}`;
2408
+ }
2409
+ function functionCallBody(body) {
2410
+ return `{"data":${body === null ? "null" : stringifyJson(parseJson(body))}}`;
2411
+ }
2412
+ function json(status, body, headers = {}) {
2413
+ return new Response(JSON.stringify(body), {
2414
+ status,
2415
+ headers: {
2416
+ "content-type": "application/json; charset=utf-8",
2417
+ ...headers
2418
+ }
2419
+ });
2420
+ }
2421
+
2422
+ // src/app/testmode/plugin.ts
2423
+ const testMode = "oaiz-test";
2424
+ const prefix = "/__oaiz-test/";
2425
+ const resetPath = `${prefix}test/reset`;
2426
+ function oaizTest({ directory = "tests" } = {}) {
2427
+ return {
2428
+ name: "oaiz-test",
2429
+ apply: (_, env) => env.command === "serve" && env.mode === testMode,
2430
+ configureServer(server) {
2431
+ const folder = resolve(server.config.root, directory);
2432
+ let started;
2433
+ server.httpServer?.once("close", () => {
2434
+ started?.then((test) => test.close(), () => void 0);
2435
+ });
2436
+ server.middlewares.use((req, res, next) => {
2437
+ if (!(req.url ?? "").startsWith(prefix)) {
2438
+ next();
2439
+ return;
2440
+ }
2441
+ (async () => {
2442
+ if (started === void 0) {
2443
+ started = createTestServer({ directory: folder });
2444
+ started.catch(() => {
2445
+ started = void 0;
2446
+ });
2447
+ }
2448
+ const test = await started;
2449
+ const request = await fetchRequest(req, server);
2450
+ if (request.method === "POST" && new URL(request.url).pathname === resetPath) {
2451
+ await test.reset();
2452
+ res.statusCode = 204;
2453
+ res.end();
2454
+ return;
2455
+ }
2456
+ await writeResponse(res, await test.handle(request));
2457
+ })().catch((error) => {
2458
+ if (res.headersSent) return next(error);
2459
+ const message = error instanceof Error ? error.message : String(error);
2460
+ server.config.logger.error(`[oaiz-test] ${message}`, { timestamp: true });
2461
+ res.statusCode = 500;
2462
+ res.setHeader("content-type", "application/json");
2463
+ res.end(JSON.stringify({
2464
+ message,
2465
+ slug: "test-mode-setup"
2466
+ }));
2467
+ });
2468
+ });
2469
+ }
2470
+ };
2471
+ }
2472
+ async function fetchRequest(req, server) {
2473
+ const protocol = server.config.server.https ? "https" : "http";
2474
+ const url = new URL(req.url ?? "/", `${protocol}://${req.headers.host}`);
2475
+ const headers = new Headers();
2476
+ for (const [name, value] of Object.entries(req.headers)) {
2477
+ if (value === void 0) continue;
2478
+ for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item);
2479
+ }
2480
+ const method = req.method ?? "GET";
2481
+ const chunks = [];
2482
+ if (method !== "GET" && method !== "HEAD") for await (const chunk of req) chunks.push(chunk);
2483
+ return new Request(url, {
2484
+ method,
2485
+ headers,
2486
+ body: chunks.length === 0 ? void 0 : Buffer.concat(chunks)
2487
+ });
2488
+ }
2489
+ async function writeResponse(res, response) {
2490
+ res.statusCode = response.status;
2491
+ response.headers.forEach((value, name) => res.setHeader(name, value));
2492
+ res.end(Buffer.from(await response.arrayBuffer()));
2493
+ }
2494
+
2495
+ export { createTestServer, oaizTest };