@palbase/backend 25.1.0 → 27.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.
- package/dist/bin/palbase-backend.cjs +2401 -1039
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +87 -51
- package/dist/bin/palbase-backend.js.map +1 -1
- package/dist/chunk-CGNN2PUH.js +213 -0
- package/dist/chunk-CGNN2PUH.js.map +1 -0
- package/dist/chunk-DRZFQRJI.js +370 -0
- package/dist/chunk-DRZFQRJI.js.map +1 -0
- package/dist/chunk-GYK6QYS4.js +708 -0
- package/dist/chunk-GYK6QYS4.js.map +1 -0
- package/dist/chunk-I3C4PFIW.js +930 -0
- package/dist/chunk-I3C4PFIW.js.map +1 -0
- package/dist/{chunk-VDF2T4AS.js → chunk-OO7R25AI.js} +1213 -591
- package/dist/chunk-OO7R25AI.js.map +1 -0
- package/dist/chunk-TS4U7NBD.js +885 -0
- package/dist/chunk-TS4U7NBD.js.map +1 -0
- package/dist/{chunk-YOY5DFQS.js → chunk-TWX6JTGJ.js} +76 -34
- package/dist/{chunk-YOY5DFQS.js.map → chunk-TWX6JTGJ.js.map} +1 -1
- package/dist/{chunk-35PNTIRN.js → chunk-VVMJEVQP.js} +63 -162
- package/dist/chunk-VVMJEVQP.js.map +1 -0
- package/dist/{chunk-7D4SUZUM.js → chunk-VXPNPVAG.js} +3 -1
- package/dist/db/env.cjs.map +1 -1
- package/dist/db/env.d.cts +2 -2
- package/dist/db/env.d.ts +2 -2
- package/dist/db/index.cjs +754 -344
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +7 -4
- package/dist/engine/index.cjs +2339 -1006
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +6 -6
- package/dist/engine/index.d.ts +6 -6
- package/dist/engine/index.js +7 -6
- package/dist/index-BrvvxSpn.d.ts +4844 -0
- package/dist/index-Bve7BBTL.d.cts +4844 -0
- package/dist/{index-CUomTA3e.d.ts → index-NuzRCuxe.d.ts} +171 -296
- package/dist/{index-ClpDeSos.d.cts → index-VtToZmUm.d.cts} +171 -296
- package/dist/index.cjs +2694 -1169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -19
- package/dist/index.d.ts +165 -19
- package/dist/index.js +738 -477
- package/dist/index.js.map +1 -1
- package/dist/module-Dl1KFVtc.d.cts +54 -0
- package/dist/module-Dl1KFVtc.d.ts +54 -0
- package/dist/openapi/index.cjs +1330 -484
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +4 -2
- package/dist/openapi/index.d.ts +4 -2
- package/dist/openapi/index.js +1264 -474
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-dZZ5JKYg.d.ts → registry-B0eyOF9x.d.ts} +1 -1
- package/dist/{registry-CC0WBQq6.d.cts → registry-Bk9_rbNd.d.cts} +1 -1
- package/dist/stack.cjs.map +1 -1
- package/dist/test/index.cjs +705 -141
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +30 -4
- package/dist/test/index.d.ts +30 -4
- package/dist/test/index.js +472 -124
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +33 -18
- package/docs/auth.md +1 -1
- package/docs/background.md +2 -2
- package/docs/database.md +221 -50
- package/docs/endpoints.md +3 -4
- package/docs/events.md +3 -3
- package/docs/getting-started.md +1 -1
- package/docs/llms-full.txt +401 -117
- package/docs/migrations.md +2 -2
- package/docs/schema.md +19 -10
- package/docs/services.md +116 -26
- package/package.json +8 -4
- package/stager/generics.js +205 -0
- package/stager/stage.js +39 -3
- package/template/AGENTS.md +110 -72
- package/template/db/public.ts +1 -1
- package/template/{controllers → modules/health}/health.controller.ts +1 -1
- package/template/modules/health/health.module.ts +24 -0
- package/template/modules/notes/note.service.test.ts +49 -0
- package/template/modules/notes/note.service.ts +108 -0
- package/template/{controllers → modules/notes}/notes.controller.ts +17 -11
- package/template/modules/notes/notes.module.ts +37 -0
- package/template/package.json +5 -3
- package/template/scripts/test.sh +33 -0
- package/template/tsconfig.json +29 -30
- package/dist/chunk-35PNTIRN.js.map +0 -1
- package/dist/chunk-CJSKYY76.js +0 -627
- package/dist/chunk-CJSKYY76.js.map +0 -1
- package/dist/chunk-CRQKCRGF.js +0 -276
- package/dist/chunk-CRQKCRGF.js.map +0 -1
- package/dist/chunk-G4R6BTLV.js +0 -662
- package/dist/chunk-G4R6BTLV.js.map +0 -1
- package/dist/chunk-VDF2T4AS.js.map +0 -1
- package/dist/chunk-XABBC7JP.js +0 -55
- package/dist/chunk-XABBC7JP.js.map +0 -1
- package/dist/endpoint-CTEHhb7A.d.ts +0 -2386
- package/dist/endpoint-DYHMo6cC.d.cts +0 -2386
- package/dist/index-CW21M9Z3.d.ts +0 -1222
- package/dist/index-CmBK76nx.d.cts +0 -1222
- package/template/services/note.service.test.ts +0 -45
- package/template/services/note.service.ts +0 -76
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-VXPNPVAG.js.map} +0 -0
- /package/template/{models/notes → modules/notes/dto}/create.ts +0 -0
|
@@ -3,17 +3,20 @@ import {
|
|
|
3
3
|
BootRefused,
|
|
4
4
|
createApp,
|
|
5
5
|
loadConfig
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-OO7R25AI.js";
|
|
7
|
+
import "../chunk-I3C4PFIW.js";
|
|
8
|
+
import "../chunk-DRZFQRJI.js";
|
|
9
|
+
import "../chunk-GYK6QYS4.js";
|
|
10
10
|
import {
|
|
11
11
|
getRegisteredControllers
|
|
12
|
-
} from "../chunk-
|
|
13
|
-
import "../chunk-
|
|
12
|
+
} from "../chunk-VVMJEVQP.js";
|
|
13
|
+
import "../chunk-CGNN2PUH.js";
|
|
14
|
+
import {
|
|
15
|
+
__name
|
|
16
|
+
} from "../chunk-VXPNPVAG.js";
|
|
14
17
|
|
|
15
18
|
// src/bin/palbase-backend.ts
|
|
16
|
-
import { readdir
|
|
19
|
+
import { readdir } from "fs/promises";
|
|
17
20
|
import { join, resolve } from "path";
|
|
18
21
|
import { pathToFileURL } from "url";
|
|
19
22
|
var USAGE = `palbase-backend \u2014 run this project's backend
|
|
@@ -35,6 +38,7 @@ function die(message) {
|
|
|
35
38
|
console.error(message);
|
|
36
39
|
process.exit(1);
|
|
37
40
|
}
|
|
41
|
+
__name(die, "die");
|
|
38
42
|
async function loadDotEnv(root) {
|
|
39
43
|
const file = Bun.file(join(root, ".env"));
|
|
40
44
|
if (!await file.exists()) return;
|
|
@@ -52,24 +56,35 @@ async function loadDotEnv(root) {
|
|
|
52
56
|
process.env[key] = value;
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
__name(loadDotEnv, "loadDotEnv");
|
|
60
|
+
var NEVER_WALK = /* @__PURE__ */ new Set([
|
|
61
|
+
"node_modules",
|
|
62
|
+
"dist",
|
|
63
|
+
"build",
|
|
64
|
+
".git"
|
|
65
|
+
]);
|
|
66
|
+
async function findModules(root) {
|
|
62
67
|
const out = [];
|
|
63
|
-
const walk = async (d) => {
|
|
64
|
-
|
|
68
|
+
const walk = /* @__PURE__ */ __name(async (d) => {
|
|
69
|
+
let entries;
|
|
70
|
+
try {
|
|
71
|
+
entries = await readdir(d, {
|
|
72
|
+
withFileTypes: true
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (NEVER_WALK.has(entry.name) || entry.name.startsWith(".palbase")) continue;
|
|
65
79
|
const full = join(d, entry.name);
|
|
66
80
|
if (entry.isDirectory()) await walk(full);
|
|
67
|
-
else if (/\.
|
|
81
|
+
else if (/\.module\.(ts|js|mts|mjs)$/.test(entry.name)) out.push(full);
|
|
68
82
|
}
|
|
69
|
-
};
|
|
70
|
-
await walk(
|
|
83
|
+
}, "walk");
|
|
84
|
+
await walk(root);
|
|
71
85
|
return out.sort();
|
|
72
86
|
}
|
|
87
|
+
__name(findModules, "findModules");
|
|
73
88
|
async function importSchemas(root) {
|
|
74
89
|
let entries;
|
|
75
90
|
try {
|
|
@@ -85,86 +100,107 @@ async function importSchemas(root) {
|
|
|
85
100
|
}
|
|
86
101
|
return out;
|
|
87
102
|
}
|
|
103
|
+
__name(importSchemas, "importSchemas");
|
|
88
104
|
async function main() {
|
|
89
105
|
const command = process.argv[2] ?? "serve";
|
|
90
106
|
if (command === "--help" || command === "-h" || command === "help") {
|
|
91
107
|
console.log(USAGE);
|
|
92
108
|
return;
|
|
93
109
|
}
|
|
94
|
-
if (![
|
|
110
|
+
if (![
|
|
111
|
+
"serve",
|
|
112
|
+
"dev",
|
|
113
|
+
"routes"
|
|
114
|
+
].includes(command)) {
|
|
95
115
|
die(`palbase-backend: unknown command "${command}".
|
|
96
116
|
|
|
97
117
|
${USAGE}`);
|
|
98
118
|
}
|
|
99
119
|
if (typeof Bun === "undefined") {
|
|
100
|
-
die(
|
|
101
|
-
"palbase-backend needs Bun: the engine serves `fetch` natively and opens its own\nPostgres pool through Bun.sql. Install it from https://bun.sh, then re-run."
|
|
102
|
-
);
|
|
120
|
+
die("palbase-backend needs Bun: the engine serves `fetch` natively and opens its own\nPostgres pool through Bun.sql. Install it from https://bun.sh, then re-run.");
|
|
103
121
|
}
|
|
104
122
|
if (command === "dev" && !process.env.PALBASE_WATCHING) {
|
|
105
123
|
const self = Bun.fileURLToPath(import.meta.url);
|
|
106
|
-
const child = Bun.spawn([
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
const child = Bun.spawn([
|
|
125
|
+
"bun",
|
|
126
|
+
"--watch",
|
|
127
|
+
self,
|
|
128
|
+
"serve"
|
|
129
|
+
], {
|
|
130
|
+
stdio: [
|
|
131
|
+
"inherit",
|
|
132
|
+
"inherit",
|
|
133
|
+
"inherit"
|
|
134
|
+
],
|
|
135
|
+
env: {
|
|
136
|
+
...process.env,
|
|
137
|
+
PALBASE_WATCHING: "1"
|
|
138
|
+
}
|
|
109
139
|
});
|
|
110
140
|
process.exit(await child.exited);
|
|
111
141
|
}
|
|
112
142
|
const root = resolve(process.env.PALBASE_PROJECT_DIR ?? process.cwd());
|
|
113
143
|
await loadDotEnv(root);
|
|
114
|
-
const files = await
|
|
144
|
+
const files = await findModules(root);
|
|
115
145
|
if (files.length === 0) {
|
|
116
|
-
die(
|
|
117
|
-
|
|
118
|
-
|
|
146
|
+
die(`palbase-backend: no *.module.ts under ${root}.
|
|
147
|
+
A module is what says which classes exist, who owns them and what they may
|
|
148
|
+
reach \u2014 and it is the only place that says it. Add one and run again:
|
|
119
149
|
|
|
120
|
-
//
|
|
121
|
-
import {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
`
|
|
127
|
-
);
|
|
150
|
+
// todos.module.ts
|
|
151
|
+
import { Module, type Token } from "@palbase/backend";
|
|
152
|
+
import { TodosController } from "./controllers/todos.controller.ts";
|
|
153
|
+
@Module({ controllers: [TodosController as Token] })
|
|
154
|
+
export class TodosModule {}
|
|
155
|
+
`);
|
|
128
156
|
}
|
|
129
157
|
for (const file of files) await import(pathToFileURL(file).href);
|
|
130
158
|
const controllers = getRegisteredControllers();
|
|
131
159
|
if (controllers.length === 0) {
|
|
132
|
-
die(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
compiled with decorators stripped. Nothing would answer, so this is fatal.`
|
|
136
|
-
);
|
|
160
|
+
die(`palbase-backend: ${files.length} module file(s) loaded but no controller registered.
|
|
161
|
+
Either none of the listed classes carries @Controller, or the files were
|
|
162
|
+
compiled with decorators stripped. Nothing would answer, so this is fatal.`);
|
|
137
163
|
}
|
|
138
164
|
let config;
|
|
139
165
|
try {
|
|
140
166
|
config = loadConfig(process.env);
|
|
141
167
|
} catch (e) {
|
|
142
168
|
if (e instanceof BootRefused) {
|
|
143
|
-
die(
|
|
144
|
-
`${e.message}
|
|
169
|
+
die(`${e.message}
|
|
145
170
|
|
|
146
171
|
Put them in a .env beside package.json, or export them. A local stack
|
|
147
|
-
(docker compose up) publishes both.`
|
|
148
|
-
);
|
|
172
|
+
(docker compose up) publishes both.`);
|
|
149
173
|
}
|
|
150
174
|
throw e;
|
|
151
175
|
}
|
|
152
|
-
const app = await createApp({
|
|
176
|
+
const app = await createApp({
|
|
177
|
+
config,
|
|
178
|
+
controllers,
|
|
179
|
+
schemas: await importSchemas(root)
|
|
180
|
+
});
|
|
153
181
|
if (command === "routes") {
|
|
154
182
|
for (const route of app.routes) console.log(route.id);
|
|
155
183
|
await app.shutdown();
|
|
156
184
|
return;
|
|
157
185
|
}
|
|
158
|
-
Bun.serve({
|
|
186
|
+
Bun.serve({
|
|
187
|
+
port: config.port,
|
|
188
|
+
idleTimeout: 60,
|
|
189
|
+
fetch: app.handle
|
|
190
|
+
});
|
|
159
191
|
console.log(`palbase-backend listening on http://localhost:${config.port}`);
|
|
160
|
-
console.log(` ${app.routes.length} endpoint(s) from ${files.length}
|
|
192
|
+
console.log(` ${app.routes.length} endpoint(s) from ${files.length} module file(s)`);
|
|
161
193
|
for (const route of app.routes) console.log(` ${route.id}`);
|
|
162
|
-
for (const signal of [
|
|
194
|
+
for (const signal of [
|
|
195
|
+
"SIGTERM",
|
|
196
|
+
"SIGINT"
|
|
197
|
+
]) {
|
|
163
198
|
process.on(signal, () => {
|
|
164
199
|
void app.shutdown().finally(() => process.exit(0));
|
|
165
200
|
});
|
|
166
201
|
}
|
|
167
202
|
}
|
|
203
|
+
__name(main, "main");
|
|
168
204
|
main().catch((e) => {
|
|
169
205
|
console.error(e instanceof Error ? e.message : e);
|
|
170
206
|
process.exit(1);
|
|
@@ -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, stat } 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. */\nasync function findControllers(root: string): Promise<string[]> {\n const dir = join(root, \"controllers\");\n try {\n if (!(await stat(dir)).isDirectory()) return [];\n } catch {\n return [];\n }\n const out: string[] = [];\n const walk = async (d: string): Promise<void> => {\n for (const entry of await readdir(d, { withFileTypes: true })) {\n const full = join(d, entry.name);\n if (entry.isDirectory()) await walk(full);\n else if (/\\.controller\\.(ts|js|mts|mjs)$/.test(entry.name)) out.push(full);\n }\n };\n await walk(dir);\n // Sorted so route precedence 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 findControllers(root);\n if (files.length === 0) {\n die(\n `palbase-backend: no controllers found under ${join(root, \"controllers\")}.\\n` +\n `A backend is its controllers — add one and run again:\\n\\n` +\n ` // controllers/hello.controller.ts\\n` +\n ` import { Controller, Get } from \"@palbase/backend\";\\n` +\n ` @Controller(\"/hello\")\\n` +\n ` class HelloController {\\n` +\n ` @Get(\"\") hi(): Promise<{ ok: boolean }> { return Promise.resolve({ ok: true }); }\\n` +\n ` }\\n`,\n );\n }\n\n // Importing IS the registration — the decorator records each class as it runs.\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} controller file(s) loaded but none registered.\\n` +\n `Every one of them is missing its @Controller decorator, 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} controller 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":[]}
|
|
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"]}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-VXPNPVAG.js";
|
|
4
|
+
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
var HTTP_ERROR_BRAND = /* @__PURE__ */ Symbol.for("palbase.backend.httpError");
|
|
7
|
+
var ENGINE_RAISED = /* @__PURE__ */ Symbol.for("palbase.backend.engineRaised");
|
|
8
|
+
function markEngineRaised(e) {
|
|
9
|
+
e[ENGINE_RAISED] = true;
|
|
10
|
+
return e;
|
|
11
|
+
}
|
|
12
|
+
__name(markEngineRaised, "markEngineRaised");
|
|
13
|
+
function isEngineRaised(e) {
|
|
14
|
+
return typeof e === "object" && e !== null && e[ENGINE_RAISED] === true;
|
|
15
|
+
}
|
|
16
|
+
__name(isEngineRaised, "isEngineRaised");
|
|
17
|
+
function isHttpError(err) {
|
|
18
|
+
if (typeof err !== "object" || err === null) return false;
|
|
19
|
+
const e = err;
|
|
20
|
+
return e[HTTP_ERROR_BRAND] === true && typeof e.status === "number" && typeof e.error === "string" && typeof e.errorDescription === "string";
|
|
21
|
+
}
|
|
22
|
+
__name(isHttpError, "isHttpError");
|
|
23
|
+
var HttpError = class extends Error {
|
|
24
|
+
static {
|
|
25
|
+
__name(this, "HttpError");
|
|
26
|
+
}
|
|
27
|
+
status;
|
|
28
|
+
error;
|
|
29
|
+
errorDescription;
|
|
30
|
+
data;
|
|
31
|
+
/** See {@link HTTP_ERROR_BRAND} — how the engine recognises this across SDK copies. */
|
|
32
|
+
[HTTP_ERROR_BRAND] = true;
|
|
33
|
+
constructor(status, error, errorDescription, data) {
|
|
34
|
+
super(errorDescription);
|
|
35
|
+
this.name = "HttpError";
|
|
36
|
+
this.status = status;
|
|
37
|
+
this.error = error;
|
|
38
|
+
this.errorDescription = errorDescription;
|
|
39
|
+
if (data !== void 0) {
|
|
40
|
+
this.data = data;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Serialize to the standard Palbase error response format.
|
|
45
|
+
* The `requestId` is injected by the runtime layer from the request context.
|
|
46
|
+
* When called without arguments (e.g. JSON.stringify), request_id is omitted.
|
|
47
|
+
* When `data` is set, it is appended as a strict-superset field.
|
|
48
|
+
*/
|
|
49
|
+
toJSON(requestId) {
|
|
50
|
+
const result = {
|
|
51
|
+
error: this.error,
|
|
52
|
+
error_description: this.errorDescription,
|
|
53
|
+
status: this.status
|
|
54
|
+
};
|
|
55
|
+
if (requestId) {
|
|
56
|
+
result.request_id = requestId;
|
|
57
|
+
}
|
|
58
|
+
if (this.data !== void 0) {
|
|
59
|
+
result.data = this.data;
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var PalError = class extends HttpError {
|
|
65
|
+
static {
|
|
66
|
+
__name(this, "PalError");
|
|
67
|
+
}
|
|
68
|
+
constructor(status, code, description, data) {
|
|
69
|
+
super(status, code, description, data);
|
|
70
|
+
this.name = "PalError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var NamedHttpError = class NamedHttpError2 extends HttpError {
|
|
74
|
+
static {
|
|
75
|
+
__name(this, "NamedHttpError");
|
|
76
|
+
}
|
|
77
|
+
constructor(status, defaultCode, name, message, code, data) {
|
|
78
|
+
super(status, code ?? defaultCode, message ?? defaultMessage(name), data);
|
|
79
|
+
this.name = name;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
function defaultMessage(name) {
|
|
83
|
+
const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
84
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
__name(defaultMessage, "defaultMessage");
|
|
87
|
+
var BadRequest = class extends NamedHttpError {
|
|
88
|
+
static {
|
|
89
|
+
__name(this, "BadRequest");
|
|
90
|
+
}
|
|
91
|
+
constructor(data, message) {
|
|
92
|
+
super(400, "bad_request", "BadRequest", message, void 0, data);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var Unauthorized = class extends NamedHttpError {
|
|
96
|
+
static {
|
|
97
|
+
__name(this, "Unauthorized");
|
|
98
|
+
}
|
|
99
|
+
constructor(message, code, data) {
|
|
100
|
+
super(401, "unauthorized", "Unauthorized", message, code, data);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var Forbidden = class extends NamedHttpError {
|
|
104
|
+
static {
|
|
105
|
+
__name(this, "Forbidden");
|
|
106
|
+
}
|
|
107
|
+
constructor(message, code, data) {
|
|
108
|
+
super(403, "forbidden", "Forbidden", message, code, data);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var NotFound = class extends NamedHttpError {
|
|
112
|
+
static {
|
|
113
|
+
__name(this, "NotFound");
|
|
114
|
+
}
|
|
115
|
+
constructor(message, code, data) {
|
|
116
|
+
super(404, "not_found", "NotFound", message, code, data);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var Conflict = class extends NamedHttpError {
|
|
120
|
+
static {
|
|
121
|
+
__name(this, "Conflict");
|
|
122
|
+
}
|
|
123
|
+
constructor(message, code, data) {
|
|
124
|
+
super(409, "conflict", "Conflict", message, code, data);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var UniqueViolation = class extends Conflict {
|
|
128
|
+
static {
|
|
129
|
+
__name(this, "UniqueViolation");
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether `e` is a unique violation — REGARDLESS of which copy of this SDK
|
|
133
|
+
* constructed it.
|
|
134
|
+
*
|
|
135
|
+
* Use this instead of `instanceof`. Measured on a live stack: a controller
|
|
136
|
+
* bundle INLINES its own copy of `@palbase/backend`, and the engine that
|
|
137
|
+
* raises this error is the runtime's copy. Two copies, two class identities,
|
|
138
|
+
* and `e instanceof UniqueViolation` is false in the one place a caller
|
|
139
|
+
* writes it — a check that reads as correct and silently never matches.
|
|
140
|
+
*/
|
|
141
|
+
static is(e) {
|
|
142
|
+
return typeof e === "object" && e !== null && e.name === "UniqueViolation" && typeof e.constraint === "string";
|
|
143
|
+
}
|
|
144
|
+
/** The unique constraint the statement violated, as Postgres named it.
|
|
145
|
+
* `""` when the driver did not say which — see `engine/db.ts`. */
|
|
146
|
+
constraint;
|
|
147
|
+
constructor(constraint, message, code, data) {
|
|
148
|
+
super(message ?? "Unique constraint violated", code ?? "unique_violation", data);
|
|
149
|
+
this.name = "UniqueViolation";
|
|
150
|
+
this.constraint = constraint;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
function isRetryable(e) {
|
|
154
|
+
return typeof e === "object" && e !== null && e.retryable === true && typeof e.sqlstate === "string";
|
|
155
|
+
}
|
|
156
|
+
__name(isRetryable, "isRetryable");
|
|
157
|
+
var SerializationFailure = class extends Conflict {
|
|
158
|
+
static {
|
|
159
|
+
__name(this, "SerializationFailure");
|
|
160
|
+
}
|
|
161
|
+
retryable = true;
|
|
162
|
+
sqlstate;
|
|
163
|
+
static is(e) {
|
|
164
|
+
return typeof e === "object" && e !== null && e.name === "SerializationFailure";
|
|
165
|
+
}
|
|
166
|
+
constructor(sqlstate = "40001", message, data) {
|
|
167
|
+
super(message ?? "i\u015Flem e\u015Fzamanl\u0131 bir i\u015Flemle serile\u015Ftirilemedi \u2014 ayn\u0131 i\u015Flemi ba\u015Ftan \xE7al\u0131\u015Ft\u0131r\u0131n ($transaction'a { retry: n } verebilirsiniz)", "serialization_failure", data);
|
|
168
|
+
this.name = "SerializationFailure";
|
|
169
|
+
this.sqlstate = sqlstate;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var DeadlockDetected = class extends Conflict {
|
|
173
|
+
static {
|
|
174
|
+
__name(this, "DeadlockDetected");
|
|
175
|
+
}
|
|
176
|
+
retryable = true;
|
|
177
|
+
sqlstate;
|
|
178
|
+
static is(e) {
|
|
179
|
+
return typeof e === "object" && e !== null && e.name === "DeadlockDetected";
|
|
180
|
+
}
|
|
181
|
+
constructor(sqlstate = "40P01", message, data) {
|
|
182
|
+
super(message ?? "deadlock: iki i\u015Flem birbirinin kilidini bekledi ve bu i\u015Flem iptal edildi \u2014 kilit s\u0131ras\u0131n\u0131 sabitlemek i\xE7in Database.$lockRows(tablo, id'ler) kullan\u0131n", "deadlock_detected", data);
|
|
183
|
+
this.name = "DeadlockDetected";
|
|
184
|
+
this.sqlstate = sqlstate;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var TooManyRequests = class extends NamedHttpError {
|
|
188
|
+
static {
|
|
189
|
+
__name(this, "TooManyRequests");
|
|
190
|
+
}
|
|
191
|
+
constructor(data, message) {
|
|
192
|
+
super(429, "too_many_requests", "TooManyRequests", message, void 0, data);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export {
|
|
197
|
+
markEngineRaised,
|
|
198
|
+
isEngineRaised,
|
|
199
|
+
isHttpError,
|
|
200
|
+
HttpError,
|
|
201
|
+
PalError,
|
|
202
|
+
BadRequest,
|
|
203
|
+
Unauthorized,
|
|
204
|
+
Forbidden,
|
|
205
|
+
NotFound,
|
|
206
|
+
Conflict,
|
|
207
|
+
UniqueViolation,
|
|
208
|
+
isRetryable,
|
|
209
|
+
SerializationFailure,
|
|
210
|
+
DeadlockDetected,
|
|
211
|
+
TooManyRequests
|
|
212
|
+
};
|
|
213
|
+
//# sourceMappingURL=chunk-CGNN2PUH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts"],"sourcesContent":["/**\n * The brand that identifies an HttpError ACROSS SDK instances.\n *\n * A process legitimately holds more than one copy of this SDK — the runtime\n * loads the engine from its own node_modules while the tenant's bundle carries\n * an inlined copy, which is why the controller registry and the error registry\n * are both anchored on `Symbol.for`. The one place that did not follow the\n * pattern was the engine's catch: `err instanceof HttpError` compares CLASS\n * IDENTITY, so a `throw new NotFound()` from the bundle's copy did not match\n * the engine's copy and every typed error in every deployed backend degraded to\n * `500 internal_error`. Measured through the edge on a real deploy: a route\n * throwing `NotFound` answered 500 while the runtime's own log printed the\n * error object with `status: 404` right beside it.\n *\n * `Symbol.for` puts this in the cross-realm registry, so every copy of the SDK\n * agrees on it by VALUE rather than by identity.\n */\nexport const HTTP_ERROR_BRAND: unique symbol = Symbol.for(\"palbase.backend.httpError\");\n\n/**\n * Set on an `HttpError` the ENGINE built out of a driver failure, as opposed to\n * one the author constructed to ANSWER a request.\n *\n * The distinction cannot be read off the status, and 409 is why. The scaffold\n * teaches `throw new Conflict(\"title already taken\")` as the way to answer\n * (template/AGENTS.md), and the engine raises `UniqueViolation` — also a 409 —\n * when a write hits a unique index. Logging by status therefore either loses the\n * engine's event or writes an \"unhandled\" line every time an author takes the\n * documented path. Measured: it did the second.\n *\n * `Symbol.for` so the mark survives the bundle/runtime SDK split, the same way\n * {@link HTTP_ERROR_BRAND} does.\n */\nexport const ENGINE_RAISED: unique symbol = Symbol.for(\"palbase.backend.engineRaised\") as never;\n\n/** Mark `e` as engine-raised and return it, so a conversion site reads as one expression. */\nexport function markEngineRaised<E extends object>(e: E): E {\n (e as Record<symbol, unknown>)[ENGINE_RAISED] = true;\n return e;\n}\n\n/** Whether the engine built this error, rather than the author throwing it to answer. */\nexport function isEngineRaised(e: unknown): boolean {\n return typeof e === \"object\" && e !== null && (e as Record<symbol, unknown>)[ENGINE_RAISED] === true;\n}\n\n/**\n * Whether a thrown value is an HttpError from ANY copy of this SDK.\n *\n * The shape is checked as well as the brand: the brand says \"this claims to be\n * one of ours\", the fields say the envelope can actually be built from it, and\n * a half-formed object must fall through to the 500 path rather than produce a\n * malformed response.\n */\nexport function isHttpError(err: unknown): err is HttpError {\n if (typeof err !== \"object\" || err === null) return false;\n const e = err as Record<PropertyKey, unknown>;\n return (\n e[HTTP_ERROR_BRAND] === true &&\n typeof e.status === \"number\" &&\n typeof e.error === \"string\" &&\n typeof e.errorDescription === \"string\"\n );\n}\n\n/** HTTP error with structured error response format.\n *\n * The base class for the throwable error classes (`PalError`, `Conflict`,\n * `NotFound`, …). Construct one directly with `throw new HttpError(404,\n * \"todo_not_found\", \"No such todo\")`, or throw a named subclass\n * (`throw new NotFound(\"todo not found\")`). The runtime catches any `HttpError`\n * and emits the standard envelope; on the wire (and to iOS) it surfaces as\n * `BackendError.server(code, status, message, requestId)`.\n *\n * The optional `data` field carries a structured payload alongside the\n * standard envelope — for errors that need to ship extra context\n * (e.g. `new Conflict(\"locked\", \"title_locked\", { retryAfter: 30 })`). It rides\n * through to the iOS typed enum's associated value.\n */\nexport class HttpError extends Error {\n public readonly status: number;\n public readonly error: string;\n public readonly errorDescription: string;\n public readonly data?: unknown;\n /** See {@link HTTP_ERROR_BRAND} — how the engine recognises this across SDK copies. */\n public readonly [HTTP_ERROR_BRAND] = true;\n\n constructor(status: number, error: string, errorDescription: string, data?: unknown) {\n super(errorDescription);\n this.name = \"HttpError\";\n this.status = status;\n this.error = error;\n this.errorDescription = errorDescription;\n if (data !== undefined) {\n this.data = data;\n }\n }\n\n /**\n * Serialize to the standard Palbase error response format.\n * The `requestId` is injected by the runtime layer from the request context.\n * When called without arguments (e.g. JSON.stringify), request_id is omitted.\n * When `data` is set, it is appended as a strict-superset field.\n */\n toJSON(requestId?: string): {\n error: string;\n error_description: string;\n status: number;\n request_id?: string;\n data?: unknown;\n } {\n const result: {\n error: string;\n error_description: string;\n status: number;\n request_id?: string;\n data?: unknown;\n } = {\n error: this.error,\n error_description: this.errorDescription,\n status: this.status,\n };\n if (requestId) {\n result.request_id = requestId;\n }\n if (this.data !== undefined) {\n result.data = this.data;\n }\n return result;\n }\n}\n\n/**\n * Throw with a custom HTTP status + wire code. The general-purpose escape hatch\n * when none of the named classes (`Conflict`/`NotFound`/…) fits.\n *\n * @example\n * throw new PalError(418, \"teapot\", \"I'm a teapot\");\n */\nexport class PalError extends HttpError {\n constructor(status: number, code: string, description: string, data?: unknown) {\n super(status, code, description, data);\n this.name = \"PalError\";\n }\n}\n\n/** Base for the named status classes. Each subclass fixes its HTTP status; the\n * `code` defaults to the class's canonical wire code (overridable), and the\n * `message` defaults to a human-readable label (overridable). */\nabstract class NamedHttpError extends HttpError {\n protected constructor(\n status: number,\n defaultCode: string,\n name: string,\n message?: string,\n code?: string,\n data?: unknown,\n ) {\n super(status, code ?? defaultCode, message ?? defaultMessage(name), data);\n this.name = name;\n }\n}\n\n/** Derive a default human-readable message from a class name\n * (\"NotFound\" → \"Not found\", \"TooManyRequests\" → \"Too many requests\"). */\nfunction defaultMessage(name: string): string {\n const spaced = name.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\");\n return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();\n}\n\n/**\n * 400 — the request was malformed or failed validation. Carries a fixed typed\n * payload: `new BadRequest({ fields: [{ field: \"email\", message: \"invalid\" }] })`.\n * The shape is declared once in the SDK so codegen surfaces `error.data.fields`\n * typed on the client.\n */\nexport class BadRequest extends NamedHttpError {\n public declare readonly data: BadRequestData;\n constructor(data: BadRequestData, message?: string) {\n super(400, \"bad_request\", \"BadRequest\", message, undefined, data);\n }\n}\n\n/** 401 — the caller is not authenticated. */\nexport class Unauthorized extends NamedHttpError {\n constructor(message?: string, code?: string, data?: unknown) {\n super(401, \"unauthorized\", \"Unauthorized\", message, code, data);\n }\n}\n\n/** 403 — the caller is authenticated but not allowed. */\nexport class Forbidden extends NamedHttpError {\n constructor(message?: string, code?: string, data?: unknown) {\n super(403, \"forbidden\", \"Forbidden\", message, code, data);\n }\n}\n\n/** 404 — the requested resource does not exist. */\nexport class NotFound extends NamedHttpError {\n constructor(message?: string, code?: string, data?: unknown) {\n super(404, \"not_found\", \"NotFound\", message, code, data);\n }\n}\n\n/** 409 — the request conflicts with the current state. */\nexport class Conflict extends NamedHttpError {\n constructor(message?: string, code?: string, data?: unknown) {\n super(409, \"conflict\", \"Conflict\", message, code, data);\n }\n}\n\n/**\n * 409 — a write was refused because it would duplicate an existing row.\n * Carries the NAME of the unique constraint Postgres named (`users_email_key`).\n *\n * The engine produces it: a statement rejected with SQLSTATE `23505` is\n * converted here rather than surfacing as an opaque driver error (see\n * `engine/db.ts`, `diagnosingDriver`). What that removes is the string match —\n * before this, the only way to act on a duplicate was to test the driver\n * message for \"duplicate key value violates unique constraint\", a contract\n * nobody signed that breaks on a Postgres upgrade, a locale, or a constraint\n * rename, silently and in production.\n *\n * THE NAME IS A FIELD AND STAYS OUT OF THE DEFAULT MESSAGE. The two are not\n * the same audience. `constraint` is read by the code that catches this — the\n * developer, who already knows the schema. `errorDescription` is the HTTP\n * response body, and an UNCAUGHT duplicate puts it in front of the\n * application's end user: `users_email_key` there discloses how the schema is\n * built to whoever sent the request. The platform's own data API took the same\n * decision one surface over and wrote down why —\n * `v2/internal/modules/database/internal/handler/pgerror.go:83-87` collapses\n * every 23xxx to a generic conflict, \"never disclose the constraint/column\n * name\". A thrower who WANTS the name on the wire passes it deliberately\n * (`new UniqueViolation(c, \\`\\${c} already exists\\`)`, or through `data`).\n *\n * @example\n * try {\n * await Database.public.users.insert({ email });\n * } catch (e) {\n * if (UniqueViolation.is(e) && e.constraint === \"users_email_key\") {\n * throw new Conflict(\"That email is taken\", \"email_taken\");\n * }\n * throw e;\n * }\n */\nexport class UniqueViolation extends Conflict {\n /**\n * Whether `e` is a unique violation — REGARDLESS of which copy of this SDK\n * constructed it.\n *\n * Use this instead of `instanceof`. Measured on a live stack: a controller\n * bundle INLINES its own copy of `@palbase/backend`, and the engine that\n * raises this error is the runtime's copy. Two copies, two class identities,\n * and `e instanceof UniqueViolation` is false in the one place a caller\n * writes it — a check that reads as correct and silently never matches.\n */\n static is(e: unknown): e is UniqueViolation {\n return (\n typeof e === \"object\" &&\n e !== null &&\n (e as { name?: unknown }).name === \"UniqueViolation\" &&\n typeof (e as { constraint?: unknown }).constraint === \"string\"\n );\n }\n\n /** The unique constraint the statement violated, as Postgres named it.\n * `\"\"` when the driver did not say which — see `engine/db.ts`. */\n public readonly constraint: string;\n\n constructor(constraint: string, message?: string, code?: string, data?: unknown) {\n super(message ?? \"Unique constraint violated\", code ?? \"unique_violation\", data);\n this.name = \"UniqueViolation\";\n this.constraint = constraint;\n }\n}\n\n/**\n * Eşzamanlılık yüzünden BAŞARISIZ OLAN, ama TEKRAR DENENİRSE başarılı olabilecek\n * hataların ortak yüzeyi (FR-036).\n *\n * `retryable: true` bir tahmin değil, Postgres'in söylediği şey: `40001` ve\n * `40P01` \"bu işlemi baştan çalıştır\" anlamına gelir. Kümenin DAR tutulması\n * kasıtlı — `23505` buraya girseydi, tekrar denemek aynı cevabı verir ve\n * döngü sonsuza kadar dönerdi.\n */\nexport interface RetryableError extends Error {\n readonly retryable: true;\n /** Beş karakterlik SQLSTATE — hangi eşzamanlılık hatası olduğunu SÖYLER. */\n readonly sqlstate: string;\n}\n\n/**\n * `e` tekrar denenebilir mi?\n *\n * `instanceof` DEĞİL şekil kontrolü — ve sebebi ÖLÇÜLMÜŞ (bkz. {@link\n * UniqueViolation.is}): bir controller bundle'ı kendi `@palbase/backend`\n * kopyasını inline'lıyor, hatayı fırlatan motor ise runtime'ın kopyası. İki\n * sınıf kimliği, ve `e instanceof SerializationFailure` yazan tek yerde\n * sessizce hiç eşleşmiyor.\n */\nexport function isRetryable(e: unknown): e is RetryableError {\n return (\n typeof e === \"object\" &&\n e !== null &&\n (e as { retryable?: unknown }).retryable === true &&\n typeof (e as { sqlstate?: unknown }).sqlstate === \"string\"\n );\n}\n\n/**\n * `40001 serialization_failure` — SERIALIZABLE ya da REPEATABLE READ altında\n * eşzamanlı bir işlem bu işlemi geçersiz kıldı.\n *\n * D-016: bu hata BLOKLANMIYOR, FIRLATILIYOR. Retry'sız bir izolasyon\n * yükseltmesi sunmak defect'tir — kullanıcıya çalışmayan bir düğme vermektir:\n * SERIALIZABLE'a çıkıp `40001`'i yakalamayan kod, önceden çalışan işlemleri\n * rastgele kaybetmeye başlar.\n */\nexport class SerializationFailure extends Conflict implements RetryableError {\n public readonly retryable = true as const;\n public readonly sqlstate: string;\n\n static is(e: unknown): e is SerializationFailure {\n return (\n typeof e === \"object\" && e !== null &&\n (e as { name?: unknown }).name === \"SerializationFailure\"\n );\n }\n\n constructor(sqlstate = \"40001\", message?: string, data?: unknown) {\n super(\n message ??\n \"işlem eşzamanlı bir işlemle serileştirilemedi — aynı işlemi baştan çalıştırın \" +\n \"($transaction'a { retry: n } verebilirsiniz)\",\n \"serialization_failure\",\n data,\n );\n this.name = \"SerializationFailure\";\n this.sqlstate = sqlstate;\n }\n}\n\n/**\n * `40P01 deadlock_detected` — iki işlem birbirinin kilidini bekliyordu;\n * Postgres birini seçip iptal etti.\n *\n * Bu hatayı GÖRMEK bir tasarım sinyali: kilit sırası deterministik değilse\n * tekrar denemek yalnız maliyeti öteler. `Database.$lockRows(...)` (FR-035)\n * ya da çok satırlı yazmanın CTE'si (FR-015) sırayı sabitler.\n */\nexport class DeadlockDetected extends Conflict implements RetryableError {\n public readonly retryable = true as const;\n public readonly sqlstate: string;\n\n static is(e: unknown): e is DeadlockDetected {\n return (\n typeof e === \"object\" && e !== null &&\n (e as { name?: unknown }).name === \"DeadlockDetected\"\n );\n }\n\n constructor(sqlstate = \"40P01\", message?: string, data?: unknown) {\n super(\n message ??\n \"deadlock: iki işlem birbirinin kilidini bekledi ve bu işlem iptal edildi — \" +\n \"kilit sırasını sabitlemek için Database.$lockRows(tablo, id'ler) kullanın\",\n \"deadlock_detected\",\n data,\n );\n this.name = \"DeadlockDetected\";\n this.sqlstate = sqlstate;\n }\n}\n\n/** A single field-level validation failure carried by {@link BadRequest}. */\nexport interface FieldError {\n /** The offending field's name (dotted path for nested fields). */\n field: string;\n /** Human-readable reason the field failed. */\n message: string;\n}\n\n/** The fixed, typed payload {@link BadRequest} ships. */\nexport interface BadRequestData {\n /** The fields that failed validation. */\n fields: FieldError[];\n}\n\n/** The fixed, typed payload {@link TooManyRequests} ships. */\nexport interface TooManyRequestsData {\n /** Seconds the caller should wait before retrying. */\n retryAfter: number;\n}\n\n/**\n * 429 — the caller has exceeded the rate limit. Carries a fixed typed payload:\n * `new TooManyRequests({ retryAfter: 30 })`. The shape is declared once in the\n * SDK (error-registry pre-seed) so codegen surfaces `error.data.retryAfter`\n * typed on the client — no per-project definition needed.\n */\nexport class TooManyRequests extends NamedHttpError {\n public declare readonly data: TooManyRequestsData;\n constructor(data: TooManyRequestsData, message?: string) {\n super(429, \"too_many_requests\", \"TooManyRequests\", message, undefined, data);\n }\n}\n"],"mappings":"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","names":["HTTP_ERROR_BRAND","Symbol","for","ENGINE_RAISED","markEngineRaised","e","isEngineRaised","isHttpError","err","status","error","errorDescription","HttpError","Error","data","name","undefined","toJSON","requestId","result","error_description","request_id","PalError","code","description","NamedHttpError","defaultCode","message","defaultMessage","spaced","replace","charAt","toUpperCase","slice","toLowerCase","BadRequest","Unauthorized","Forbidden","NotFound","Conflict","UniqueViolation","is","constraint","isRetryable","retryable","sqlstate","SerializationFailure","DeadlockDetected","TooManyRequests"]}
|