@palbase/backend 22.1.0 → 23.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/bin/palbase-backend.cjs +750 -40
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +5 -5
  4. package/dist/{chunk-YL4C5NRY.js → chunk-HQRJDARQ.js} +2 -2
  5. package/dist/{chunk-74XDEF5J.js → chunk-M5MCBWJI.js} +723 -37
  6. package/dist/chunk-M5MCBWJI.js.map +1 -0
  7. package/dist/{chunk-W5ODXPY3.js → chunk-NS5V43YQ.js} +14 -1
  8. package/dist/chunk-NS5V43YQ.js.map +1 -0
  9. package/dist/{chunk-SQC5EIWY.js → chunk-OHALWEOG.js} +19 -9
  10. package/dist/chunk-OHALWEOG.js.map +1 -0
  11. package/dist/{chunk-I3ON7MYF.js → chunk-PY7YJDCT.js} +129 -18
  12. package/dist/chunk-PY7YJDCT.js.map +1 -0
  13. package/dist/{chunk-N32VDWKH.js → chunk-R3KN6RHD.js} +4 -59
  14. package/dist/chunk-R3KN6RHD.js.map +1 -0
  15. package/dist/{chunk-QMVK4X3V.js → chunk-RCLNBJCM.js} +98 -98
  16. package/dist/chunk-RCLNBJCM.js.map +1 -0
  17. package/dist/db/env.cjs.map +1 -1
  18. package/dist/db/env.d.cts +3 -21
  19. package/dist/db/env.d.ts +3 -21
  20. package/dist/db/index.cjs +140 -16
  21. package/dist/db/index.cjs.map +1 -1
  22. package/dist/db/index.d.cts +3 -2
  23. package/dist/db/index.d.ts +3 -2
  24. package/dist/db/index.js +2 -2
  25. package/dist/{endpoint-BVT6jcVW.d.cts → endpoint-CVWXh6oG.d.ts} +147 -15
  26. package/dist/{endpoint-BVT6jcVW.d.ts → endpoint-c9h5jriX.d.cts} +147 -15
  27. package/dist/engine/index.cjs +750 -40
  28. package/dist/engine/index.cjs.map +1 -1
  29. package/dist/engine/index.d.cts +6 -5
  30. package/dist/engine/index.d.ts +6 -5
  31. package/dist/engine/index.js +4 -4
  32. package/dist/{index-BS1gW4nV.d.cts → index-BZrJXnVh.d.ts} +142 -72
  33. package/dist/{index-BqCiHao8.d.cts → index-By8Dle5U.d.cts} +196 -21
  34. package/dist/{index-vwHoS0l2.d.ts → index-CwAJ7HEe.d.ts} +196 -21
  35. package/dist/{index-CCZqzych.d.ts → index-CxeQSfJP.d.cts} +142 -72
  36. package/dist/index.cjs +388 -1101
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +89 -1134
  39. package/dist/index.d.ts +89 -1134
  40. package/dist/index.js +133 -902
  41. package/dist/index.js.map +1 -1
  42. package/dist/openapi/index.cjs +32 -61
  43. package/dist/openapi/index.cjs.map +1 -1
  44. package/dist/openapi/index.d.cts +6 -2
  45. package/dist/openapi/index.d.ts +6 -2
  46. package/dist/openapi/index.js +34 -26
  47. package/dist/openapi/index.js.map +1 -1
  48. package/dist/{registry-Bsuf-orT.d.ts → registry-B3niOVYp.d.ts} +108 -170
  49. package/dist/{registry-BWttGlaT.d.cts → registry-CqPK2Qby.d.cts} +108 -170
  50. package/dist/{purchases/keys.cjs → stack.cjs} +4 -4
  51. package/dist/stack.cjs.map +1 -0
  52. package/dist/stack.d.cts +76 -0
  53. package/dist/stack.d.ts +76 -0
  54. package/dist/stack.js +1 -0
  55. package/dist/test/index.cjs +482 -9
  56. package/dist/test/index.cjs.map +1 -1
  57. package/dist/test/index.d.cts +35 -3
  58. package/dist/test/index.d.ts +35 -3
  59. package/dist/test/index.js +480 -8
  60. package/dist/test/index.js.map +1 -1
  61. package/docs/README.md +7 -6
  62. package/docs/llms-full.txt +7 -260
  63. package/docs/llms.txt +0 -2
  64. package/package.json +9 -8
  65. package/stager/return_types.js +23 -0
  66. package/template/package.json +1 -1
  67. package/dist/chunk-74XDEF5J.js.map +0 -1
  68. package/dist/chunk-I3ON7MYF.js.map +0 -1
  69. package/dist/chunk-N32VDWKH.js.map +0 -1
  70. package/dist/chunk-QMVK4X3V.js.map +0 -1
  71. package/dist/chunk-SQC5EIWY.js.map +0 -1
  72. package/dist/chunk-W5ODXPY3.js.map +0 -1
  73. package/dist/purchases/keys.cjs.map +0 -1
  74. package/dist/purchases/keys.d.cts +0 -42
  75. package/dist/purchases/keys.d.ts +0 -42
  76. package/dist/purchases/keys.js +0 -1
  77. package/docs/config.md +0 -147
  78. package/docs/resources.md +0 -97
  79. package/template/config/secrets.ts +0 -24
  80. /package/dist/{chunk-YL4C5NRY.js.map → chunk-HQRJDARQ.js.map} +0 -0
  81. /package/dist/{purchases/keys.js.map → stack.js.map} +0 -0
@@ -22,10 +22,117 @@ var test_exports = {};
22
22
  __export(test_exports, {
23
23
  TestApiError: () => TestApiError,
24
24
  api: () => api,
25
- createTestApi: () => createTestApi
25
+ createTestApi: () => createTestApi,
26
+ fakeDatabase: () => fakeDatabase
26
27
  });
27
28
  module.exports = __toCommonJS(test_exports);
28
29
 
30
+ // ../core/dist/index.js
31
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
32
+ var POW_CHALLENGE_ID_HEADER = "X-PoW-Challenge-ID";
33
+ var POW_NONCE_HEADER = "X-PoW-Nonce";
34
+ function asPowChallenge(details) {
35
+ if (typeof details !== "object" || details === null) return null;
36
+ const env = details;
37
+ if (env.error !== "pow_required") return null;
38
+ const c = env.challenge;
39
+ if (typeof c !== "object" || c === null) return null;
40
+ const { id, prefix, difficulty } = c;
41
+ if (typeof id !== "string" || typeof prefix !== "string") return null;
42
+ if (typeof difficulty !== "number" || !Number.isInteger(difficulty) || difficulty < 0) return null;
43
+ return { id, prefix, difficulty };
44
+ }
45
+ var encoder = new TextEncoder();
46
+ var hasher = null;
47
+ async function digester() {
48
+ if (hasher) return hasher;
49
+ const runtime = globalThis;
50
+ const nodeish = runtime.process?.versions?.node !== void 0 || runtime.process?.versions?.bun !== void 0;
51
+ if (nodeish) {
52
+ try {
53
+ const mod = await import(
54
+ /* @vite-ignore */
55
+ `${"node:"}crypto`
56
+ );
57
+ if (typeof mod.createHash === "function") {
58
+ const createHash = mod.createHash;
59
+ hasher = (input) => new Uint8Array(createHash("sha256").update(input).digest());
60
+ return hasher;
61
+ }
62
+ } catch {
63
+ }
64
+ }
65
+ hasher = async (input) => new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(input)));
66
+ return hasher;
67
+ }
68
+ var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
69
+ function leadingZeroBits(hash) {
70
+ let bits = 0;
71
+ for (const byte of hash) {
72
+ if (byte === 0) {
73
+ bits += 8;
74
+ continue;
75
+ }
76
+ return bits + Math.clz32(byte) - 24;
77
+ }
78
+ return bits;
79
+ }
80
+ var MAX_POW_DIFFICULTY = 24;
81
+ function powBudget(difficulty) {
82
+ return 8 * 2 ** difficulty;
83
+ }
84
+ var POW_TIME_BUDGET_MS = 12e4;
85
+ var CALIBRATION_WARMUP = 1024;
86
+ var CALIBRATION_END = 9216;
87
+ async function solvePowChallenge(challenge, maxIterations = powBudget(challenge.difficulty), signal) {
88
+ if (challenge.difficulty > MAX_POW_DIFFICULTY) {
89
+ throw new Error(
90
+ `proof-of-work: refusing difficulty ${challenge.difficulty}; this client attempts at most ${MAX_POW_DIFFICULTY}, which is the highest a Palbase stack issues`
91
+ );
92
+ }
93
+ const digest = await digester();
94
+ let warmedAt = 0;
95
+ let calibrated = false;
96
+ let deadline = Number.POSITIVE_INFINITY;
97
+ for (let nonce = 0; nonce < maxIterations; nonce++) {
98
+ if ((nonce & 1023) === 0) {
99
+ if (signal?.aborted) {
100
+ throw new DOMException("proof-of-work solve aborted", "AbortError");
101
+ }
102
+ if (now() > deadline) {
103
+ throw new Error(
104
+ `proof-of-work: gave up on difficulty ${challenge.difficulty} after ${POW_TIME_BUDGET_MS / 1e3}s and ${nonce.toLocaleString()} attempts \u2014 the tail this run drew is longer than the allowance`
105
+ );
106
+ }
107
+ }
108
+ if (nonce === CALIBRATION_WARMUP) {
109
+ warmedAt = now();
110
+ }
111
+ if (!calibrated && nonce === CALIBRATION_END) {
112
+ calibrated = true;
113
+ const elapsed = Math.max(now() - warmedAt, 1e-3);
114
+ const rate = (CALIBRATION_END - CALIBRATION_WARMUP) / (elapsed / 1e3);
115
+ const expectedMs = 2 ** challenge.difficulty / rate * 1e3;
116
+ if (expectedMs > POW_TIME_BUDGET_MS) {
117
+ throw new Error(
118
+ `proof-of-work: difficulty ${challenge.difficulty} needs about ${Math.round(expectedMs / 1e3)}s here (${Math.round(rate).toLocaleString()} digests/s) and this client allows ${POW_TIME_BUDGET_MS / 1e3}s; refusing before spending the time rather than after`
119
+ );
120
+ }
121
+ deadline = now() + (POW_TIME_BUDGET_MS - (now() - warmedAt));
122
+ }
123
+ const hash = await digest(challenge.prefix + nonce);
124
+ if (leadingZeroBits(hash) >= challenge.difficulty) {
125
+ return {
126
+ [POW_CHALLENGE_ID_HEADER]: challenge.id,
127
+ [POW_NONCE_HEADER]: String(nonce)
128
+ };
129
+ }
130
+ }
131
+ throw new Error(
132
+ `proof-of-work: no nonce found for difficulty ${challenge.difficulty} within ${maxIterations} attempts`
133
+ );
134
+ }
135
+
29
136
  // src/test/api.ts
30
137
  var TestApiError = class extends Error {
31
138
  status;
@@ -53,24 +160,45 @@ function required(value, envName) {
53
160
  }
54
161
  return value;
55
162
  }
163
+ function isLocalTarget(baseUrl) {
164
+ try {
165
+ const { hostname } = new URL(baseUrl);
166
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]" || hostname === "::1";
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+ function secondsUntilExpiry(token) {
172
+ const body = token.split(".")[1];
173
+ if (!body) return null;
174
+ try {
175
+ const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
176
+ return typeof claims.exp === "number" ? claims.exp - Math.floor(Date.now() / 1e3) : null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
56
181
  function createTestApi(config) {
57
182
  const baseUrl = required(config.baseUrl, "PALBASE_TEST_BASE_URL").replace(/\/$/, "");
58
183
  const apiKey = required(config.apiKey, "PALBASE_TEST_API_KEY");
59
- const candidateToken = required(config.candidateToken, "PALBASE_TEST_CANDIDATE");
184
+ const local = isLocalTarget(baseUrl);
185
+ const candidateToken = local ? config.candidateToken ?? "" : required(config.candidateToken ?? "", "PALBASE_TEST_CANDIDATE");
186
+ const doFetch = config.fetch ?? fetch;
60
187
  const requests = [];
61
188
  let bearer = null;
62
189
  async function call(method, path, body, opts = {}) {
63
190
  const headers = {
64
191
  apikey: apiKey,
65
192
  // Selects the release under test. Omit it and the gateway serves the LIVE
66
- // one, which would make the whole suite grade the wrong code.
67
- "x-palbase-candidate": candidateToken,
193
+ // one, which would make the whole suite grade the wrong code. Absent only
194
+ // against a local stack, which has a single version.
195
+ ...candidateToken ? { "x-palbase-candidate": candidateToken } : {},
68
196
  ...opts.headers
69
197
  };
70
198
  if (bearer) headers.authorization = `Bearer ${bearer}`;
71
199
  if (body !== void 0) headers["content-type"] = "application/json";
72
200
  const startedAt = Date.now();
73
- const res = await fetch(`${baseUrl}${path}`, {
201
+ const res = await doFetch(`${baseUrl}${path}`, {
74
202
  method,
75
203
  headers,
76
204
  body: body === void 0 ? void 0 : JSON.stringify(body)
@@ -78,7 +206,18 @@ function createTestApi(config) {
78
206
  const text = await res.text();
79
207
  const parsed = text ? safeParse(text) : void 0;
80
208
  requests.push({ method, path, status: res.status, ms: Date.now() - startedAt });
81
- if (!res.ok) throw new TestApiError(method, path, res.status, parsed);
209
+ if (!res.ok) {
210
+ if (res.status === 401 && bearer) {
211
+ const left = secondsUntilExpiry(bearer);
212
+ if (left !== null && left <= 0) {
213
+ throw new TestApiError(method, path, res.status, {
214
+ error: "access_token_expired",
215
+ error_description: `this run's access token EXPIRED ${Math.abs(left)}s ago \u2014 a test identity is minted for the length of ONE deploy, so a saved token does not survive to the next run. Re-mint it (\`palbase test-user create --json\`, or let \`palbase test\` do it) and run again.`
216
+ });
217
+ }
218
+ }
219
+ throw new TestApiError(method, path, res.status, parsed);
220
+ }
82
221
  return parsed;
83
222
  }
84
223
  return {
@@ -110,11 +249,21 @@ function createTestApi(config) {
110
249
  return this.signIn(identity);
111
250
  },
112
251
  async signIn(credentials) {
113
- const result = await call(
252
+ const attempt = async (extra) => call(
114
253
  "POST",
115
254
  "/auth/login",
116
- credentials
255
+ credentials,
256
+ extra ? { headers: extra } : {}
117
257
  );
258
+ let result;
259
+ try {
260
+ result = await attempt();
261
+ } catch (e) {
262
+ const refusal = e;
263
+ const challenge = refusal.status === 403 ? asPowChallenge(refusal.body) : null;
264
+ if (!challenge) throw e;
265
+ result = await attempt(await solvePowChallenge(challenge));
266
+ }
118
267
  bearer = result.access_token;
119
268
  return result.user ?? { id: "" };
120
269
  },
@@ -154,10 +303,334 @@ var api = new Proxy({}, {
154
303
  return Reflect.get(configured, prop, configured);
155
304
  }
156
305
  });
306
+
307
+ // src/__tests__/helpers/mock-db.ts
308
+ function createMockDB() {
309
+ const store = /* @__PURE__ */ new Map();
310
+ const tracked = {
311
+ inserted: /* @__PURE__ */ new Map(),
312
+ updated: /* @__PURE__ */ new Map(),
313
+ deleted: /* @__PURE__ */ new Map()
314
+ };
315
+ function rowsOf(table) {
316
+ let rows = store.get(table);
317
+ if (!rows) {
318
+ rows = [];
319
+ store.set(table, rows);
320
+ }
321
+ return rows;
322
+ }
323
+ function track(map, table, row) {
324
+ const list = map.get(table);
325
+ if (list) list.push(row);
326
+ else map.set(table, [row]);
327
+ }
328
+ const ops = {
329
+ async search(_table, _params) {
330
+ return [];
331
+ },
332
+ async similar() {
333
+ return [];
334
+ },
335
+ async recommend() {
336
+ return [];
337
+ },
338
+ async supersede(_table, _id, row) {
339
+ return { id: crypto.randomUUID(), ...row };
340
+ },
341
+ async query(_sql, _params) {
342
+ return [];
343
+ },
344
+ async insert(table, data) {
345
+ const record = { id: crypto.randomUUID(), ...data };
346
+ rowsOf(table).push(record);
347
+ track(tracked.inserted, table, record);
348
+ return record;
349
+ },
350
+ // Same semantics the engine's SQL has: match on the conflict columns, update
351
+ // everything else, and return the resulting row either way.
352
+ async upsert(table, data, opts) {
353
+ if (opts.onConflict.length === 0) {
354
+ throw new Error(`upsert into ${table}: onConflict en az bir kolon ad\u0131 ister`);
355
+ }
356
+ const rows = rowsOf(table);
357
+ const existing = rows.find((r) => opts.onConflict.every((c) => r[c] === data[c]));
358
+ if (existing) {
359
+ for (const [k, v] of Object.entries(data)) {
360
+ if (!opts.onConflict.includes(k)) existing[k] = v;
361
+ }
362
+ return existing;
363
+ }
364
+ const record = { id: crypto.randomUUID(), ...data };
365
+ rows.push(record);
366
+ track(tracked.inserted, table, record);
367
+ return record;
368
+ },
369
+ async update(table, id, data) {
370
+ const rows = store.get(table) ?? [];
371
+ const idx = rows.findIndex((r) => r["id"] === id);
372
+ const updated = idx >= 0 ? { ...rows[idx], ...data } : { id, ...data };
373
+ if (idx >= 0) {
374
+ rows[idx] = updated;
375
+ }
376
+ track(tracked.updated, table, updated);
377
+ return updated;
378
+ },
379
+ async delete(table, id) {
380
+ const rows = store.get(table) ?? [];
381
+ const idx = rows.findIndex((r) => r["id"] === id);
382
+ if (idx >= 0) rows.splice(idx, 1);
383
+ const list = tracked.deleted.get(table);
384
+ if (list) list.push(id);
385
+ else tracked.deleted.set(table, [id]);
386
+ },
387
+ async findById(table, id) {
388
+ const rows = store.get(table) ?? [];
389
+ return rows.find((r) => r["id"] === id) ?? null;
390
+ },
391
+ // The SAME filter language the engine compiles to SQL: a plain value is
392
+ // equality, an object is an operator set. A fake that understood less would
393
+ // pass a service test that the live database then fails — which is the one
394
+ // thing a stand-in must never do.
395
+ async findMany(table, query, opts) {
396
+ const rows = store.get(table) ?? [];
397
+ const matches2 = (row, key, cond) => {
398
+ if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
399
+ return Object.entries(cond).every(([op, v]) => {
400
+ const cell = row[key];
401
+ switch (op) {
402
+ case "in":
403
+ return Array.isArray(v) && v.includes(cell);
404
+ case "neq":
405
+ return cell !== v;
406
+ case "gt":
407
+ return cell > v;
408
+ case "gte":
409
+ return cell >= v;
410
+ case "lt":
411
+ return cell < v;
412
+ case "lte":
413
+ return cell <= v;
414
+ default:
415
+ throw new Error(`findMany(${table}): where.${key} bilinmeyen operat\xF6r "${op}"`);
416
+ }
417
+ });
418
+ }
419
+ return row[key] === cond;
420
+ };
421
+ let out = query ? rows.filter((row) => Object.entries(query).every(([k, c]) => matches2(row, k, c))) : [...rows];
422
+ const order = opts?.orderBy;
423
+ if (order) {
424
+ const dir = order.direction === "desc" ? -1 : 1;
425
+ out = [...out].sort((a, b) => {
426
+ const x = a[order.column];
427
+ const y = b[order.column];
428
+ return x === y ? 0 : (x < y ? -1 : 1) * dir;
429
+ });
430
+ }
431
+ return opts?.limit === void 0 ? out : out.slice(0, opts.limit);
432
+ }
433
+ };
434
+ async function txPlan(plan) {
435
+ const snapshot = /* @__PURE__ */ new Map();
436
+ for (const [table, rows] of store) snapshot.set(table, [...rows]);
437
+ const trackedSnapshot = {
438
+ inserted: cloneTracked(tracked.inserted),
439
+ updated: cloneTracked(tracked.updated),
440
+ deleted: new Map([...tracked.deleted].map(([k, v]) => [k, [...v]]))
441
+ };
442
+ const results = [];
443
+ try {
444
+ for (const op of plan.ops) {
445
+ const result = applyOp(op, results);
446
+ results.push(result);
447
+ const failure = guardFailure(op.guard, result.rows.length);
448
+ if (failure) throw failure;
449
+ }
450
+ } catch (err) {
451
+ store.clear();
452
+ for (const [table, rows] of snapshot) store.set(table, rows);
453
+ tracked.inserted = trackedSnapshot.inserted;
454
+ tracked.updated = trackedSnapshot.updated;
455
+ tracked.deleted = trackedSnapshot.deleted;
456
+ throw err;
457
+ }
458
+ return { results };
459
+ }
460
+ function applyOp(op, results) {
461
+ switch (op.op) {
462
+ case "upsert": {
463
+ const values = resolveMap(op.values ?? {}, results, null);
464
+ const conflict = op.onConflict ?? [];
465
+ const rows = rowsOf(op.table);
466
+ const hit = rows.find((r) => conflict.every((c) => r[c] === values[c]));
467
+ if (hit) {
468
+ for (const [key, value] of Object.entries(values)) {
469
+ if (!conflict.includes(key)) hit[key] = value;
470
+ }
471
+ return { rows: [hit], rows_affected: 1 };
472
+ }
473
+ const created = { id: crypto.randomUUID(), ...values };
474
+ rows.push(created);
475
+ track(tracked.inserted, op.table, created);
476
+ return { rows: [created], rows_affected: 1 };
477
+ }
478
+ case "insert": {
479
+ const record = { id: crypto.randomUUID(), ...resolveMap(op.values ?? {}, results, null) };
480
+ rowsOf(op.table).push(record);
481
+ track(tracked.inserted, op.table, record);
482
+ return { rows: [record], rows_affected: 1 };
483
+ }
484
+ case "insertMany": {
485
+ const written = (op.rows ?? []).map((row) => {
486
+ const record = { id: crypto.randomUUID(), ...resolveMap(row, results, null) };
487
+ rowsOf(op.table).push(record);
488
+ track(tracked.inserted, op.table, record);
489
+ return record;
490
+ });
491
+ return { rows: written, rows_affected: written.length };
492
+ }
493
+ case "update": {
494
+ const rows = rowsOf(op.table);
495
+ const where = resolveMap(op.where ?? {}, results, null);
496
+ const written = [];
497
+ for (let i = 0; i < rows.length; i++) {
498
+ const row = rows[i];
499
+ if (!row || !matches(row, where)) continue;
500
+ const next = { ...row, ...resolveMap(op.set ?? {}, results, row) };
501
+ rows[i] = next;
502
+ track(tracked.updated, op.table, next);
503
+ written.push(next);
504
+ }
505
+ return { rows: written, rows_affected: written.length };
506
+ }
507
+ case "delete": {
508
+ const rows = rowsOf(op.table);
509
+ const where = resolveMap(op.where ?? {}, results, null);
510
+ const removed = rows.filter((row) => matches(row, where));
511
+ for (const row of removed) {
512
+ rows.splice(rows.indexOf(row), 1);
513
+ const id = row["id"];
514
+ const list = tracked.deleted.get(op.table);
515
+ const key = typeof id === "string" ? id : String(id);
516
+ if (list) list.push(key);
517
+ else tracked.deleted.set(op.table, [key]);
518
+ }
519
+ return { rows: removed, rows_affected: removed.length };
520
+ }
521
+ case "select": {
522
+ const where = resolveMap(op.where ?? {}, results, null);
523
+ let found = rowsOf(op.table).filter((row) => matches(row, where));
524
+ if (op.limit !== void 0) found = found.slice(0, op.limit);
525
+ return { rows: found, rows_affected: found.length };
526
+ }
527
+ }
528
+ }
529
+ const client = {
530
+ ...ops,
531
+ // No real savepoint in memory: the fake runs the callback against the SAME
532
+ // store. An assertion about rollback here would be asserting the fake.
533
+ attempt: (fn) => fn(ops),
534
+ txPlan,
535
+ // In tests there is no real DB role; `asService()` returns the same
536
+ // in-memory client so RLS-bypass code paths still hit the same store and
537
+ // tracking maps. The omitted `asService` matches the contract (no
538
+ // double-bypass), so callers can't recurse.
539
+ asService() {
540
+ return client;
541
+ },
542
+ inserted(table) {
543
+ return tracked.inserted.get(table) ?? [];
544
+ },
545
+ updated(table) {
546
+ return tracked.updated.get(table) ?? [];
547
+ },
548
+ deleted(table) {
549
+ return tracked.deleted.get(table) ?? [];
550
+ },
551
+ seed(table, data) {
552
+ store.set(table, [...data]);
553
+ }
554
+ };
555
+ return client;
556
+ }
557
+ function cloneTracked(map) {
558
+ return new Map([...map].map(([k, v]) => [k, [...v]]));
559
+ }
560
+ function resolveValue(value, results, current, column) {
561
+ if (typeof value !== "object" || value === null) return value;
562
+ const tagged = value;
563
+ if (tagged.$ref) {
564
+ const row = results[tagged.$ref.op]?.rows[0];
565
+ if (!row) {
566
+ throw txRejection(409, "tx_ref_unresolved", {
567
+ message: `operation ${tagged.$ref.op} produced no row to reference`
568
+ });
569
+ }
570
+ return row[tagged.$ref.field];
571
+ }
572
+ if (tagged.$expr) {
573
+ const fn = tagged.$expr["fn"];
574
+ if (fn === "now") return (/* @__PURE__ */ new Date()).toISOString();
575
+ const by = Number(tagged.$expr["by"]);
576
+ const base = Number(current?.[column] ?? 0);
577
+ return fn === "dec" ? base - by : base + by;
578
+ }
579
+ return value;
580
+ }
581
+ function resolveMap(map, results, current) {
582
+ const out = {};
583
+ for (const [key, value] of Object.entries(map)) {
584
+ out[key] = resolveValue(value, results, current, key);
585
+ }
586
+ return out;
587
+ }
588
+ function matches(row, where) {
589
+ return Object.entries(where).every(
590
+ ([key, value]) => value === null ? row[key] === null || row[key] === void 0 : row[key] === value
591
+ );
592
+ }
593
+ function guardFailure(guard, count) {
594
+ if (!guard) return null;
595
+ const ok = guard.kind === "one" ? count === 1 : guard.kind === "none" ? count === 0 : guard.kind === "atLeast" ? count >= guard.n : count <= guard.n;
596
+ if (ok) return null;
597
+ return txRejection(409, "tx_guard_failed", {
598
+ slot: guard.slot,
599
+ message: `expected ${guard.kind} ${guard.n} row(s), got ${count}`
600
+ });
601
+ }
602
+ function txRejection(status, code, extra) {
603
+ const err = new Error(extra.message);
604
+ err.status = status;
605
+ err.error_code = code;
606
+ if (extra.slot !== void 0) err.slot = extra.slot;
607
+ return err;
608
+ }
609
+
610
+ // src/test/fake-db.ts
611
+ function fakeDatabase() {
612
+ const mock = createMockDB();
613
+ const queries = [];
614
+ const db = Object.assign(Object.create(Object.getPrototypeOf(mock)), mock, {
615
+ query: async (sql, params = []) => {
616
+ queries.push({ sql, params });
617
+ return mock.query(sql, params);
618
+ }
619
+ });
620
+ return {
621
+ db,
622
+ queries,
623
+ seed: (table, rows) => mock.seed(table, rows),
624
+ inserted: (table) => mock.inserted(table),
625
+ updated: (table) => mock.updated(table),
626
+ deleted: (table) => mock.deleted(table)
627
+ };
628
+ }
157
629
  // Annotate the CommonJS export names for ESM import in node:
158
630
  0 && (module.exports = {
159
631
  TestApiError,
160
632
  api,
161
- createTestApi
633
+ createTestApi,
634
+ fakeDatabase
162
635
  });
163
636
  //# sourceMappingURL=index.cjs.map