@palbase/backend 33.0.2 → 34.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 (84) hide show
  1. package/dist/bin/palbase-backend.cjs +2273 -358
  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-YIQ4RS4F.js → chunk-5UGPXKMJ.js} +331 -5
  6. package/dist/chunk-5UGPXKMJ.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-SI4KGEM3.js → chunk-P7MGSAFT.js} +102 -96
  12. package/dist/chunk-P7MGSAFT.js.map +1 -0
  13. package/dist/{chunk-XOX6RFPZ.js → chunk-PRH45NZU.js} +46 -3
  14. package/dist/chunk-PRH45NZU.js.map +1 -0
  15. package/dist/{chunk-26GH54D2.js → chunk-RZLY4BOY.js} +5 -3
  16. package/dist/{chunk-26GH54D2.js.map → chunk-RZLY4BOY.js.map} +1 -1
  17. package/dist/{chunk-KATPXCJ5.js → chunk-UMESJJRS.js} +19 -40
  18. package/dist/chunk-UMESJJRS.js.map +1 -0
  19. package/dist/chunk-VXPNPVAG.js +40 -0
  20. package/dist/chunk-VXPNPVAG.js.map +1 -0
  21. package/dist/{chunk-WWUG2QXF.js → chunk-X2UYIXDS.js} +59 -4
  22. package/dist/chunk-X2UYIXDS.js.map +1 -0
  23. package/dist/{chunk-C6COAB3E.js → chunk-XZPDPYVY.js} +1905 -195
  24. package/dist/chunk-XZPDPYVY.js.map +1 -0
  25. package/dist/{chunk-TVCCR6SO.js → chunk-YO5GYM73.js} +58 -9
  26. package/dist/chunk-YO5GYM73.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 +285 -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-BXPdsTA5.d.ts} +370 -58
  36. package/dist/{index-DB_nW-AV.d.cts → endpoint-DqhUP7RT.d.cts} +370 -58
  37. package/dist/engine/index.cjs +2279 -356
  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-DLveQoOf.d.cts → index-Co0Hj_aO.d.cts} +108 -50
  43. package/dist/{index-D-3duy8Y.d.ts → index-MhCL7ll4.d.ts} +108 -50
  44. package/dist/index.cjs +806 -386
  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 +27 -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-BkIVHPzB.d.cts} +3 -1
  57. package/dist/{registry-JjF5lcj4.d.ts → registry-CvGLR5j1.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/auth.md +58 -0
  66. package/docs/database.md +308 -0
  67. package/docs/examples/check-query-contracts.ts +5 -0
  68. package/docs/examples/database-dx.contract.json +65 -0
  69. package/docs/examples/database-dx.ts +63 -0
  70. package/docs/examples/money-transfer.ts +73 -0
  71. package/docs/llms-full.txt +382 -1
  72. package/docs/schema.md +15 -0
  73. package/package.json +1 -2
  74. package/template/db/public.ts +12 -0
  75. package/template/package.json +1 -1
  76. package/dist/chunk-AAT5G7KY.js.map +0 -1
  77. package/dist/chunk-C6COAB3E.js.map +0 -1
  78. package/dist/chunk-KATPXCJ5.js.map +0 -1
  79. package/dist/chunk-SI4KGEM3.js.map +0 -1
  80. package/dist/chunk-TVCCR6SO.js.map +0 -1
  81. package/dist/chunk-WWUG2QXF.js.map +0 -1
  82. package/dist/chunk-XOX6RFPZ.js.map +0 -1
  83. package/dist/chunk-YIQ4RS4F.js.map +0 -1
  84. package/stack-images.json +0 -24
@@ -3,17 +3,19 @@ import {
3
3
  BootRefused,
4
4
  createApp,
5
5
  loadConfig
6
- } from "../chunk-C6COAB3E.js";
7
- import "../chunk-WWUG2QXF.js";
8
- import "../chunk-TVCCR6SO.js";
9
- import "../chunk-XOX6RFPZ.js";
10
- import "../chunk-SI4KGEM3.js";
6
+ } from "../chunk-XZPDPYVY.js";
7
+ import "../chunk-X2UYIXDS.js";
8
+ import "../chunk-GHNC2UHS.js";
9
+ import "../chunk-YO5GYM73.js";
10
+ import "../chunk-PRH45NZU.js";
11
+ import "../chunk-P7MGSAFT.js";
11
12
  import {
12
13
  getRegisteredControllers
13
- } from "../chunk-AAT5G7KY.js";
14
+ } from "../chunk-JJW3L62T.js";
15
+ import "../chunk-UMESJJRS.js";
14
16
  import {
15
17
  __name
16
- } from "../chunk-KATPXCJ5.js";
18
+ } from "../chunk-VXPNPVAG.js";
17
19
 
18
20
  // src/bin/palbase-backend.ts
19
21
  import { readdir } from "fs/promises";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bin/palbase-backend.ts"],"sourcesContent":["#!/usr/bin/env bun\n/**\n * palbase-backend — run this project's backend.\n *\n * npm run dev → palbase-backend dev (reloads on change)\n * npm start → palbase-backend serve\n *\n * There is no scaffolding step and no server file to write. The project IS the\n * backend: every `controllers/*.controller.ts` is imported, which is what\n * registers it, and the engine is built around whatever registered.\n *\n * It is the same engine the deployed runtime builds — not a development\n * stand-in. A dev server that is a different program from the production one is\n * a source of \"works locally\" reports, and this one has no second implementation\n * to disagree with.\n */\nimport { readdir } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nimport { createApp, loadConfig, BootRefused } from \"../engine/index.js\";\nimport { getRegisteredControllers } from \"../decorators/controller.js\";\n\nconst USAGE = `palbase-backend — run this project's backend\n\n palbase-backend serve start the backend\n palbase-backend dev start it and reload on change\n palbase-backend routes print the route table and exit\n\nConfiguration comes from the environment (a .env beside package.json is read):\n\n DATABASE_URL required — the stack's Postgres\n AUTH_JWKS_URL required — where your stack publishes its signing keys\n MODULE_BASE_URL where Documents/Storage/Notifications/Flags/Realtime live\n PALBASE_ANON_KEY publishable key, sent on module calls\n PALBASE_SERVICE_ROLE_KEY secret key, for privileged module calls\n PORT default 3000\n`;\n\nfunction die(message: string): never {\n console.error(message);\n process.exit(1);\n}\n\n/** Read a `.env` beside the project, without adding a dependency for it. */\nasync function loadDotEnv(root: string): Promise<void> {\n const file = Bun.file(join(root, \".env\"));\n if (!(await file.exists())) return;\n for (const raw of (await file.text()).split(\"\\n\")) {\n const line = raw.trim();\n if (!line || line.startsWith(\"#\")) continue;\n const eq = line.indexOf(\"=\");\n if (eq < 1) continue;\n const key = line.slice(0, eq).trim();\n // The environment wins: an exported value is the operator being explicit,\n // and a file quietly overriding it is how a \"why is it still pointing at\n // the old database\" hour begins.\n if (process.env[key] !== undefined) continue;\n let value = line.slice(eq + 1).trim();\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n process.env[key] = value;\n }\n}\n\n/** Every `*.controller.ts` under `controllers/`, sorted, recursively. */\n/** Directories that are never a project's source. */\nconst NEVER_WALK = new Set([\"node_modules\", \"dist\", \"build\", \".git\"]);\n\n/**\n * The project's MODULE files — the same one glob the bundlers use.\n *\n * Importing a module imports the classes it owns, so this reaches every\n * controller, service, job, hook, webhook and room the project declares, and\n * nothing it does not. It used to walk `controllers/` for `*.controller.ts`,\n * which made the directory a declaration and left `dev` seeing a different set\n * of classes than a deploy would.\n *\n * Recursive: a module lives beside the domain it owns, not in a folder this\n * file names.\n */\nasync function findModules(root: string): Promise<string[]> {\n const out: string[] = [];\n const walk = async (d: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(d, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (NEVER_WALK.has(entry.name) || entry.name.startsWith(\".palbase\")) continue;\n const full = join(d, entry.name);\n if (entry.isDirectory()) await walk(full);\n else if (/\\.module\\.(ts|js|mts|mjs)$/.test(entry.name)) out.push(full);\n }\n };\n await walk(root);\n // Sorted so declaration order is the same on every machine.\n return out.sort();\n}\n\nasync function importSchemas(root: string): Promise<unknown[]> {\n // ONE SCHEMA PER FILE, so the whole directory is the declaration. Reading a\n // single well-known file would take the first one and leave the rest of the\n // project's tables with no typed surface and no complaint.\n //\n // SORTED, because readdir order is not specified: an unsorted set would hand\n // the runtime a different answer from one boot to the next.\n let entries: string[];\n try {\n entries = (await readdir(join(root, \"db\"))).sort();\n } catch {\n // Not every project declares a database; the typed `.tables` surface is\n // simply absent then.\n return [];\n }\n const out: unknown[] = [];\n for (const entry of entries) {\n if (!entry.endsWith(\".ts\") && !entry.endsWith(\".js\")) continue;\n if (entry.endsWith(\".d.ts\")) continue;\n out.push((await import(pathToFileURL(join(root, \"db\", entry)).href)).default);\n }\n return out;\n}\n\nasync function main(): Promise<void> {\n const command = process.argv[2] ?? \"serve\";\n if (command === \"--help\" || command === \"-h\" || command === \"help\") {\n console.log(USAGE);\n return;\n }\n if (![\"serve\", \"dev\", \"routes\"].includes(command)) {\n die(`palbase-backend: unknown command \"${command}\".\\n\\n${USAGE}`);\n }\n if (typeof Bun === \"undefined\") {\n die(\n \"palbase-backend needs Bun: the engine serves `fetch` natively and opens its own\\n\" +\n \"Postgres pool through Bun.sql. Install it from https://bun.sh, then re-run.\",\n );\n }\n\n // `dev` is `serve` under Bun's watcher. Re-exec rather than reimplement:\n // one code path serves, and the reload is the runtime's job, not ours.\n if (command === \"dev\" && !process.env.PALBASE_WATCHING) {\n const self = Bun.fileURLToPath(import.meta.url);\n const child = Bun.spawn([\"bun\", \"--watch\", self, \"serve\"], {\n stdio: [\"inherit\", \"inherit\", \"inherit\"],\n env: { ...process.env, PALBASE_WATCHING: \"1\" },\n });\n process.exit(await child.exited);\n }\n\n const root = resolve(process.env.PALBASE_PROJECT_DIR ?? process.cwd());\n await loadDotEnv(root);\n\n const files = await findModules(root);\n if (files.length === 0) {\n die(\n `palbase-backend: no *.module.ts under ${root}.\\n` +\n `A module is what says which classes exist, who owns them and what they may\\n` +\n `reach — and it is the only place that says it. Add one and run again:\\n\\n` +\n ` // todos.module.ts\\n` +\n ` import { Module, type Token } from \"@palbase/backend\";\\n` +\n ` import { TodosController } from \"./controllers/todos.controller.ts\";\\n` +\n ` @Module({ controllers: [TodosController as Token] })\\n` +\n ` export class TodosModule {}\\n`,\n );\n }\n\n // Importing IS the registration — for the module AND for everything it owns,\n // because a module imports the classes it lists.\n for (const file of files) await import(pathToFileURL(file).href);\n const controllers = getRegisteredControllers();\n if (controllers.length === 0) {\n die(\n `palbase-backend: ${files.length} module file(s) loaded but no controller registered.\\n` +\n `Either none of the listed classes carries @Controller, or the files were\\n` +\n `compiled with decorators stripped. Nothing would answer, so this is fatal.`,\n );\n }\n\n let config;\n try {\n config = loadConfig(process.env as Record<string, string | undefined>);\n } catch (e) {\n if (e instanceof BootRefused) {\n die(\n `${e.message}\\n\\n` +\n `Put them in a .env beside package.json, or export them. A local stack\\n` +\n `(docker compose up) publishes both.`,\n );\n }\n throw e;\n }\n\n const app = await createApp({ config, controllers, schemas: await importSchemas(root) });\n\n if (command === \"routes\") {\n for (const route of app.routes) console.log(route.id);\n await app.shutdown();\n return;\n }\n\n Bun.serve({ port: config.port, idleTimeout: 60, fetch: app.handle });\n console.log(`palbase-backend listening on http://localhost:${config.port}`);\n console.log(` ${app.routes.length} endpoint(s) from ${files.length} module file(s)`);\n for (const route of app.routes) console.log(` ${route.id}`);\n\n for (const signal of [\"SIGTERM\", \"SIGINT\"] as const) {\n process.on(signal, () => {\n void app.shutdown().finally(() => process.exit(0));\n });\n }\n}\n\n// Not a top-level await: this file is also emitted in a CommonJS flavour, where\n// one is a build error. The rejection handler is the point either way — an\n// unhandled one exits 0 on some runtimes, which would report a dead backend as\n// a successful start.\nmain().catch((e: unknown) => {\n console.error(e instanceof Error ? e.message : e);\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgBA,SAASA,eAAe;AACxB,SAASC,MAAMC,eAAe;AAC9B,SAASC,qBAAqB;AAK9B,IAAMC,QAAQ;;;;;;;;;;;;;;;AAgBd,SAASC,IAAIC,SAAe;AAC1BC,UAAQC,MAAMF,OAAAA;AACdG,UAAQC,KAAK,CAAA;AACf;AAHSL;AAMT,eAAeM,WAAWC,MAAY;AACpC,QAAMC,OAAOC,IAAID,KAAKE,KAAKH,MAAM,MAAA,CAAA;AACjC,MAAI,CAAE,MAAMC,KAAKG,OAAM,EAAK;AAC5B,aAAWC,QAAQ,MAAMJ,KAAKK,KAAI,GAAIC,MAAM,IAAA,GAAO;AACjD,UAAMC,OAAOH,IAAII,KAAI;AACrB,QAAI,CAACD,QAAQA,KAAKE,WAAW,GAAA,EAAM;AACnC,UAAMC,KAAKH,KAAKI,QAAQ,GAAA;AACxB,QAAID,KAAK,EAAG;AACZ,UAAME,MAAML,KAAKM,MAAM,GAAGH,EAAAA,EAAIF,KAAI;AAIlC,QAAIZ,QAAQkB,IAAIF,GAAAA,MAASG,OAAW;AACpC,QAAIC,QAAQT,KAAKM,MAAMH,KAAK,CAAA,EAAGF,KAAI;AACnC,QACGQ,MAAMP,WAAW,GAAA,KAAQO,MAAMC,SAAS,GAAA,KACxCD,MAAMP,WAAW,GAAA,KAAQO,MAAMC,SAAS,GAAA,GACzC;AACAD,cAAQA,MAAMH,MAAM,GAAG,EAAC;IAC1B;AACAjB,YAAQkB,IAAIF,GAAAA,IAAOI;EACrB;AACF;AAtBelB;AA0Bf,IAAMoB,aAAa,oBAAIC,IAAI;EAAC;EAAgB;EAAQ;EAAS;CAAO;AAcpE,eAAeC,YAAYrB,MAAY;AACrC,QAAMsB,MAAgB,CAAA;AACtB,QAAMC,OAAO,8BAAOC,MAAAA;AAClB,QAAIC;AACJ,QAAI;AACFA,gBAAU,MAAMC,QAAQF,GAAG;QAAEG,eAAe;MAAK,CAAA;IACnD,QAAQ;AACN;IACF;AACA,eAAWC,SAASH,SAAS;AAC3B,UAAIN,WAAWU,IAAID,MAAME,IAAI,KAAKF,MAAME,KAAKpB,WAAW,UAAA,EAAa;AACrE,YAAMqB,OAAO5B,KAAKqB,GAAGI,MAAME,IAAI;AAC/B,UAAIF,MAAMI,YAAW,EAAI,OAAMT,KAAKQ,IAAAA;eAC3B,6BAA6BE,KAAKL,MAAME,IAAI,EAAGR,KAAIY,KAAKH,IAAAA;IACnE;EACF,GAba;AAcb,QAAMR,KAAKvB,IAAAA;AAEX,SAAOsB,IAAIa,KAAI;AACjB;AAnBed;AAqBf,eAAee,cAAcpC,MAAY;AAOvC,MAAIyB;AACJ,MAAI;AACFA,eAAW,MAAMC,QAAQvB,KAAKH,MAAM,IAAA,CAAA,GAAQmC,KAAI;EAClD,QAAQ;AAGN,WAAO,CAAA;EACT;AACA,QAAMb,MAAiB,CAAA;AACvB,aAAWM,SAASH,SAAS;AAC3B,QAAI,CAACG,MAAMV,SAAS,KAAA,KAAU,CAACU,MAAMV,SAAS,KAAA,EAAQ;AACtD,QAAIU,MAAMV,SAAS,OAAA,EAAU;AAC7BI,QAAIY,MAAM,MAAM,OAAOG,cAAclC,KAAKH,MAAM,MAAM4B,KAAAA,CAAAA,EAAQU,OAAOC,OAAO;EAC9E;AACA,SAAOjB;AACT;AAtBec;AAwBf,eAAeI,OAAAA;AACb,QAAMC,UAAU5C,QAAQ6C,KAAK,CAAA,KAAM;AACnC,MAAID,YAAY,YAAYA,YAAY,QAAQA,YAAY,QAAQ;AAClE9C,YAAQgD,IAAInD,KAAAA;AACZ;EACF;AACA,MAAI,CAAC;IAAC;IAAS;IAAO;IAAUoD,SAASH,OAAAA,GAAU;AACjDhD,QAAI,qCAAqCgD,OAAAA;;EAAgBjD,KAAAA,EAAO;EAClE;AACA,MAAI,OAAOU,QAAQ,aAAa;AAC9BT,QACE,8JACE;EAEN;AAIA,MAAIgD,YAAY,SAAS,CAAC5C,QAAQkB,IAAI8B,kBAAkB;AACtD,UAAMC,OAAO5C,IAAI6C,cAAc,YAAYC,GAAG;AAC9C,UAAMC,QAAQ/C,IAAIgD,MAAM;MAAC;MAAO;MAAWJ;MAAM;OAAU;MACzDK,OAAO;QAAC;QAAW;QAAW;;MAC9BpC,KAAK;QAAE,GAAGlB,QAAQkB;QAAK8B,kBAAkB;MAAI;IAC/C,CAAA;AACAhD,YAAQC,KAAK,MAAMmD,MAAMG,MAAM;EACjC;AAEA,QAAMpD,OAAOqD,QAAQxD,QAAQkB,IAAIuC,uBAAuBzD,QAAQ0D,IAAG,CAAA;AACnE,QAAMxD,WAAWC,IAAAA;AAEjB,QAAMwD,QAAQ,MAAMnC,YAAYrB,IAAAA;AAChC,MAAIwD,MAAMC,WAAW,GAAG;AACtBhE,QACE,yCAAyCO,IAAAA;;;;;;;;;CAON;EAEvC;AAIA,aAAWC,QAAQuD,MAAO,OAAM,OAAOnB,cAAcpC,IAAAA,EAAMqC;AAC3D,QAAMoB,cAAcC,yBAAAA;AACpB,MAAID,YAAYD,WAAW,GAAG;AAC5BhE,QACE,oBAAoB+D,MAAMC,MAAM;;2EAE8C;EAElF;AAEA,MAAIG;AACJ,MAAI;AACFA,aAASC,WAAWhE,QAAQkB,GAAG;EACjC,SAAS+C,GAAG;AACV,QAAIA,aAAaC,aAAa;AAC5BtE,UACE,GAAGqE,EAAEpE,OAAO;;;oCAE2B;IAE3C;AACA,UAAMoE;EACR;AAEA,QAAME,MAAM,MAAMC,UAAU;IAAEL;IAAQF;IAAaQ,SAAS,MAAM9B,cAAcpC,IAAAA;EAAM,CAAA;AAEtF,MAAIyC,YAAY,UAAU;AACxB,eAAW0B,SAASH,IAAII,OAAQzE,SAAQgD,IAAIwB,MAAME,EAAE;AACpD,UAAML,IAAIM,SAAQ;AAClB;EACF;AAEApE,MAAIqE,MAAM;IAAEC,MAAMZ,OAAOY;IAAMC,aAAa;IAAIC,OAAOV,IAAIW;EAAO,CAAA;AAClEhF,UAAQgD,IAAI,iDAAiDiB,OAAOY,IAAI,EAAE;AAC1E7E,UAAQgD,IAAI,KAAKqB,IAAII,OAAOX,MAAM,qBAAqBD,MAAMC,MAAM,iBAAiB;AACpF,aAAWU,SAASH,IAAII,OAAQzE,SAAQgD,IAAI,KAAKwB,MAAME,EAAE,EAAE;AAE3D,aAAWO,UAAU;IAAC;IAAW;KAAoB;AACnD/E,YAAQgF,GAAGD,QAAQ,MAAA;AACjB,WAAKZ,IAAIM,SAAQ,EAAGQ,QAAQ,MAAMjF,QAAQC,KAAK,CAAA,CAAA;IACjD,CAAA;EACF;AACF;AAxFe0C;AA8FfA,KAAAA,EAAOuC,MAAM,CAACjB,MAAAA;AACZnE,UAAQC,MAAMkE,aAAakB,QAAQlB,EAAEpE,UAAUoE,CAAAA;AAC/CjE,UAAQC,KAAK,CAAA;AACf,CAAA;","names":["readdir","join","resolve","pathToFileURL","USAGE","die","message","console","error","process","exit","loadDotEnv","root","file","Bun","join","exists","raw","text","split","line","trim","startsWith","eq","indexOf","key","slice","env","undefined","value","endsWith","NEVER_WALK","Set","findModules","out","walk","d","entries","readdir","withFileTypes","entry","has","name","full","isDirectory","test","push","sort","importSchemas","pathToFileURL","href","default","main","command","argv","log","includes","PALBASE_WATCHING","self","fileURLToPath","url","child","spawn","stdio","exited","resolve","PALBASE_PROJECT_DIR","cwd","files","length","controllers","getRegisteredControllers","config","loadConfig","e","BootRefused","app","createApp","schemas","route","routes","id","shutdown","serve","port","idleTimeout","fetch","handle","signal","on","finally","catch","Error"]}
1
+ {"version":3,"sources":["../../src/bin/palbase-backend.ts"],"sourcesContent":["#!/usr/bin/env bun\n/**\n * palbase-backend — run this project's backend.\n *\n * npm run dev → palbase-backend dev (reloads on change)\n * npm start → palbase-backend serve\n *\n * There is no scaffolding step and no server file to write. The project IS the\n * backend: every `controllers/*.controller.ts` is imported, which is what\n * registers it, and the engine is built around whatever registered.\n *\n * It is the same engine the deployed runtime builds — not a development\n * stand-in. A dev server that is a different program from the production one is\n * a source of \"works locally\" reports, and this one has no second implementation\n * to disagree with.\n */\nimport { readdir } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nimport { createApp, loadConfig, BootRefused } from \"../engine/index.js\";\nimport { getRegisteredControllers } from \"../decorators/controller.js\";\n\nconst USAGE = `palbase-backend — run this project's backend\n\n palbase-backend serve start the backend\n palbase-backend dev start it and reload on change\n palbase-backend routes print the route table and exit\n\nConfiguration comes from the environment (a .env beside package.json is read):\n\n DATABASE_URL required — the stack's Postgres\n AUTH_JWKS_URL required — where your stack publishes its signing keys\n MODULE_BASE_URL where Documents/Storage/Notifications/Flags/Realtime live\n PALBASE_ANON_KEY publishable key, sent on module calls\n PALBASE_SERVICE_ROLE_KEY secret key, for privileged module calls\n PORT default 3000\n`;\n\nfunction die(message: string): never {\n console.error(message);\n process.exit(1);\n}\n\n/** Read a `.env` beside the project, without adding a dependency for it. */\nasync function loadDotEnv(root: string): Promise<void> {\n const file = Bun.file(join(root, \".env\"));\n if (!(await file.exists())) return;\n for (const raw of (await file.text()).split(\"\\n\")) {\n const line = raw.trim();\n if (!line || line.startsWith(\"#\")) continue;\n const eq = line.indexOf(\"=\");\n if (eq < 1) continue;\n const key = line.slice(0, eq).trim();\n // The environment wins: an exported value is the operator being explicit,\n // and a file quietly overriding it is how a \"why is it still pointing at\n // the old database\" hour begins.\n if (process.env[key] !== undefined) continue;\n let value = line.slice(eq + 1).trim();\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n process.env[key] = value;\n }\n}\n\n/** Every `*.controller.ts` under `controllers/`, sorted, recursively. */\n/** Directories that are never a project's source. */\nconst NEVER_WALK = new Set([\"node_modules\", \"dist\", \"build\", \".git\"]);\n\n/**\n * The project's MODULE files — the same one glob the bundlers use.\n *\n * Importing a module imports the classes it owns, so this reaches every\n * controller, service, job, hook, webhook and room the project declares, and\n * nothing it does not. It used to walk `controllers/` for `*.controller.ts`,\n * which made the directory a declaration and left `dev` seeing a different set\n * of classes than a deploy would.\n *\n * Recursive: a module lives beside the domain it owns, not in a folder this\n * file names.\n */\nasync function findModules(root: string): Promise<string[]> {\n const out: string[] = [];\n const walk = async (d: string): Promise<void> => {\n let entries;\n try {\n entries = await readdir(d, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (NEVER_WALK.has(entry.name) || entry.name.startsWith(\".palbase\")) continue;\n const full = join(d, entry.name);\n if (entry.isDirectory()) await walk(full);\n else if (/\\.module\\.(ts|js|mts|mjs)$/.test(entry.name)) out.push(full);\n }\n };\n await walk(root);\n // Sorted so declaration order is the same on every machine.\n return out.sort();\n}\n\nasync function importSchemas(root: string): Promise<unknown[]> {\n // ONE SCHEMA PER FILE, so the whole directory is the declaration. Reading a\n // single well-known file would take the first one and leave the rest of the\n // project's tables with no typed surface and no complaint.\n //\n // SORTED, because readdir order is not specified: an unsorted set would hand\n // the runtime a different answer from one boot to the next.\n let entries: string[];\n try {\n entries = (await readdir(join(root, \"db\"))).sort();\n } catch {\n // Not every project declares a database; the typed `.tables` surface is\n // simply absent then.\n return [];\n }\n const out: unknown[] = [];\n for (const entry of entries) {\n if (!entry.endsWith(\".ts\") && !entry.endsWith(\".js\")) continue;\n if (entry.endsWith(\".d.ts\")) continue;\n out.push((await import(pathToFileURL(join(root, \"db\", entry)).href)).default);\n }\n return out;\n}\n\nasync function main(): Promise<void> {\n const command = process.argv[2] ?? \"serve\";\n if (command === \"--help\" || command === \"-h\" || command === \"help\") {\n console.log(USAGE);\n return;\n }\n if (![\"serve\", \"dev\", \"routes\"].includes(command)) {\n die(`palbase-backend: unknown command \"${command}\".\\n\\n${USAGE}`);\n }\n if (typeof Bun === \"undefined\") {\n die(\n \"palbase-backend needs Bun: the engine serves `fetch` natively and opens its own\\n\" +\n \"Postgres pool through Bun.sql. Install it from https://bun.sh, then re-run.\",\n );\n }\n\n // `dev` is `serve` under Bun's watcher. Re-exec rather than reimplement:\n // one code path serves, and the reload is the runtime's job, not ours.\n if (command === \"dev\" && !process.env.PALBASE_WATCHING) {\n const self = Bun.fileURLToPath(import.meta.url);\n const child = Bun.spawn([\"bun\", \"--watch\", self, \"serve\"], {\n stdio: [\"inherit\", \"inherit\", \"inherit\"],\n env: { ...process.env, PALBASE_WATCHING: \"1\" },\n });\n process.exit(await child.exited);\n }\n\n const root = resolve(process.env.PALBASE_PROJECT_DIR ?? process.cwd());\n await loadDotEnv(root);\n\n const files = await findModules(root);\n if (files.length === 0) {\n die(\n `palbase-backend: no *.module.ts under ${root}.\\n` +\n `A module is what says which classes exist, who owns them and what they may\\n` +\n `reach — and it is the only place that says it. Add one and run again:\\n\\n` +\n ` // todos.module.ts\\n` +\n ` import { Module, type Token } from \"@palbase/backend\";\\n` +\n ` import { TodosController } from \"./controllers/todos.controller.ts\";\\n` +\n ` @Module({ controllers: [TodosController as Token] })\\n` +\n ` export class TodosModule {}\\n`,\n );\n }\n\n // Importing IS the registration — for the module AND for everything it owns,\n // because a module imports the classes it lists.\n for (const file of files) await import(pathToFileURL(file).href);\n const controllers = getRegisteredControllers();\n if (controllers.length === 0) {\n die(\n `palbase-backend: ${files.length} module file(s) loaded but no controller registered.\\n` +\n `Either none of the listed classes carries @Controller, or the files were\\n` +\n `compiled with decorators stripped. Nothing would answer, so this is fatal.`,\n );\n }\n\n let config;\n try {\n config = loadConfig(process.env as Record<string, string | undefined>);\n } catch (e) {\n if (e instanceof BootRefused) {\n die(\n `${e.message}\\n\\n` +\n `Put them in a .env beside package.json, or export them. A local stack\\n` +\n `(docker compose up) publishes both.`,\n );\n }\n throw e;\n }\n\n const app = await createApp({ config, controllers, schemas: await importSchemas(root) });\n\n if (command === \"routes\") {\n for (const route of app.routes) console.log(route.id);\n await app.shutdown();\n return;\n }\n\n Bun.serve({ port: config.port, idleTimeout: 60, fetch: app.handle });\n console.log(`palbase-backend listening on http://localhost:${config.port}`);\n console.log(` ${app.routes.length} endpoint(s) from ${files.length} module file(s)`);\n for (const route of app.routes) console.log(` ${route.id}`);\n\n for (const signal of [\"SIGTERM\", \"SIGINT\"] as const) {\n process.on(signal, () => {\n void app.shutdown().finally(() => process.exit(0));\n });\n }\n}\n\n// Not a top-level await: this file is also emitted in a CommonJS flavour, where\n// one is a build error. The rejection handler is the point either way — an\n// unhandled one exits 0 on some runtimes, which would report a dead backend as\n// a successful start.\nmain().catch((e: unknown) => {\n console.error(e instanceof Error ? e.message : e);\n process.exit(1);\n});\n"],"mappings":"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","names":["readdir","join","resolve","pathToFileURL","USAGE","die","message","console","error","process","exit","loadDotEnv","root","file","Bun","join","exists","raw","text","split","line","trim","startsWith","eq","indexOf","key","slice","env","undefined","value","endsWith","NEVER_WALK","Set","findModules","out","walk","d","entries","readdir","withFileTypes","entry","has","name","full","isDirectory","test","push","sort","importSchemas","pathToFileURL","href","default","main","command","argv","log","includes","PALBASE_WATCHING","self","fileURLToPath","url","child","spawn","stdio","exited","resolve","PALBASE_PROJECT_DIR","cwd","files","length","controllers","getRegisteredControllers","config","loadConfig","e","BootRefused","app","createApp","schemas","route","routes","id","shutdown","serve","port","idleTimeout","fetch","handle","signal","on","finally","catch","Error"]}
@@ -1,9 +1,23 @@
1
1
  import {
2
- qualifiedTableKey
3
- } from "./chunk-SI4KGEM3.js";
2
+ queryColumnContract,
3
+ stableContract
4
+ } from "./chunk-PRH45NZU.js";
5
+ import {
6
+ TxPlanBuilder,
7
+ brandRef,
8
+ qualifiedTableKey,
9
+ retryBudget,
10
+ runTxPlan
11
+ } from "./chunk-P7MGSAFT.js";
12
+ import {
13
+ isRetryable
14
+ } from "./chunk-UMESJJRS.js";
4
15
  import {
5
16
  __name
6
- } from "./chunk-KATPXCJ5.js";
17
+ } from "./chunk-VXPNPVAG.js";
18
+
19
+ // src/db/policy.ts
20
+ import { z } from "zod";
7
21
 
8
22
  // src/db/schema.ts
9
23
  var IndexBuilder = class {
@@ -81,6 +95,10 @@ function defineTable(name, input) {
81
95
  if (name.trim() === "") {
82
96
  throw new Error("defineTable(name, \u2026): name must not be empty");
83
97
  }
98
+ {
99
+ const reserved = reservedCanRelation(name, input.columns);
100
+ if (reserved !== null) throw new Error(`defineTable(${JSON.stringify(name)}): ${reserved}`);
101
+ }
84
102
  const policies = (input.policies?.(exprCtx(Object.keys(input.columns), 0, name)) ?? []).map(toPolicyDef);
85
103
  const rls = policies.length > 0 || input.rls !== false;
86
104
  const owned = {};
@@ -109,6 +127,7 @@ function defineTable(name, input) {
109
127
  const declaredIndexes = (input.indexes?.(exprCtx(Object.keys(input.columns), 0, name)) ?? []).map((i) => i instanceof IndexBuilder ? i._def : i);
110
128
  if (declaredIndexes.length > 0) def.indexes = declaredIndexes;
111
129
  if (input.appendOnly === true) def.appendOnly = true;
130
+ if (input.ignored === true) def.ignored = true;
112
131
  if (input.search !== void 0) {
113
132
  const s = input.search;
114
133
  if (s.from !== void 0) {
@@ -168,6 +187,13 @@ function defineTable(name, input) {
168
187
  }
169
188
  __name(defineTable, "defineTable");
170
189
  var declaringSchemaOf = /* @__PURE__ */ new WeakMap();
190
+ function declaredTableKey(table) {
191
+ const metadata = table[TABLE_META];
192
+ const schema = declaringSchemaOf.get(metadata);
193
+ if (schema === void 0) throw new Error(`defineQuery: table ${metadata.name} must belong to defineSchema before defining its query`);
194
+ return qualifiedTableKey(schema, metadata.name);
195
+ }
196
+ __name(declaredTableKey, "declaredTableKey");
171
197
  function resolveReferences(tables, schemaName) {
172
198
  for (const table of Object.values(tables)) {
173
199
  for (const [colName, builder] of Object.entries(table.columns)) {
@@ -491,6 +517,45 @@ function policy(name) {
491
517
  return new PolicyBuilder(name);
492
518
  }
493
519
  __name(policy, "policy");
520
+ function can(table) {
521
+ const def = table;
522
+ if (typeof def.name !== "string" || def.columns === null || typeof def.columns !== "object" || !Array.isArray(def.policies)) {
523
+ throw new Error("can(<table>): expects the schema's table definition \u2014 can(publicSchema.tables.notes) \u2014 not the defineTable handle (`notes`)");
524
+ }
525
+ if (Object.prototype.hasOwnProperty.call(table.columns, "can")) {
526
+ throw new Error(`can(${table.name}): the table declares a column named "can", so the row carries that column and no policy echo \u2014 rename the column to get can`);
527
+ }
528
+ const rel = reservedCanRelation(table.name, table.columns);
529
+ if (rel !== null) {
530
+ throw new Error(`can(${table.name}): ${rel}`);
531
+ }
532
+ const names = table.policies.filter((p) => p.command === "update" && p.permissive !== false && Buffer.byteLength(`can:${p.name}`, "utf8") <= 63).map((p) => p.name);
533
+ if (names.length === 0) {
534
+ const all = table.policies.filter((p) => p.command === "all").map((p) => p.name);
535
+ const hint = all.length > 0 ? ` The table's ${all.map((n) => `"${n}"`).join(", ")} ${all.length === 1 ? "policy is" : "policies are"} for("all") \u2014 for("all") governs UPDATE but is not a named update policy and never appears in can.` : "";
536
+ throw new Error(`can(${table.name}): the table declares no update policy \u2014 there is nothing a row could echo. Name one with policy("<name>").for("update").using(\u2026) first.${hint}`);
537
+ }
538
+ return z.array(z.enum(names));
539
+ }
540
+ __name(can, "can");
541
+ function reservedCanRelation(tableName, columns) {
542
+ if (tableName === "can") {
543
+ return `a table named "can" would put a relation named "can" on every parent it references \u2014 rename the table`;
544
+ }
545
+ for (const [col2, b] of Object.entries(columns)) {
546
+ const def = b._def;
547
+ if (def.referencesThunk === void 0 && def.selfRefColumn === void 0) continue;
548
+ if (def.reverseAs === "can") {
549
+ return `column "${col2}" names its reverse relation "can" \u2014 the parent's rows would carry a relation where can is reserved; pick another reverseAs`;
550
+ }
551
+ const forward = def.owns === true ? "owner" : def.refAs ?? (col2.endsWith("_id") ? col2.slice(0, -3) : col2);
552
+ if (forward === "can") {
553
+ return `column "${col2}" would be read as a relation named "can" (${def.refAs === void 0 ? "derived from the column name" : "its as: option"}) \u2014 can is reserved for the policy echo; rename it`;
554
+ }
555
+ }
556
+ return null;
557
+ }
558
+ __name(reservedCanRelation, "reservedCanRelation");
494
559
 
495
560
  // src/db/extensions.ts
496
561
  var PALBASE_EXTENSIONS = [
@@ -559,11 +624,265 @@ var openai = {
559
624
  }
560
625
  };
561
626
 
627
+ // src/db/typed-db.ts
628
+ function makeTypedTable(name, raw2) {
629
+ return {
630
+ insert: /* @__PURE__ */ __name((data) => raw2.insert(name, data), "insert"),
631
+ aggregate: /* @__PURE__ */ __name((q) => raw2.aggregate(name, q), "aggregate"),
632
+ insertMany: /* @__PURE__ */ __name((rows, opts) => raw2.insertMany(name, rows, opts), "insertMany"),
633
+ put: /* @__PURE__ */ __name((q) => raw2.put(name, q.data, {
634
+ onConflict: q.onConflict
635
+ }), "put"),
636
+ update: /* @__PURE__ */ __name((q) => raw2.update(name, q.where.id, q.set), "update"),
637
+ delete: /* @__PURE__ */ __name((id) => raw2.delete(name, id), "delete"),
638
+ findUnique: /* @__PURE__ */ __name((q) => raw2.findUnique(name, q.where, {
639
+ select: q.select
640
+ }), "findUnique"),
641
+ findById: /* @__PURE__ */ __name((id) => raw2.findById(name, id), "findById"),
642
+ page: /* @__PURE__ */ __name((q) => {
643
+ const { where, ...opts } = q ?? {};
644
+ return raw2.page(name, where, opts);
645
+ }, "page"),
646
+ findMany: /* @__PURE__ */ __name((q) => {
647
+ const { where, ...opts } = q ?? {};
648
+ return raw2.findMany(name, where, opts);
649
+ }, "findMany"),
650
+ updateMany: /* @__PURE__ */ __name((q) => raw2.updateMany(name, q.where, q.set, q.returning === void 0 ? void 0 : {
651
+ returning: q.returning
652
+ }), "updateMany"),
653
+ deleteMany: /* @__PURE__ */ __name((q) => raw2.deleteMany(name, q.where), "deleteMany"),
654
+ count: /* @__PURE__ */ __name((q) => raw2.count(name, q?.where), "count")
655
+ };
656
+ }
657
+ __name(makeTypedTable, "makeTypedTable");
658
+ function makeTypedDB(schema, raw2) {
659
+ const tables = {};
660
+ for (const key of Object.keys(schema.tables)) {
661
+ const tableDef = schema.tables[key];
662
+ if (tableDef !== void 0) {
663
+ tables[key] = makeTypedTable(qualifiedTableKey(schema.name, tableDef.name), raw2);
664
+ }
665
+ }
666
+ const result = {
667
+ tables,
668
+ diagnostics: /* @__PURE__ */ __name(() => raw2.diagnostics(), "diagnostics"),
669
+ readQuery: /* @__PURE__ */ __name((query) => raw2.readQuery(query), "readQuery"),
670
+ compileQuery: /* @__PURE__ */ __name((query) => raw2.compileQuery(query), "compileQuery"),
671
+ explainQuery: /* @__PURE__ */ __name((query) => raw2.explainQuery(query), "explainQuery"),
672
+ command(fn, options) {
673
+ const builder = new TxPlanBuilder();
674
+ const planTables = Object.fromEntries(Object.entries(schema.tables).map(([key, table]) => [
675
+ key,
676
+ builder.table(qualifiedTableKey(schema.name, table.name))
677
+ ]));
678
+ return runTxPlan({
679
+ txPlan: /* @__PURE__ */ __name((plan) => raw2.command(plan, options), "txPlan")
680
+ }, {
681
+ tables: planTables
682
+ }, builder, fn);
683
+ },
684
+ transaction(fn) {
685
+ const builder = new TxPlanBuilder();
686
+ const planTables = {};
687
+ for (const key of Object.keys(schema.tables)) {
688
+ const tableDef = schema.tables[key];
689
+ if (tableDef !== void 0) planTables[key] = builder.table(qualifiedTableKey(schema.name, tableDef.name));
690
+ }
691
+ return runTxPlan(raw2, {
692
+ tables: planTables
693
+ }, builder, fn);
694
+ }
695
+ };
696
+ return result;
697
+ }
698
+ __name(makeTypedDB, "makeTypedDB");
699
+ function col(name) {
700
+ return brandRef({
701
+ $col: name
702
+ }, "col");
703
+ }
704
+ __name(col, "col");
705
+ async function withRetry(fn, opts = {}) {
706
+ const budget = retryBudget(opts.retry);
707
+ let attempt = 0;
708
+ for (; ; ) {
709
+ try {
710
+ return await fn();
711
+ } catch (e) {
712
+ if (attempt >= budget || !isRetryable(e)) throw e;
713
+ attempt += 1;
714
+ }
715
+ }
716
+ }
717
+ __name(withRetry, "withRetry");
718
+ function sqlFragment(strings, ...values) {
719
+ return brandRef({
720
+ $sql: {
721
+ text: [
722
+ ...strings
723
+ ],
724
+ values
725
+ }
726
+ }, "sql");
727
+ }
728
+ __name(sqlFragment, "sqlFragment");
729
+
730
+ // src/db/query.ts
731
+ import { z as z2 } from "zod";
732
+ import { OpenAPIRegistry, OpenApiGeneratorV31, extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
733
+ extendZodWithOpenApi(z2);
734
+ function responseColumn(column, validator) {
735
+ const contract = queryColumnContract(column);
736
+ let schema;
737
+ if (validator) return validator;
738
+ if (contract.codec === "number") schema = z2.number().finite();
739
+ else if (contract.codec === "decimal") schema = z2.string();
740
+ else switch (contract.type) {
741
+ case "integer":
742
+ schema = z2.number().int().min(-2147483648).max(2147483647);
743
+ break;
744
+ case "boolean":
745
+ schema = z2.boolean();
746
+ break;
747
+ case "vector":
748
+ schema = z2.array(z2.number().finite());
749
+ break;
750
+ case "enum": {
751
+ const values = contract.enumValues;
752
+ if (!values?.length) throw new Error("Query response requires declared enum values");
753
+ schema = z2.enum(values);
754
+ break;
755
+ }
756
+ case "uuid":
757
+ case "text":
758
+ case "timestamp":
759
+ case "numeric":
760
+ case "bigint":
761
+ schema = z2.string();
762
+ break;
763
+ case "jsonb":
764
+ throw new Error("Query response requires a fields validator for a selected jsonb column: its TypeScript payload is erased at runtime");
765
+ default:
766
+ throw new Error(`Query response does not support column type ${contract.type}`);
767
+ }
768
+ return contract.nullable ? schema.nullable() : schema;
769
+ }
770
+ __name(responseColumn, "responseColumn");
771
+ function defineQuery(name, options) {
772
+ 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");
773
+ const table = declaredTableKey(options.table);
774
+ const select = [
775
+ ...options.select
776
+ ];
777
+ if (!select.length || new Set(select).size !== select.length) throw new Error(`Query ${name}: select must be explicit, nonempty and distinct`);
778
+ const shape = {}, columns = {};
779
+ for (const key of select) {
780
+ if (!Object.hasOwn(options.table[TABLE_META].columns, key)) throw new Error(`Query ${name}: unknown column ${key}`);
781
+ columns[key] = queryColumnContract(options.table[key]);
782
+ shape[key] = responseColumn(options.table[key], options.fields?.[key]);
783
+ }
784
+ const response = z2.array(z2.object(shape).strict());
785
+ const queryInput = options.input, where = options.where, limit = options.limit;
786
+ const orderBy = options.orderBy === void 0 ? void 0 : JSON.parse(JSON.stringify(options.orderBy));
787
+ const prepare = /* @__PURE__ */ __name((input) => {
788
+ const parsed = queryInput.parse(input);
789
+ const filter = where === void 0 ? {} : where(parsed);
790
+ if (!filter || typeof filter !== "object" || Array.isArray(filter) || typeof filter.then === "function") {
791
+ throw new Error(`Query ${name}: where must return a synchronous filter object`);
792
+ }
793
+ const size = typeof limit === "function" ? limit(parsed) : limit;
794
+ if (limit !== void 0 && (typeof size !== "number" || !Number.isSafeInteger(size) || size < 0)) throw new Error(`Query ${name}: limit must produce a nonnegative safe integer`);
795
+ return {
796
+ name,
797
+ table,
798
+ columns: JSON.parse(JSON.stringify(columns)),
799
+ where: filter,
800
+ options: {
801
+ select: [
802
+ ...select
803
+ ],
804
+ ...orderBy === void 0 ? {} : {
805
+ orderBy: structuredClone(orderBy)
806
+ },
807
+ ...limit === void 0 ? {} : {
808
+ limit: size
809
+ }
810
+ }
811
+ };
812
+ }, "prepare");
813
+ const attach = /* @__PURE__ */ __name((schema) => {
814
+ const api = {
815
+ queryName: name,
816
+ input: queryInput,
817
+ prepare,
818
+ async run(database, input) {
819
+ const request = prepare(input);
820
+ const rows = await ("$readQuery" in database ? database.$readQuery(request) : database.readQuery(request));
821
+ return schema.parse(rows);
822
+ },
823
+ sql(database, input) {
824
+ const request = prepare(input);
825
+ return "$compileQuery" in database ? database.$compileQuery(request) : database.compileQuery(request);
826
+ },
827
+ explain(database, input) {
828
+ const request = prepare(input);
829
+ return "$explainQuery" in database ? database.$explainQuery(request) : database.explainQuery(request);
830
+ },
831
+ contract() {
832
+ const registry = new OpenAPIRegistry();
833
+ registry.register(`${name}Input`, queryInput);
834
+ registry.register(`${name}Response`, schema);
835
+ return JSON.parse(JSON.stringify({
836
+ name,
837
+ table,
838
+ select,
839
+ columns,
840
+ components: new OpenApiGeneratorV31(registry.definitions).generateComponents().components
841
+ }));
842
+ }
843
+ };
844
+ for (const key of [
845
+ "min",
846
+ "max",
847
+ "length",
848
+ "describe",
849
+ "openapi"
850
+ ]) {
851
+ const modify = schema[key].bind(schema);
852
+ Object.defineProperty(schema, key, {
853
+ value: /* @__PURE__ */ __name((...args) => attach(modify(...args)), "value"),
854
+ configurable: true
855
+ });
856
+ }
857
+ return Object.assign(schema, api);
858
+ }, "attach");
859
+ return attach(response);
860
+ }
861
+ __name(defineQuery, "defineQuery");
862
+ function queryManifest(queries) {
863
+ const sorted = [
864
+ ...queries
865
+ ].sort((a, b) => a.queryName.localeCompare(b.queryName));
866
+ if (new Set(sorted.map((query) => query.queryName)).size !== sorted.length) throw new Error("Query manifest contains duplicate names");
867
+ return JSON.parse(stableContract({
868
+ version: 1,
869
+ queries: sorted.map((query) => query.contract())
870
+ }));
871
+ }
872
+ __name(queryManifest, "queryManifest");
873
+ function assertQueryManifest(queries, baseline) {
874
+ if (stableContract(queryManifest(queries)) !== stableContract(baseline)) {
875
+ throw new Error("Query contract drift: compare queryManifest() with the committed manifest and review the API/client changes before updating it");
876
+ }
877
+ }
878
+ __name(assertQueryManifest, "assertQueryManifest");
879
+
562
880
  export {
563
881
  PolicyExprRef,
564
882
  exprCtx,
565
883
  PolicyBuilder,
566
884
  policy,
885
+ can,
567
886
  IndexBuilder,
568
887
  index,
569
888
  TABLE_META,
@@ -573,6 +892,13 @@ export {
573
892
  EXTENSION_DEPENDENCIES,
574
893
  isPalbaseExtension,
575
894
  raw,
576
- openai
895
+ openai,
896
+ makeTypedDB,
897
+ col,
898
+ withRetry,
899
+ sqlFragment,
900
+ defineQuery,
901
+ queryManifest,
902
+ assertQueryManifest
577
903
  };
578
- //# sourceMappingURL=chunk-YIQ4RS4F.js.map
904
+ //# sourceMappingURL=chunk-5UGPXKMJ.js.map