@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.
- package/dist/bin/palbase-backend.cjs +2273 -358
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +9 -7
- package/dist/bin/palbase-backend.js.map +1 -1
- package/dist/{chunk-YIQ4RS4F.js → chunk-5UGPXKMJ.js} +331 -5
- package/dist/chunk-5UGPXKMJ.js.map +1 -0
- package/dist/chunk-GHNC2UHS.js +20 -0
- package/dist/chunk-GHNC2UHS.js.map +1 -0
- package/dist/{chunk-AAT5G7KY.js → chunk-JJW3L62T.js} +2 -2
- package/dist/chunk-JJW3L62T.js.map +1 -0
- package/dist/{chunk-SI4KGEM3.js → chunk-P7MGSAFT.js} +102 -96
- package/dist/chunk-P7MGSAFT.js.map +1 -0
- package/dist/{chunk-XOX6RFPZ.js → chunk-PRH45NZU.js} +46 -3
- package/dist/chunk-PRH45NZU.js.map +1 -0
- package/dist/{chunk-26GH54D2.js → chunk-RZLY4BOY.js} +5 -3
- package/dist/{chunk-26GH54D2.js.map → chunk-RZLY4BOY.js.map} +1 -1
- package/dist/{chunk-KATPXCJ5.js → chunk-UMESJJRS.js} +19 -40
- package/dist/chunk-UMESJJRS.js.map +1 -0
- package/dist/chunk-VXPNPVAG.js +40 -0
- package/dist/chunk-VXPNPVAG.js.map +1 -0
- package/dist/{chunk-WWUG2QXF.js → chunk-X2UYIXDS.js} +59 -4
- package/dist/chunk-X2UYIXDS.js.map +1 -0
- package/dist/{chunk-C6COAB3E.js → chunk-XZPDPYVY.js} +1905 -195
- package/dist/chunk-XZPDPYVY.js.map +1 -0
- package/dist/{chunk-TVCCR6SO.js → chunk-YO5GYM73.js} +58 -9
- package/dist/chunk-YO5GYM73.js.map +1 -0
- package/dist/db/env.cjs.map +1 -1
- package/dist/db/env.d.cts +2 -0
- package/dist/db/env.d.ts +2 -0
- package/dist/db/index.cjs +285 -2
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +46 -3
- package/dist/db/index.d.ts +46 -3
- package/dist/db/index.js +14 -5
- package/dist/{index-DMZG3kpo.d.ts → endpoint-BXPdsTA5.d.ts} +370 -58
- package/dist/{index-DB_nW-AV.d.cts → endpoint-DqhUP7RT.d.cts} +370 -58
- package/dist/engine/index.cjs +2279 -356
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +5 -5
- package/dist/engine/index.d.ts +5 -5
- package/dist/engine/index.js +17 -7
- package/dist/{index-DLveQoOf.d.cts → index-Co0Hj_aO.d.cts} +108 -50
- package/dist/{index-D-3duy8Y.d.ts → index-MhCL7ll4.d.ts} +108 -50
- package/dist/index.cjs +806 -386
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +27 -15
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.cjs +7 -0
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +3 -3
- package/dist/openapi/index.d.ts +3 -3
- package/dist/openapi/index.js +4 -3
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-DSTThhKf.d.cts → registry-BkIVHPzB.d.cts} +3 -1
- package/dist/{registry-JjF5lcj4.d.ts → registry-CvGLR5j1.d.ts} +3 -1
- package/dist/test/index.cjs +103 -66
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +3 -3
- package/dist/test/index.d.ts +3 -3
- package/dist/test/index.js +33 -7
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +1 -1
- package/docs/auth.md +58 -0
- package/docs/database.md +308 -0
- package/docs/examples/check-query-contracts.ts +5 -0
- package/docs/examples/database-dx.contract.json +65 -0
- package/docs/examples/database-dx.ts +63 -0
- package/docs/examples/money-transfer.ts +73 -0
- package/docs/llms-full.txt +382 -1
- package/docs/schema.md +15 -0
- package/package.json +1 -2
- package/template/db/public.ts +12 -0
- package/template/package.json +1 -1
- package/dist/chunk-AAT5G7KY.js.map +0 -1
- package/dist/chunk-C6COAB3E.js.map +0 -1
- package/dist/chunk-KATPXCJ5.js.map +0 -1
- package/dist/chunk-SI4KGEM3.js.map +0 -1
- package/dist/chunk-TVCCR6SO.js.map +0 -1
- package/dist/chunk-WWUG2QXF.js.map +0 -1
- package/dist/chunk-XOX6RFPZ.js.map +0 -1
- package/dist/chunk-YIQ4RS4F.js.map +0 -1
- package/stack-images.json +0 -24
|
@@ -3,17 +3,19 @@ import {
|
|
|
3
3
|
BootRefused,
|
|
4
4
|
createApp,
|
|
5
5
|
loadConfig
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-
|
|
10
|
-
import "../chunk-
|
|
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-
|
|
14
|
+
} from "../chunk-JJW3L62T.js";
|
|
15
|
+
import "../chunk-UMESJJRS.js";
|
|
14
16
|
import {
|
|
15
17
|
__name
|
|
16
|
-
} from "../chunk-
|
|
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
|
-
|
|
3
|
-
|
|
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-
|
|
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-
|
|
904
|
+
//# sourceMappingURL=chunk-5UGPXKMJ.js.map
|