@palbase/backend 33.0.2 → 34.0.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 +2020 -350
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +9 -7
  4. package/dist/bin/palbase-backend.js.map +1 -1
  5. package/dist/{chunk-SI4KGEM3.js → chunk-FOWA7PF4.js} +97 -96
  6. package/dist/chunk-FOWA7PF4.js.map +1 -0
  7. package/dist/chunk-GHNC2UHS.js +20 -0
  8. package/dist/chunk-GHNC2UHS.js.map +1 -0
  9. package/dist/{chunk-AAT5G7KY.js → chunk-JJW3L62T.js} +2 -2
  10. package/dist/chunk-JJW3L62T.js.map +1 -0
  11. package/dist/{chunk-TVCCR6SO.js → chunk-LOGL2ZHD.js} +58 -9
  12. package/dist/chunk-LOGL2ZHD.js.map +1 -0
  13. package/dist/{chunk-C6COAB3E.js → chunk-MNGZE2MM.js} +1651 -186
  14. package/dist/chunk-MNGZE2MM.js.map +1 -0
  15. package/dist/{chunk-XOX6RFPZ.js → chunk-PRH45NZU.js} +46 -3
  16. package/dist/chunk-PRH45NZU.js.map +1 -0
  17. package/dist/{chunk-26GH54D2.js → chunk-RZLY4BOY.js} +5 -3
  18. package/dist/{chunk-26GH54D2.js.map → chunk-RZLY4BOY.js.map} +1 -1
  19. package/dist/{chunk-YIQ4RS4F.js → chunk-SEM36BWY.js} +283 -5
  20. package/dist/chunk-SEM36BWY.js.map +1 -0
  21. package/dist/{chunk-KATPXCJ5.js → chunk-UMESJJRS.js} +19 -40
  22. package/dist/chunk-UMESJJRS.js.map +1 -0
  23. package/dist/chunk-VXPNPVAG.js +40 -0
  24. package/dist/chunk-VXPNPVAG.js.map +1 -0
  25. package/dist/{chunk-WWUG2QXF.js → chunk-YX7UA7VC.js} +53 -4
  26. package/dist/chunk-YX7UA7VC.js.map +1 -0
  27. package/dist/db/env.cjs.map +1 -1
  28. package/dist/db/env.d.cts +2 -0
  29. package/dist/db/env.d.ts +2 -0
  30. package/dist/db/index.cjs +261 -2
  31. package/dist/db/index.cjs.map +1 -1
  32. package/dist/db/index.d.cts +46 -3
  33. package/dist/db/index.d.ts +46 -3
  34. package/dist/db/index.js +14 -5
  35. package/dist/{index-DMZG3kpo.d.ts → endpoint-DpNUx-ON.d.ts} +315 -57
  36. package/dist/{index-DB_nW-AV.d.cts → endpoint-kYgEHrrV.d.cts} +315 -57
  37. package/dist/engine/index.cjs +2026 -348
  38. package/dist/engine/index.cjs.map +1 -1
  39. package/dist/engine/index.d.cts +5 -5
  40. package/dist/engine/index.d.ts +5 -5
  41. package/dist/engine/index.js +17 -7
  42. package/dist/{index-D-3duy8Y.d.ts → index-JrHqNsjQ.d.ts} +108 -50
  43. package/dist/{index-DLveQoOf.d.cts → index-pnDsEfxJ.d.cts} +108 -50
  44. package/dist/index.cjs +616 -254
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +7 -6
  47. package/dist/index.d.ts +7 -6
  48. package/dist/index.js +25 -15
  49. package/dist/index.js.map +1 -1
  50. package/dist/openapi/index.cjs +7 -0
  51. package/dist/openapi/index.cjs.map +1 -1
  52. package/dist/openapi/index.d.cts +3 -3
  53. package/dist/openapi/index.d.ts +3 -3
  54. package/dist/openapi/index.js +4 -3
  55. package/dist/openapi/index.js.map +1 -1
  56. package/dist/{registry-DSTThhKf.d.cts → registry-BcOd8Zhf.d.cts} +3 -1
  57. package/dist/{registry-JjF5lcj4.d.ts → registry-BoX5iNyY.d.ts} +3 -1
  58. package/dist/test/index.cjs +103 -66
  59. package/dist/test/index.cjs.map +1 -1
  60. package/dist/test/index.d.cts +3 -3
  61. package/dist/test/index.d.ts +3 -3
  62. package/dist/test/index.js +33 -7
  63. package/dist/test/index.js.map +1 -1
  64. package/docs/README.md +1 -1
  65. package/docs/database.md +308 -0
  66. package/docs/examples/check-query-contracts.ts +5 -0
  67. package/docs/examples/database-dx.contract.json +65 -0
  68. package/docs/examples/database-dx.ts +63 -0
  69. package/docs/examples/money-transfer.ts +73 -0
  70. package/docs/llms-full.txt +309 -1
  71. package/package.json +1 -2
  72. package/template/package.json +1 -1
  73. package/dist/chunk-AAT5G7KY.js.map +0 -1
  74. package/dist/chunk-C6COAB3E.js.map +0 -1
  75. package/dist/chunk-KATPXCJ5.js.map +0 -1
  76. package/dist/chunk-SI4KGEM3.js.map +0 -1
  77. package/dist/chunk-TVCCR6SO.js.map +0 -1
  78. package/dist/chunk-WWUG2QXF.js.map +0 -1
  79. package/dist/chunk-XOX6RFPZ.js.map +0 -1
  80. package/dist/chunk-YIQ4RS4F.js.map +0 -1
  81. package/stack-images.json +0 -24
package/dist/db/index.cjs CHANGED
@@ -27,11 +27,14 @@ __export(db_exports, {
27
27
  TABLE_META: () => TABLE_META,
28
28
  TxPlanError: () => TxPlanError,
29
29
  TxRefError: () => TxRefError,
30
+ assertQueryManifest: () => assertQueryManifest,
30
31
  bigint: () => bigint,
31
32
  boolean: () => boolean,
32
33
  dec: () => dec,
34
+ defineQuery: () => defineQuery,
33
35
  defineSchema: () => defineSchema,
34
36
  defineTable: () => defineTable,
37
+ defineWorkload: () => defineWorkload,
35
38
  enumType: () => enumType,
36
39
  inc: () => inc,
37
40
  installationRef: () => installationRef,
@@ -43,6 +46,7 @@ __export(db_exports, {
43
46
  openai: () => openai,
44
47
  ownedByUser: () => ownedByUser,
45
48
  policy: () => policy,
49
+ queryManifest: () => queryManifest,
46
50
  raw: () => raw,
47
51
  text: () => text,
48
52
  timestamp: () => timestamp,
@@ -451,6 +455,13 @@ function defineTable(name, input) {
451
455
  }
452
456
  __name(defineTable, "defineTable");
453
457
  var declaringSchemaOf = /* @__PURE__ */ new WeakMap();
458
+ function declaredTableKey(table) {
459
+ const metadata = table[TABLE_META];
460
+ const schema = declaringSchemaOf.get(metadata);
461
+ if (schema === void 0) throw new Error(`defineQuery: table ${metadata.name} must belong to defineSchema before defining its query`);
462
+ return qualifiedTableKey(schema, metadata.name);
463
+ }
464
+ __name(declaredTableKey, "declaredTableKey");
454
465
  function resolveReferences(tables, schemaName) {
455
466
  for (const table of Object.values(tables)) {
456
467
  for (const [colName, builder] of Object.entries(table.columns)) {
@@ -541,6 +552,187 @@ function defineSchema(name, input) {
541
552
  }
542
553
  __name(defineSchema, "defineSchema");
543
554
 
555
+ // src/db/query.ts
556
+ var import_zod = require("zod");
557
+ var import_zod_to_openapi = require("@asteasolutions/zod-to-openapi");
558
+
559
+ // src/db/query-contract.ts
560
+ function queryColumnContract(column) {
561
+ const def = column && typeof column === "object" && "_def" in column ? column._def : column;
562
+ if (!def?.type) throw new Error("Query contract requires declared column types");
563
+ if (def.transform !== void 0 && def.codec === void 0) throw new Error("Query contracts require a named codec instead of a free-form column transform");
564
+ return {
565
+ type: def.type,
566
+ nullable: def.nullable === true,
567
+ ...def.codec === void 0 ? {} : {
568
+ codec: def.codec
569
+ },
570
+ ...def.enumValues === void 0 ? {} : {
571
+ enumValues: [
572
+ ...def.enumValues
573
+ ]
574
+ },
575
+ ...def.dimensions === void 0 ? {} : {
576
+ dimensions: def.dimensions
577
+ }
578
+ };
579
+ }
580
+ __name(queryColumnContract, "queryColumnContract");
581
+ function stableContract(value) {
582
+ if (Array.isArray(value)) return `[${value.map(stableContract).join(",")}]`;
583
+ if (value && typeof value === "object") return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableContract(value[key])}`).join(",")}}`;
584
+ return JSON.stringify(value) ?? "null";
585
+ }
586
+ __name(stableContract, "stableContract");
587
+
588
+ // src/db/query.ts
589
+ (0, import_zod_to_openapi.extendZodWithOpenApi)(import_zod.z);
590
+ function responseColumn(column, validator) {
591
+ const contract = queryColumnContract(column);
592
+ let schema;
593
+ if (validator) return validator;
594
+ if (contract.codec === "number") schema = import_zod.z.number().finite();
595
+ else if (contract.codec === "decimal") schema = import_zod.z.string();
596
+ else switch (contract.type) {
597
+ case "integer":
598
+ schema = import_zod.z.number().int().min(-2147483648).max(2147483647);
599
+ break;
600
+ case "boolean":
601
+ schema = import_zod.z.boolean();
602
+ break;
603
+ case "vector":
604
+ schema = import_zod.z.array(import_zod.z.number().finite());
605
+ break;
606
+ case "enum": {
607
+ const values = contract.enumValues;
608
+ if (!values?.length) throw new Error("Query response requires declared enum values");
609
+ schema = import_zod.z.enum(values);
610
+ break;
611
+ }
612
+ case "uuid":
613
+ case "text":
614
+ case "timestamp":
615
+ case "numeric":
616
+ case "bigint":
617
+ schema = import_zod.z.string();
618
+ break;
619
+ case "jsonb":
620
+ throw new Error("Query response requires a fields validator for a selected jsonb column: its TypeScript payload is erased at runtime");
621
+ default:
622
+ throw new Error(`Query response does not support column type ${contract.type}`);
623
+ }
624
+ return contract.nullable ? schema.nullable() : schema;
625
+ }
626
+ __name(responseColumn, "responseColumn");
627
+ function defineQuery(name, options) {
628
+ if (typeof name !== "string" || !/^[A-Za-z][A-Za-z0-9_]{0,127}$/.test(name)) throw new Error("defineQuery requires a stable name of 1\u2013128 letters, digits or underscores");
629
+ const table = declaredTableKey(options.table);
630
+ const select = [
631
+ ...options.select
632
+ ];
633
+ if (!select.length || new Set(select).size !== select.length) throw new Error(`Query ${name}: select must be explicit, nonempty and distinct`);
634
+ const shape = {}, columns = {};
635
+ for (const key of select) {
636
+ if (!Object.hasOwn(options.table[TABLE_META].columns, key)) throw new Error(`Query ${name}: unknown column ${key}`);
637
+ columns[key] = queryColumnContract(options.table[key]);
638
+ shape[key] = responseColumn(options.table[key], options.fields?.[key]);
639
+ }
640
+ const response = import_zod.z.array(import_zod.z.object(shape).strict());
641
+ const queryInput = options.input, where = options.where, limit = options.limit;
642
+ const orderBy = options.orderBy === void 0 ? void 0 : JSON.parse(JSON.stringify(options.orderBy));
643
+ const prepare = /* @__PURE__ */ __name((input) => {
644
+ const parsed = queryInput.parse(input);
645
+ const filter = where === void 0 ? {} : where(parsed);
646
+ if (!filter || typeof filter !== "object" || Array.isArray(filter) || typeof filter.then === "function") {
647
+ throw new Error(`Query ${name}: where must return a synchronous filter object`);
648
+ }
649
+ const size = typeof limit === "function" ? limit(parsed) : limit;
650
+ if (limit !== void 0 && (typeof size !== "number" || !Number.isSafeInteger(size) || size < 0)) throw new Error(`Query ${name}: limit must produce a nonnegative safe integer`);
651
+ return {
652
+ name,
653
+ table,
654
+ columns: JSON.parse(JSON.stringify(columns)),
655
+ where: filter,
656
+ options: {
657
+ select: [
658
+ ...select
659
+ ],
660
+ ...orderBy === void 0 ? {} : {
661
+ orderBy: structuredClone(orderBy)
662
+ },
663
+ ...limit === void 0 ? {} : {
664
+ limit: size
665
+ }
666
+ }
667
+ };
668
+ }, "prepare");
669
+ const attach = /* @__PURE__ */ __name((schema) => {
670
+ const api = {
671
+ queryName: name,
672
+ input: queryInput,
673
+ prepare,
674
+ async run(database, input) {
675
+ const request = prepare(input);
676
+ const rows = await ("$readQuery" in database ? database.$readQuery(request) : database.readQuery(request));
677
+ return schema.parse(rows);
678
+ },
679
+ sql(database, input) {
680
+ const request = prepare(input);
681
+ return "$compileQuery" in database ? database.$compileQuery(request) : database.compileQuery(request);
682
+ },
683
+ explain(database, input) {
684
+ const request = prepare(input);
685
+ return "$explainQuery" in database ? database.$explainQuery(request) : database.explainQuery(request);
686
+ },
687
+ contract() {
688
+ const registry = new import_zod_to_openapi.OpenAPIRegistry();
689
+ registry.register(`${name}Input`, queryInput);
690
+ registry.register(`${name}Response`, schema);
691
+ return JSON.parse(JSON.stringify({
692
+ name,
693
+ table,
694
+ select,
695
+ columns,
696
+ components: new import_zod_to_openapi.OpenApiGeneratorV31(registry.definitions).generateComponents().components
697
+ }));
698
+ }
699
+ };
700
+ for (const key of [
701
+ "min",
702
+ "max",
703
+ "length",
704
+ "describe",
705
+ "openapi"
706
+ ]) {
707
+ const modify = schema[key].bind(schema);
708
+ Object.defineProperty(schema, key, {
709
+ value: /* @__PURE__ */ __name((...args) => attach(modify(...args)), "value"),
710
+ configurable: true
711
+ });
712
+ }
713
+ return Object.assign(schema, api);
714
+ }, "attach");
715
+ return attach(response);
716
+ }
717
+ __name(defineQuery, "defineQuery");
718
+ function queryManifest(queries) {
719
+ const sorted = [
720
+ ...queries
721
+ ].sort((a, b) => a.queryName.localeCompare(b.queryName));
722
+ if (new Set(sorted.map((query) => query.queryName)).size !== sorted.length) throw new Error("Query manifest contains duplicate names");
723
+ return JSON.parse(stableContract({
724
+ version: 1,
725
+ queries: sorted.map((query) => query.contract())
726
+ }));
727
+ }
728
+ __name(queryManifest, "queryManifest");
729
+ function assertQueryManifest(queries, baseline) {
730
+ if (stableContract(queryManifest(queries)) !== stableContract(baseline)) {
731
+ throw new Error("Query contract drift: compare queryManifest() with the committed manifest and review the API/client changes before updating it");
732
+ }
733
+ }
734
+ __name(assertQueryManifest, "assertQueryManifest");
735
+
544
736
  // src/db/extensions.ts
545
737
  var PALBASE_EXTENSIONS = [
546
738
  // Search & text
@@ -1506,6 +1698,13 @@ function translateRejection(err, builder) {
1506
1698
  }
1507
1699
  __name(translateRejection, "translateRejection");
1508
1700
 
1701
+ // src/db/sqlstate.ts
1702
+ var SQLSTATE = Object.freeze({
1703
+ serializationFailure: "40001",
1704
+ deadlockDetected: "40P01",
1705
+ commandGuardFailed: "PBC01"
1706
+ });
1707
+
1509
1708
  // src/errors.ts
1510
1709
  var HTTP_ERROR_BRAND = /* @__PURE__ */ Symbol.for("palbase.backend.httpError");
1511
1710
  var HttpError = class extends Error {
@@ -1550,6 +1749,39 @@ var HttpError = class extends Error {
1550
1749
  }
1551
1750
  };
1552
1751
 
1752
+ // src/db/workload.ts
1753
+ function defineWorkload(name, options) {
1754
+ const result = {
1755
+ name,
1756
+ concurrency: options.concurrency,
1757
+ queueLimit: options.queueLimit,
1758
+ tenantConcurrency: options.tenantConcurrency ?? options.concurrency,
1759
+ tenantQueueLimit: options.tenantQueueLimit ?? options.queueLimit,
1760
+ queueTimeoutMs: options.queueTimeoutMs ?? 1e3,
1761
+ tenantClaim: options.tenantClaim ?? "tenant_id"
1762
+ };
1763
+ validateWorkload(result);
1764
+ return Object.freeze(result);
1765
+ }
1766
+ __name(defineWorkload, "defineWorkload");
1767
+ function validateWorkload(value) {
1768
+ if (typeof value.name !== "string" || !/^[a-zA-Z][a-zA-Z0-9_.-]{0,63}$/.test(value.name)) throw new Error("workload name must be 1\u201364 letters/digits/dots/dashes");
1769
+ for (const name of [
1770
+ "concurrency",
1771
+ "tenantConcurrency",
1772
+ "queueLimit",
1773
+ "tenantQueueLimit",
1774
+ "queueTimeoutMs"
1775
+ ]) {
1776
+ const n = value[name], min = name.endsWith("Limit") ? 0 : 1;
1777
+ const max = name === "queueTimeoutMs" ? 6e4 : name.endsWith("Limit") ? 1e4 : 1e3;
1778
+ if (!Number.isInteger(n) || n < min || n > max) throw new Error(`workload ${name} must be an integer between ${min} and ${max}`);
1779
+ }
1780
+ if (value.tenantConcurrency > value.concurrency || value.tenantQueueLimit > value.queueLimit) throw new Error("workload tenant limits cannot exceed global limits");
1781
+ if (typeof value.tenantClaim !== "string" || !/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(value.tenantClaim)) throw new Error("workload tenantClaim must be a top-level claim name");
1782
+ }
1783
+ __name(validateWorkload, "validateWorkload");
1784
+
1553
1785
  // src/db/typed-db.ts
1554
1786
  function makeTypedTable(name, raw2) {
1555
1787
  return {
@@ -1561,7 +1793,14 @@ function makeTypedTable(name, raw2) {
1561
1793
  }), "put"),
1562
1794
  update: /* @__PURE__ */ __name((q) => raw2.update(name, q.where.id, q.set), "update"),
1563
1795
  delete: /* @__PURE__ */ __name((id) => raw2.delete(name, id), "delete"),
1796
+ findUnique: /* @__PURE__ */ __name((q) => raw2.findUnique(name, q.where, {
1797
+ select: q.select
1798
+ }), "findUnique"),
1564
1799
  findById: /* @__PURE__ */ __name((id) => raw2.findById(name, id), "findById"),
1800
+ page: /* @__PURE__ */ __name((q) => {
1801
+ const { where, ...opts } = q ?? {};
1802
+ return raw2.page(name, where, opts);
1803
+ }, "page"),
1565
1804
  findMany: /* @__PURE__ */ __name((q) => {
1566
1805
  const { where, ...opts } = q ?? {};
1567
1806
  return raw2.findMany(name, where, opts);
@@ -1579,17 +1818,33 @@ function makeTypedDB(schema, raw2) {
1579
1818
  for (const key of Object.keys(schema.tables)) {
1580
1819
  const tableDef = schema.tables[key];
1581
1820
  if (tableDef !== void 0) {
1582
- tables[key] = makeTypedTable(tableDef.name, raw2);
1821
+ tables[key] = makeTypedTable(qualifiedTableKey(schema.name, tableDef.name), raw2);
1583
1822
  }
1584
1823
  }
1585
1824
  const result = {
1586
1825
  tables,
1826
+ diagnostics: /* @__PURE__ */ __name(() => raw2.diagnostics(), "diagnostics"),
1827
+ readQuery: /* @__PURE__ */ __name((query) => raw2.readQuery(query), "readQuery"),
1828
+ compileQuery: /* @__PURE__ */ __name((query) => raw2.compileQuery(query), "compileQuery"),
1829
+ explainQuery: /* @__PURE__ */ __name((query) => raw2.explainQuery(query), "explainQuery"),
1830
+ command(fn, options) {
1831
+ const builder = new TxPlanBuilder();
1832
+ const planTables = Object.fromEntries(Object.entries(schema.tables).map(([key, table]) => [
1833
+ key,
1834
+ builder.table(qualifiedTableKey(schema.name, table.name))
1835
+ ]));
1836
+ return runTxPlan({
1837
+ txPlan: /* @__PURE__ */ __name((plan) => raw2.command(plan, options), "txPlan")
1838
+ }, {
1839
+ tables: planTables
1840
+ }, builder, fn);
1841
+ },
1587
1842
  transaction(fn) {
1588
1843
  const builder = new TxPlanBuilder();
1589
1844
  const planTables = {};
1590
1845
  for (const key of Object.keys(schema.tables)) {
1591
1846
  const tableDef = schema.tables[key];
1592
- if (tableDef !== void 0) planTables[key] = builder.table(tableDef.name);
1847
+ if (tableDef !== void 0) planTables[key] = builder.table(qualifiedTableKey(schema.name, tableDef.name));
1593
1848
  }
1594
1849
  return runTxPlan(raw2, {
1595
1850
  tables: planTables
@@ -1607,11 +1862,14 @@ __name(makeTypedDB, "makeTypedDB");
1607
1862
  TABLE_META,
1608
1863
  TxPlanError,
1609
1864
  TxRefError,
1865
+ assertQueryManifest,
1610
1866
  bigint,
1611
1867
  boolean,
1612
1868
  dec,
1869
+ defineQuery,
1613
1870
  defineSchema,
1614
1871
  defineTable,
1872
+ defineWorkload,
1615
1873
  enumType,
1616
1874
  inc,
1617
1875
  installationRef,
@@ -1623,6 +1881,7 @@ __name(makeTypedDB, "makeTypedDB");
1623
1881
  openai,
1624
1882
  ownedByUser,
1625
1883
  policy,
1884
+ queryManifest,
1626
1885
  raw,
1627
1886
  text,
1628
1887
  timestamp,