@palbase/backend 23.0.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 (60) hide show
  1. package/dist/bin/palbase-backend.cjs +313 -24
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +5 -5
  4. package/dist/{chunk-Y5HXVUMP.js → chunk-HQRJDARQ.js} +2 -2
  5. package/dist/{chunk-REZU6UKT.js → chunk-M5MCBWJI.js} +296 -25
  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-FSGSB42K.js → chunk-OHALWEOG.js} +9 -4
  10. package/dist/chunk-OHALWEOG.js.map +1 -0
  11. package/dist/{chunk-HAF67F2H.js → chunk-PY7YJDCT.js} +57 -3
  12. package/dist/chunk-PY7YJDCT.js.map +1 -0
  13. package/dist/{chunk-ZC6Q2BRD.js → chunk-R3KN6RHD.js} +2 -2
  14. package/dist/{chunk-OMRTHM4X.js → chunk-RCLNBJCM.js} +1 -1
  15. package/dist/chunk-RCLNBJCM.js.map +1 -0
  16. package/dist/db/index.cjs +68 -1
  17. package/dist/db/index.cjs.map +1 -1
  18. package/dist/db/index.d.cts +2 -2
  19. package/dist/db/index.d.ts +2 -2
  20. package/dist/db/index.js +2 -2
  21. package/dist/{endpoint-BavvbW4P.d.ts → endpoint-CVWXh6oG.d.ts} +70 -8
  22. package/dist/{endpoint-i8TTCohk.d.cts → endpoint-c9h5jriX.d.cts} +70 -8
  23. package/dist/engine/index.cjs +313 -24
  24. package/dist/engine/index.cjs.map +1 -1
  25. package/dist/engine/index.d.cts +4 -4
  26. package/dist/engine/index.d.ts +4 -4
  27. package/dist/engine/index.js +4 -4
  28. package/dist/{index-B7YBEG5w.d.ts → index-BZrJXnVh.d.ts} +49 -4
  29. package/dist/{index-Bmvx1EvJ.d.cts → index-By8Dle5U.d.cts} +86 -17
  30. package/dist/{index-B3jmmItD.d.ts → index-CwAJ7HEe.d.ts} +86 -17
  31. package/dist/{index-E7OscPJT.d.cts → index-CxeQSfJP.d.cts} +49 -4
  32. package/dist/index.cjs +104 -3
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +8 -8
  35. package/dist/index.d.ts +8 -8
  36. package/dist/index.js +32 -6
  37. package/dist/index.js.map +1 -1
  38. package/dist/openapi/index.cjs +29 -4
  39. package/dist/openapi/index.cjs.map +1 -1
  40. package/dist/openapi/index.d.cts +5 -2
  41. package/dist/openapi/index.d.ts +5 -2
  42. package/dist/openapi/index.js +32 -7
  43. package/dist/openapi/index.js.map +1 -1
  44. package/dist/{registry-DY3d9l1k.d.ts → registry-B3niOVYp.d.ts} +101 -7
  45. package/dist/{registry-C3H2uPeZ.d.cts → registry-CqPK2Qby.d.cts} +101 -7
  46. package/dist/test/index.cjs +482 -9
  47. package/dist/test/index.cjs.map +1 -1
  48. package/dist/test/index.d.cts +35 -3
  49. package/dist/test/index.d.ts +35 -3
  50. package/dist/test/index.js +480 -8
  51. package/dist/test/index.js.map +1 -1
  52. package/package.json +4 -3
  53. package/stager/return_types.js +23 -0
  54. package/dist/chunk-FSGSB42K.js.map +0 -1
  55. package/dist/chunk-HAF67F2H.js.map +0 -1
  56. package/dist/chunk-OMRTHM4X.js.map +0 -1
  57. package/dist/chunk-REZU6UKT.js.map +0 -1
  58. package/dist/chunk-W5ODXPY3.js.map +0 -1
  59. /package/dist/{chunk-Y5HXVUMP.js.map → chunk-HQRJDARQ.js.map} +0 -0
  60. /package/dist/{chunk-ZC6Q2BRD.js.map → chunk-R3KN6RHD.js.map} +0 -0
@@ -1,5 +1,111 @@
1
1
  import "../chunk-7D4SUZUM.js";
2
2
 
3
+ // ../core/dist/index.js
4
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
5
+ var POW_CHALLENGE_ID_HEADER = "X-PoW-Challenge-ID";
6
+ var POW_NONCE_HEADER = "X-PoW-Nonce";
7
+ function asPowChallenge(details) {
8
+ if (typeof details !== "object" || details === null) return null;
9
+ const env = details;
10
+ if (env.error !== "pow_required") return null;
11
+ const c = env.challenge;
12
+ if (typeof c !== "object" || c === null) return null;
13
+ const { id, prefix, difficulty } = c;
14
+ if (typeof id !== "string" || typeof prefix !== "string") return null;
15
+ if (typeof difficulty !== "number" || !Number.isInteger(difficulty) || difficulty < 0) return null;
16
+ return { id, prefix, difficulty };
17
+ }
18
+ var encoder = new TextEncoder();
19
+ var hasher = null;
20
+ async function digester() {
21
+ if (hasher) return hasher;
22
+ const runtime = globalThis;
23
+ const nodeish = runtime.process?.versions?.node !== void 0 || runtime.process?.versions?.bun !== void 0;
24
+ if (nodeish) {
25
+ try {
26
+ const mod = await import(
27
+ /* @vite-ignore */
28
+ `${"node:"}crypto`
29
+ );
30
+ if (typeof mod.createHash === "function") {
31
+ const createHash = mod.createHash;
32
+ hasher = (input) => new Uint8Array(createHash("sha256").update(input).digest());
33
+ return hasher;
34
+ }
35
+ } catch {
36
+ }
37
+ }
38
+ hasher = async (input) => new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(input)));
39
+ return hasher;
40
+ }
41
+ var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
42
+ function leadingZeroBits(hash) {
43
+ let bits = 0;
44
+ for (const byte of hash) {
45
+ if (byte === 0) {
46
+ bits += 8;
47
+ continue;
48
+ }
49
+ return bits + Math.clz32(byte) - 24;
50
+ }
51
+ return bits;
52
+ }
53
+ var MAX_POW_DIFFICULTY = 24;
54
+ function powBudget(difficulty) {
55
+ return 8 * 2 ** difficulty;
56
+ }
57
+ var POW_TIME_BUDGET_MS = 12e4;
58
+ var CALIBRATION_WARMUP = 1024;
59
+ var CALIBRATION_END = 9216;
60
+ async function solvePowChallenge(challenge, maxIterations = powBudget(challenge.difficulty), signal) {
61
+ if (challenge.difficulty > MAX_POW_DIFFICULTY) {
62
+ throw new Error(
63
+ `proof-of-work: refusing difficulty ${challenge.difficulty}; this client attempts at most ${MAX_POW_DIFFICULTY}, which is the highest a Palbase stack issues`
64
+ );
65
+ }
66
+ const digest = await digester();
67
+ let warmedAt = 0;
68
+ let calibrated = false;
69
+ let deadline = Number.POSITIVE_INFINITY;
70
+ for (let nonce = 0; nonce < maxIterations; nonce++) {
71
+ if ((nonce & 1023) === 0) {
72
+ if (signal?.aborted) {
73
+ throw new DOMException("proof-of-work solve aborted", "AbortError");
74
+ }
75
+ if (now() > deadline) {
76
+ throw new Error(
77
+ `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`
78
+ );
79
+ }
80
+ }
81
+ if (nonce === CALIBRATION_WARMUP) {
82
+ warmedAt = now();
83
+ }
84
+ if (!calibrated && nonce === CALIBRATION_END) {
85
+ calibrated = true;
86
+ const elapsed = Math.max(now() - warmedAt, 1e-3);
87
+ const rate = (CALIBRATION_END - CALIBRATION_WARMUP) / (elapsed / 1e3);
88
+ const expectedMs = 2 ** challenge.difficulty / rate * 1e3;
89
+ if (expectedMs > POW_TIME_BUDGET_MS) {
90
+ throw new Error(
91
+ `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`
92
+ );
93
+ }
94
+ deadline = now() + (POW_TIME_BUDGET_MS - (now() - warmedAt));
95
+ }
96
+ const hash = await digest(challenge.prefix + nonce);
97
+ if (leadingZeroBits(hash) >= challenge.difficulty) {
98
+ return {
99
+ [POW_CHALLENGE_ID_HEADER]: challenge.id,
100
+ [POW_NONCE_HEADER]: String(nonce)
101
+ };
102
+ }
103
+ }
104
+ throw new Error(
105
+ `proof-of-work: no nonce found for difficulty ${challenge.difficulty} within ${maxIterations} attempts`
106
+ );
107
+ }
108
+
3
109
  // src/test/api.ts
4
110
  var TestApiError = class extends Error {
5
111
  status;
@@ -27,24 +133,45 @@ function required(value, envName) {
27
133
  }
28
134
  return value;
29
135
  }
136
+ function isLocalTarget(baseUrl) {
137
+ try {
138
+ const { hostname } = new URL(baseUrl);
139
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]" || hostname === "::1";
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+ function secondsUntilExpiry(token) {
145
+ const body = token.split(".")[1];
146
+ if (!body) return null;
147
+ try {
148
+ const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
149
+ return typeof claims.exp === "number" ? claims.exp - Math.floor(Date.now() / 1e3) : null;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
30
154
  function createTestApi(config) {
31
155
  const baseUrl = required(config.baseUrl, "PALBASE_TEST_BASE_URL").replace(/\/$/, "");
32
156
  const apiKey = required(config.apiKey, "PALBASE_TEST_API_KEY");
33
- const candidateToken = required(config.candidateToken, "PALBASE_TEST_CANDIDATE");
157
+ const local = isLocalTarget(baseUrl);
158
+ const candidateToken = local ? config.candidateToken ?? "" : required(config.candidateToken ?? "", "PALBASE_TEST_CANDIDATE");
159
+ const doFetch = config.fetch ?? fetch;
34
160
  const requests = [];
35
161
  let bearer = null;
36
162
  async function call(method, path, body, opts = {}) {
37
163
  const headers = {
38
164
  apikey: apiKey,
39
165
  // Selects the release under test. Omit it and the gateway serves the LIVE
40
- // one, which would make the whole suite grade the wrong code.
41
- "x-palbase-candidate": candidateToken,
166
+ // one, which would make the whole suite grade the wrong code. Absent only
167
+ // against a local stack, which has a single version.
168
+ ...candidateToken ? { "x-palbase-candidate": candidateToken } : {},
42
169
  ...opts.headers
43
170
  };
44
171
  if (bearer) headers.authorization = `Bearer ${bearer}`;
45
172
  if (body !== void 0) headers["content-type"] = "application/json";
46
173
  const startedAt = Date.now();
47
- const res = await fetch(`${baseUrl}${path}`, {
174
+ const res = await doFetch(`${baseUrl}${path}`, {
48
175
  method,
49
176
  headers,
50
177
  body: body === void 0 ? void 0 : JSON.stringify(body)
@@ -52,7 +179,18 @@ function createTestApi(config) {
52
179
  const text = await res.text();
53
180
  const parsed = text ? safeParse(text) : void 0;
54
181
  requests.push({ method, path, status: res.status, ms: Date.now() - startedAt });
55
- if (!res.ok) throw new TestApiError(method, path, res.status, parsed);
182
+ if (!res.ok) {
183
+ if (res.status === 401 && bearer) {
184
+ const left = secondsUntilExpiry(bearer);
185
+ if (left !== null && left <= 0) {
186
+ throw new TestApiError(method, path, res.status, {
187
+ error: "access_token_expired",
188
+ 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.`
189
+ });
190
+ }
191
+ }
192
+ throw new TestApiError(method, path, res.status, parsed);
193
+ }
56
194
  return parsed;
57
195
  }
58
196
  return {
@@ -84,11 +222,21 @@ function createTestApi(config) {
84
222
  return this.signIn(identity);
85
223
  },
86
224
  async signIn(credentials) {
87
- const result = await call(
225
+ const attempt = async (extra) => call(
88
226
  "POST",
89
227
  "/auth/login",
90
- credentials
228
+ credentials,
229
+ extra ? { headers: extra } : {}
91
230
  );
231
+ let result;
232
+ try {
233
+ result = await attempt();
234
+ } catch (e) {
235
+ const refusal = e;
236
+ const challenge = refusal.status === 403 ? asPowChallenge(refusal.body) : null;
237
+ if (!challenge) throw e;
238
+ result = await attempt(await solvePowChallenge(challenge));
239
+ }
92
240
  bearer = result.access_token;
93
241
  return result.user ?? { id: "" };
94
242
  },
@@ -128,9 +276,333 @@ var api = new Proxy({}, {
128
276
  return Reflect.get(configured, prop, configured);
129
277
  }
130
278
  });
279
+
280
+ // src/__tests__/helpers/mock-db.ts
281
+ function createMockDB() {
282
+ const store = /* @__PURE__ */ new Map();
283
+ const tracked = {
284
+ inserted: /* @__PURE__ */ new Map(),
285
+ updated: /* @__PURE__ */ new Map(),
286
+ deleted: /* @__PURE__ */ new Map()
287
+ };
288
+ function rowsOf(table) {
289
+ let rows = store.get(table);
290
+ if (!rows) {
291
+ rows = [];
292
+ store.set(table, rows);
293
+ }
294
+ return rows;
295
+ }
296
+ function track(map, table, row) {
297
+ const list = map.get(table);
298
+ if (list) list.push(row);
299
+ else map.set(table, [row]);
300
+ }
301
+ const ops = {
302
+ async search(_table, _params) {
303
+ return [];
304
+ },
305
+ async similar() {
306
+ return [];
307
+ },
308
+ async recommend() {
309
+ return [];
310
+ },
311
+ async supersede(_table, _id, row) {
312
+ return { id: crypto.randomUUID(), ...row };
313
+ },
314
+ async query(_sql, _params) {
315
+ return [];
316
+ },
317
+ async insert(table, data) {
318
+ const record = { id: crypto.randomUUID(), ...data };
319
+ rowsOf(table).push(record);
320
+ track(tracked.inserted, table, record);
321
+ return record;
322
+ },
323
+ // Same semantics the engine's SQL has: match on the conflict columns, update
324
+ // everything else, and return the resulting row either way.
325
+ async upsert(table, data, opts) {
326
+ if (opts.onConflict.length === 0) {
327
+ throw new Error(`upsert into ${table}: onConflict en az bir kolon ad\u0131 ister`);
328
+ }
329
+ const rows = rowsOf(table);
330
+ const existing = rows.find((r) => opts.onConflict.every((c) => r[c] === data[c]));
331
+ if (existing) {
332
+ for (const [k, v] of Object.entries(data)) {
333
+ if (!opts.onConflict.includes(k)) existing[k] = v;
334
+ }
335
+ return existing;
336
+ }
337
+ const record = { id: crypto.randomUUID(), ...data };
338
+ rows.push(record);
339
+ track(tracked.inserted, table, record);
340
+ return record;
341
+ },
342
+ async update(table, id, data) {
343
+ const rows = store.get(table) ?? [];
344
+ const idx = rows.findIndex((r) => r["id"] === id);
345
+ const updated = idx >= 0 ? { ...rows[idx], ...data } : { id, ...data };
346
+ if (idx >= 0) {
347
+ rows[idx] = updated;
348
+ }
349
+ track(tracked.updated, table, updated);
350
+ return updated;
351
+ },
352
+ async delete(table, id) {
353
+ const rows = store.get(table) ?? [];
354
+ const idx = rows.findIndex((r) => r["id"] === id);
355
+ if (idx >= 0) rows.splice(idx, 1);
356
+ const list = tracked.deleted.get(table);
357
+ if (list) list.push(id);
358
+ else tracked.deleted.set(table, [id]);
359
+ },
360
+ async findById(table, id) {
361
+ const rows = store.get(table) ?? [];
362
+ return rows.find((r) => r["id"] === id) ?? null;
363
+ },
364
+ // The SAME filter language the engine compiles to SQL: a plain value is
365
+ // equality, an object is an operator set. A fake that understood less would
366
+ // pass a service test that the live database then fails — which is the one
367
+ // thing a stand-in must never do.
368
+ async findMany(table, query, opts) {
369
+ const rows = store.get(table) ?? [];
370
+ const matches2 = (row, key, cond) => {
371
+ if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
372
+ return Object.entries(cond).every(([op, v]) => {
373
+ const cell = row[key];
374
+ switch (op) {
375
+ case "in":
376
+ return Array.isArray(v) && v.includes(cell);
377
+ case "neq":
378
+ return cell !== v;
379
+ case "gt":
380
+ return cell > v;
381
+ case "gte":
382
+ return cell >= v;
383
+ case "lt":
384
+ return cell < v;
385
+ case "lte":
386
+ return cell <= v;
387
+ default:
388
+ throw new Error(`findMany(${table}): where.${key} bilinmeyen operat\xF6r "${op}"`);
389
+ }
390
+ });
391
+ }
392
+ return row[key] === cond;
393
+ };
394
+ let out = query ? rows.filter((row) => Object.entries(query).every(([k, c]) => matches2(row, k, c))) : [...rows];
395
+ const order = opts?.orderBy;
396
+ if (order) {
397
+ const dir = order.direction === "desc" ? -1 : 1;
398
+ out = [...out].sort((a, b) => {
399
+ const x = a[order.column];
400
+ const y = b[order.column];
401
+ return x === y ? 0 : (x < y ? -1 : 1) * dir;
402
+ });
403
+ }
404
+ return opts?.limit === void 0 ? out : out.slice(0, opts.limit);
405
+ }
406
+ };
407
+ async function txPlan(plan) {
408
+ const snapshot = /* @__PURE__ */ new Map();
409
+ for (const [table, rows] of store) snapshot.set(table, [...rows]);
410
+ const trackedSnapshot = {
411
+ inserted: cloneTracked(tracked.inserted),
412
+ updated: cloneTracked(tracked.updated),
413
+ deleted: new Map([...tracked.deleted].map(([k, v]) => [k, [...v]]))
414
+ };
415
+ const results = [];
416
+ try {
417
+ for (const op of plan.ops) {
418
+ const result = applyOp(op, results);
419
+ results.push(result);
420
+ const failure = guardFailure(op.guard, result.rows.length);
421
+ if (failure) throw failure;
422
+ }
423
+ } catch (err) {
424
+ store.clear();
425
+ for (const [table, rows] of snapshot) store.set(table, rows);
426
+ tracked.inserted = trackedSnapshot.inserted;
427
+ tracked.updated = trackedSnapshot.updated;
428
+ tracked.deleted = trackedSnapshot.deleted;
429
+ throw err;
430
+ }
431
+ return { results };
432
+ }
433
+ function applyOp(op, results) {
434
+ switch (op.op) {
435
+ case "upsert": {
436
+ const values = resolveMap(op.values ?? {}, results, null);
437
+ const conflict = op.onConflict ?? [];
438
+ const rows = rowsOf(op.table);
439
+ const hit = rows.find((r) => conflict.every((c) => r[c] === values[c]));
440
+ if (hit) {
441
+ for (const [key, value] of Object.entries(values)) {
442
+ if (!conflict.includes(key)) hit[key] = value;
443
+ }
444
+ return { rows: [hit], rows_affected: 1 };
445
+ }
446
+ const created = { id: crypto.randomUUID(), ...values };
447
+ rows.push(created);
448
+ track(tracked.inserted, op.table, created);
449
+ return { rows: [created], rows_affected: 1 };
450
+ }
451
+ case "insert": {
452
+ const record = { id: crypto.randomUUID(), ...resolveMap(op.values ?? {}, results, null) };
453
+ rowsOf(op.table).push(record);
454
+ track(tracked.inserted, op.table, record);
455
+ return { rows: [record], rows_affected: 1 };
456
+ }
457
+ case "insertMany": {
458
+ const written = (op.rows ?? []).map((row) => {
459
+ const record = { id: crypto.randomUUID(), ...resolveMap(row, results, null) };
460
+ rowsOf(op.table).push(record);
461
+ track(tracked.inserted, op.table, record);
462
+ return record;
463
+ });
464
+ return { rows: written, rows_affected: written.length };
465
+ }
466
+ case "update": {
467
+ const rows = rowsOf(op.table);
468
+ const where = resolveMap(op.where ?? {}, results, null);
469
+ const written = [];
470
+ for (let i = 0; i < rows.length; i++) {
471
+ const row = rows[i];
472
+ if (!row || !matches(row, where)) continue;
473
+ const next = { ...row, ...resolveMap(op.set ?? {}, results, row) };
474
+ rows[i] = next;
475
+ track(tracked.updated, op.table, next);
476
+ written.push(next);
477
+ }
478
+ return { rows: written, rows_affected: written.length };
479
+ }
480
+ case "delete": {
481
+ const rows = rowsOf(op.table);
482
+ const where = resolveMap(op.where ?? {}, results, null);
483
+ const removed = rows.filter((row) => matches(row, where));
484
+ for (const row of removed) {
485
+ rows.splice(rows.indexOf(row), 1);
486
+ const id = row["id"];
487
+ const list = tracked.deleted.get(op.table);
488
+ const key = typeof id === "string" ? id : String(id);
489
+ if (list) list.push(key);
490
+ else tracked.deleted.set(op.table, [key]);
491
+ }
492
+ return { rows: removed, rows_affected: removed.length };
493
+ }
494
+ case "select": {
495
+ const where = resolveMap(op.where ?? {}, results, null);
496
+ let found = rowsOf(op.table).filter((row) => matches(row, where));
497
+ if (op.limit !== void 0) found = found.slice(0, op.limit);
498
+ return { rows: found, rows_affected: found.length };
499
+ }
500
+ }
501
+ }
502
+ const client = {
503
+ ...ops,
504
+ // No real savepoint in memory: the fake runs the callback against the SAME
505
+ // store. An assertion about rollback here would be asserting the fake.
506
+ attempt: (fn) => fn(ops),
507
+ txPlan,
508
+ // In tests there is no real DB role; `asService()` returns the same
509
+ // in-memory client so RLS-bypass code paths still hit the same store and
510
+ // tracking maps. The omitted `asService` matches the contract (no
511
+ // double-bypass), so callers can't recurse.
512
+ asService() {
513
+ return client;
514
+ },
515
+ inserted(table) {
516
+ return tracked.inserted.get(table) ?? [];
517
+ },
518
+ updated(table) {
519
+ return tracked.updated.get(table) ?? [];
520
+ },
521
+ deleted(table) {
522
+ return tracked.deleted.get(table) ?? [];
523
+ },
524
+ seed(table, data) {
525
+ store.set(table, [...data]);
526
+ }
527
+ };
528
+ return client;
529
+ }
530
+ function cloneTracked(map) {
531
+ return new Map([...map].map(([k, v]) => [k, [...v]]));
532
+ }
533
+ function resolveValue(value, results, current, column) {
534
+ if (typeof value !== "object" || value === null) return value;
535
+ const tagged = value;
536
+ if (tagged.$ref) {
537
+ const row = results[tagged.$ref.op]?.rows[0];
538
+ if (!row) {
539
+ throw txRejection(409, "tx_ref_unresolved", {
540
+ message: `operation ${tagged.$ref.op} produced no row to reference`
541
+ });
542
+ }
543
+ return row[tagged.$ref.field];
544
+ }
545
+ if (tagged.$expr) {
546
+ const fn = tagged.$expr["fn"];
547
+ if (fn === "now") return (/* @__PURE__ */ new Date()).toISOString();
548
+ const by = Number(tagged.$expr["by"]);
549
+ const base = Number(current?.[column] ?? 0);
550
+ return fn === "dec" ? base - by : base + by;
551
+ }
552
+ return value;
553
+ }
554
+ function resolveMap(map, results, current) {
555
+ const out = {};
556
+ for (const [key, value] of Object.entries(map)) {
557
+ out[key] = resolveValue(value, results, current, key);
558
+ }
559
+ return out;
560
+ }
561
+ function matches(row, where) {
562
+ return Object.entries(where).every(
563
+ ([key, value]) => value === null ? row[key] === null || row[key] === void 0 : row[key] === value
564
+ );
565
+ }
566
+ function guardFailure(guard, count) {
567
+ if (!guard) return null;
568
+ const ok = guard.kind === "one" ? count === 1 : guard.kind === "none" ? count === 0 : guard.kind === "atLeast" ? count >= guard.n : count <= guard.n;
569
+ if (ok) return null;
570
+ return txRejection(409, "tx_guard_failed", {
571
+ slot: guard.slot,
572
+ message: `expected ${guard.kind} ${guard.n} row(s), got ${count}`
573
+ });
574
+ }
575
+ function txRejection(status, code, extra) {
576
+ const err = new Error(extra.message);
577
+ err.status = status;
578
+ err.error_code = code;
579
+ if (extra.slot !== void 0) err.slot = extra.slot;
580
+ return err;
581
+ }
582
+
583
+ // src/test/fake-db.ts
584
+ function fakeDatabase() {
585
+ const mock = createMockDB();
586
+ const queries = [];
587
+ const db = Object.assign(Object.create(Object.getPrototypeOf(mock)), mock, {
588
+ query: async (sql, params = []) => {
589
+ queries.push({ sql, params });
590
+ return mock.query(sql, params);
591
+ }
592
+ });
593
+ return {
594
+ db,
595
+ queries,
596
+ seed: (table, rows) => mock.seed(table, rows),
597
+ inserted: (table) => mock.inserted(table),
598
+ updated: (table) => mock.updated(table),
599
+ deleted: (table) => mock.deleted(table)
600
+ };
601
+ }
131
602
  export {
132
603
  TestApiError,
133
604
  api,
134
- createTestApi
605
+ createTestApi,
606
+ fakeDatabase
135
607
  };
136
608
  //# sourceMappingURL=index.js.map