kernelcms 0.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.
@@ -0,0 +1,459 @@
1
+ // ../db-postgres/src/index.ts
2
+ import pg from "pg";
3
+ var { Pool } = pg;
4
+ var SQL_TYPE = {
5
+ text: "text",
6
+ integer: "integer",
7
+ real: "double precision",
8
+ boolean: "boolean",
9
+ timestamp: "timestamptz",
10
+ json: "jsonb"
11
+ };
12
+ function quote(ident) {
13
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(ident)) {
14
+ throw new Error(`Invalid SQL identifier: ${ident}`);
15
+ }
16
+ return `"${ident}"`;
17
+ }
18
+ var Params = class {
19
+ values = [];
20
+ next(value) {
21
+ this.values.push(value);
22
+ return `$${this.values.length}`;
23
+ }
24
+ };
25
+ var PostgresAdapter = class {
26
+ kind = "db";
27
+ name = "postgres";
28
+ contractVersion = "1.0";
29
+ capabilities = {
30
+ transactions: true,
31
+ joins: "application",
32
+ jsonQuery: false,
33
+ fullTextSearch: false,
34
+ returning: true
35
+ };
36
+ pool = null;
37
+ logger = null;
38
+ options;
39
+ tables = /* @__PURE__ */ new Map();
40
+ allowedCols = /* @__PURE__ */ new Map();
41
+ savepointSeq = 0;
42
+ constructor(options) {
43
+ this.options = options;
44
+ }
45
+ async init(ctx) {
46
+ this.logger = ctx.logger;
47
+ const url = this.options.url ?? process.env.DATABASE_URL;
48
+ if (!url && !this.options.pool) {
49
+ throw new Error(
50
+ "PostgreSQL adapter requires a connection string. Set DATABASE_URL or pass `url` to postgresAdapter()."
51
+ );
52
+ }
53
+ const ssl = this.options.ssl === true ? { rejectUnauthorized: false } : this.options.ssl;
54
+ const config = {
55
+ connectionString: url,
56
+ max: this.options.max ?? 10,
57
+ ...ssl === void 0 ? {} : { ssl },
58
+ ...this.options.pool
59
+ };
60
+ this.pool = new Pool(config);
61
+ const probe = await this.pool.connect();
62
+ probe.release();
63
+ this.logger.info("postgres connected");
64
+ }
65
+ requirePool() {
66
+ if (!this.pool) throw new Error("Postgres adapter not initialized. Did you call initKernel()?");
67
+ return this.pool;
68
+ }
69
+ table(name) {
70
+ const t = this.tables.get(name);
71
+ if (!t) throw new Error(`Unknown table "${name}". Run migrations first.`);
72
+ return t;
73
+ }
74
+ allowed(table) {
75
+ return this.allowedCols.get(table) ?? /* @__PURE__ */ new Set(["id"]);
76
+ }
77
+ async migrate(schema) {
78
+ const pool = this.requirePool();
79
+ const report = { createdTables: [], addedColumns: [], statements: [] };
80
+ for (const table of schema.tables) {
81
+ this.tables.set(table.table, table);
82
+ const allowed = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt"]);
83
+ for (const c of table.columns) allowed.add(c.name);
84
+ this.allowedCols.set(table.table, allowed);
85
+ const existing = await pool.query(
86
+ `SELECT column_name FROM information_schema.columns
87
+ WHERE table_schema = current_schema() AND table_name = $1`,
88
+ [table.table]
89
+ );
90
+ if (existing.rows.length === 0) {
91
+ const sql = this.createTableSql(table);
92
+ await pool.query(sql);
93
+ report.createdTables.push(table.table);
94
+ report.statements.push(sql);
95
+ } else {
96
+ const have = new Set(existing.rows.map((r) => String(r.column_name)));
97
+ for (const col of table.columns) {
98
+ if (!have.has(col.name)) {
99
+ const sql = `ALTER TABLE ${quote(table.table)} ADD COLUMN ${this.columnSql(col)};`;
100
+ await pool.query(sql);
101
+ report.addedColumns.push(`${table.table}.${col.name}`);
102
+ report.statements.push(sql);
103
+ }
104
+ }
105
+ }
106
+ for (const col of table.columns) {
107
+ if (col.indexed && !col.unique) {
108
+ const idx = `idx_${table.table}_${col.name}`;
109
+ await pool.query(
110
+ `CREATE INDEX IF NOT EXISTS ${quote(idx)} ON ${quote(table.table)} (${quote(col.name)});`
111
+ );
112
+ }
113
+ }
114
+ }
115
+ this.logger?.info(
116
+ `migrated: ${report.createdTables.length} table(s) created, ${report.addedColumns.length} column(s) added`
117
+ );
118
+ return report;
119
+ }
120
+ columnSql(col) {
121
+ const parts = [quote(col.name), SQL_TYPE[col.type]];
122
+ if (col.unique) parts.push("UNIQUE");
123
+ return parts.join(" ");
124
+ }
125
+ createTableSql(table) {
126
+ const cols = [`${quote("id")} text PRIMARY KEY`];
127
+ for (const col of table.columns) cols.push(this.columnSql(col));
128
+ if (table.timestamps) {
129
+ cols.push(`${quote("createdAt")} timestamptz`);
130
+ cols.push(`${quote("updatedAt")} timestamptz`);
131
+ }
132
+ return `CREATE TABLE IF NOT EXISTS ${quote(table.table)} (
133
+ ${cols.join(",\n ")}
134
+ );`;
135
+ }
136
+ // -- value codecs ---------------------------------------------------------
137
+ encode(col, value) {
138
+ if (value === void 0 || value === null) return null;
139
+ switch (col.type) {
140
+ case "boolean":
141
+ return Boolean(value);
142
+ case "json":
143
+ return JSON.stringify(value);
144
+ case "integer":
145
+ return Math.trunc(Number(value));
146
+ case "real":
147
+ return Number(value);
148
+ case "timestamp":
149
+ return value instanceof Date ? value.toISOString() : String(value);
150
+ default:
151
+ return String(value);
152
+ }
153
+ }
154
+ decode(col, value) {
155
+ if (value === void 0 || value === null) return null;
156
+ switch (col.type) {
157
+ case "boolean":
158
+ return Boolean(value);
159
+ case "json":
160
+ if (typeof value === "string") {
161
+ try {
162
+ return JSON.parse(value);
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
167
+ return value;
168
+ case "timestamp":
169
+ return value instanceof Date ? value.toISOString() : value;
170
+ default:
171
+ return value;
172
+ }
173
+ }
174
+ decodeRow(table, raw) {
175
+ const out = { id: raw.id };
176
+ if ("createdAt" in raw) out.createdAt = raw.createdAt instanceof Date ? raw.createdAt.toISOString() : raw.createdAt;
177
+ if ("updatedAt" in raw) out.updatedAt = raw.updatedAt instanceof Date ? raw.updatedAt.toISOString() : raw.updatedAt;
178
+ for (const col of table.columns) out[col.name] = this.decode(col, raw[col.name]);
179
+ return out;
180
+ }
181
+ colByName(table) {
182
+ const m = /* @__PURE__ */ new Map();
183
+ for (const c of table.columns) m.set(c.name, c);
184
+ return m;
185
+ }
186
+ // -- where compilation ----------------------------------------------------
187
+ buildWhere(where, table, cols, allowed, params) {
188
+ const clauses = [];
189
+ if (where.and?.length) {
190
+ const sub = where.and.map((w) => this.buildWhere(w, table, cols, allowed, params)).filter(Boolean);
191
+ if (sub.length) clauses.push(`(${sub.join(" AND ")})`);
192
+ }
193
+ if (where.or?.length) {
194
+ const sub = where.or.map((w) => this.buildWhere(w, table, cols, allowed, params)).filter(Boolean);
195
+ if (sub.length) clauses.push(`(${sub.join(" OR ")})`);
196
+ }
197
+ for (const [field, cond] of Object.entries(where)) {
198
+ if (field === "and" || field === "or" || cond === void 0) continue;
199
+ if (!allowed.has(field)) throw new Error(`Cannot filter on unknown field "${field}" of "${table.table}".`);
200
+ clauses.push(this.fieldClause(field, cond, cols.get(field), params));
201
+ }
202
+ return clauses.length ? clauses.join(" AND ") : "";
203
+ }
204
+ encodeOperand(col, operand) {
205
+ if (col) return this.encode(col, operand);
206
+ if (typeof operand === "boolean") return operand;
207
+ return operand;
208
+ }
209
+ fieldClause(field, cond, col, params) {
210
+ const id = quote(field);
211
+ const parts = [];
212
+ for (const [op, operand] of Object.entries(cond)) {
213
+ switch (op) {
214
+ case "equals":
215
+ if (operand === null) parts.push(`${id} IS NULL`);
216
+ else parts.push(`${id} = ${params.next(this.encodeOperand(col, operand))}`);
217
+ break;
218
+ case "not_equals":
219
+ if (operand === null) parts.push(`${id} IS NOT NULL`);
220
+ else parts.push(`(${id} <> ${params.next(this.encodeOperand(col, operand))} OR ${id} IS NULL)`);
221
+ break;
222
+ case "in":
223
+ case "not_in": {
224
+ const arr = Array.isArray(operand) ? operand : [operand];
225
+ if (arr.length === 0) {
226
+ parts.push(op === "in" ? "false" : "true");
227
+ break;
228
+ }
229
+ const ph = arr.map((v) => params.next(this.encodeOperand(col, v))).join(", ");
230
+ parts.push(`${id} ${op === "in" ? "IN" : "NOT IN"} (${ph})`);
231
+ break;
232
+ }
233
+ case "greater_than":
234
+ parts.push(`${id} > ${params.next(this.encodeOperand(col, operand))}`);
235
+ break;
236
+ case "greater_than_equal":
237
+ parts.push(`${id} >= ${params.next(this.encodeOperand(col, operand))}`);
238
+ break;
239
+ case "less_than":
240
+ parts.push(`${id} < ${params.next(this.encodeOperand(col, operand))}`);
241
+ break;
242
+ case "less_than_equal":
243
+ parts.push(`${id} <= ${params.next(this.encodeOperand(col, operand))}`);
244
+ break;
245
+ case "like":
246
+ case "contains": {
247
+ const literal = String(operand).replace(/[\\%_]/g, (ch) => `\\${ch}`);
248
+ parts.push(`${id}::text ILIKE ${params.next(`%${literal}%`)} ESCAPE '\\'`);
249
+ break;
250
+ }
251
+ case "exists":
252
+ parts.push(operand ? `${id} IS NOT NULL` : `${id} IS NULL`);
253
+ break;
254
+ default:
255
+ throw new Error(`Unsupported operator "${op}".`);
256
+ }
257
+ }
258
+ return parts.length ? `(${parts.join(" AND ")})` : "";
259
+ }
260
+ whereSql(table, where, params) {
261
+ if (!where) return "";
262
+ const clause = this.buildWhere(where, table, this.colByName(table), this.allowed(table.table), params);
263
+ return clause ? ` WHERE ${clause}` : "";
264
+ }
265
+ // -- CRUD (each takes the queryable so transactions reuse a bound client) --
266
+ async rawGet(q, table, id) {
267
+ const res = await q.query(`SELECT * FROM ${quote(table.table)} WHERE ${quote("id")} = $1`, [id]);
268
+ const row = res.rows[0];
269
+ return row ? this.decodeRow(table, row) : null;
270
+ }
271
+ async _findByID(q, args) {
272
+ return this.rawGet(q, this.table(args.collection), args.id);
273
+ }
274
+ async _find(q, args) {
275
+ const table = this.table(args.collection);
276
+ const allowed = this.allowed(table.table);
277
+ const countParams = new Params();
278
+ const whereForCount = this.whereSql(table, args.where, countParams);
279
+ const totalRes = await q.query(
280
+ `SELECT COUNT(*)::int AS c FROM ${quote(table.table)}${whereForCount}`,
281
+ countParams.values
282
+ );
283
+ const totalDocs = Number(totalRes.rows[0]?.c ?? 0);
284
+ const params = new Params();
285
+ const whereSql = this.whereSql(table, args.where, params);
286
+ const sortParts = (args.sort ?? []).filter((s) => allowed.has(s.field)).map((s) => `${quote(s.field)} ${s.direction === "desc" ? "DESC" : "ASC"}`);
287
+ const orderBy = sortParts.length ? ` ORDER BY ${sortParts.join(", ")}` : ` ORDER BY ${quote("id")} ASC`;
288
+ const limit = args.limit;
289
+ const offset = (args.page - 1) * limit;
290
+ const limitPh = params.next(limit);
291
+ const offsetPh = params.next(offset);
292
+ const res = await q.query(
293
+ `SELECT * FROM ${quote(table.table)}${whereSql}${orderBy} LIMIT ${limitPh} OFFSET ${offsetPh}`,
294
+ params.values
295
+ );
296
+ const docs = res.rows.map((r) => this.decodeRow(table, r));
297
+ const totalPages = limit > 0 ? Math.max(Math.ceil(totalDocs / limit), 1) : 1;
298
+ const page = args.page;
299
+ return {
300
+ docs,
301
+ totalDocs,
302
+ limit,
303
+ page,
304
+ totalPages,
305
+ hasPrevPage: page > 1,
306
+ hasNextPage: page < totalPages,
307
+ prevPage: page > 1 ? page - 1 : null,
308
+ nextPage: page < totalPages ? page + 1 : null,
309
+ pagingCounter: offset + 1
310
+ };
311
+ }
312
+ async _create(q, args) {
313
+ const table = this.table(args.collection);
314
+ const id = String(args.data.id);
315
+ const fields = [quote("id")];
316
+ const params = new Params();
317
+ const placeholders = [params.next(id)];
318
+ for (const col of table.columns) {
319
+ if (Object.prototype.hasOwnProperty.call(args.data, col.name)) {
320
+ fields.push(quote(col.name));
321
+ placeholders.push(params.next(this.encode(col, args.data[col.name])));
322
+ }
323
+ }
324
+ if (table.timestamps) {
325
+ fields.push(quote("createdAt"), quote("updatedAt"));
326
+ placeholders.push("now()", "now()");
327
+ }
328
+ const res = await q.query(
329
+ `INSERT INTO ${quote(table.table)} (${fields.join(", ")}) VALUES (${placeholders.join(", ")}) RETURNING *`,
330
+ params.values
331
+ );
332
+ const created = res.rows[0];
333
+ if (!created) throw new Error("Insert succeeded but returned no row.");
334
+ return this.decodeRow(table, created);
335
+ }
336
+ async _update(q, args) {
337
+ const table = this.table(args.collection);
338
+ const sets = [];
339
+ const params = new Params();
340
+ for (const col of table.columns) {
341
+ if (Object.prototype.hasOwnProperty.call(args.data, col.name)) {
342
+ sets.push(`${quote(col.name)} = ${params.next(this.encode(col, args.data[col.name]))}`);
343
+ }
344
+ }
345
+ if (table.timestamps) sets.push(`${quote("updatedAt")} = now()`);
346
+ if (sets.length === 0) return this.rawGet(q, table, args.id);
347
+ const idPh = params.next(args.id);
348
+ const res = await q.query(
349
+ `UPDATE ${quote(table.table)} SET ${sets.join(", ")} WHERE ${quote("id")} = ${idPh} RETURNING *`,
350
+ params.values
351
+ );
352
+ const row = res.rows[0];
353
+ return row ? this.decodeRow(table, row) : null;
354
+ }
355
+ async _delete(q, args) {
356
+ const table = this.table(args.collection);
357
+ const res = await q.query(`DELETE FROM ${quote(table.table)} WHERE ${quote("id")} = $1 RETURNING *`, [args.id]);
358
+ const row = res.rows[0];
359
+ return row ? this.decodeRow(table, row) : null;
360
+ }
361
+ async _count(q, args) {
362
+ const table = this.table(args.collection);
363
+ const params = new Params();
364
+ const whereSql = this.whereSql(table, args.where, params);
365
+ const res = await q.query(`SELECT COUNT(*)::int AS c FROM ${quote(table.table)}${whereSql}`, params.values);
366
+ return Number(res.rows[0]?.c ?? 0);
367
+ }
368
+ // -- public contract surface (pool-backed) --------------------------------
369
+ async findByID(args) {
370
+ return this._findByID(this.requirePool(), args);
371
+ }
372
+ async find(args) {
373
+ return this._find(this.requirePool(), args);
374
+ }
375
+ async create(args) {
376
+ return this._create(this.requirePool(), args);
377
+ }
378
+ async update(args) {
379
+ return this._update(this.requirePool(), args);
380
+ }
381
+ async delete(args) {
382
+ return this._delete(this.requirePool(), args);
383
+ }
384
+ async count(args) {
385
+ return this._count(this.requirePool(), args);
386
+ }
387
+ /** A transaction-scoped adapter bound to a single client (or savepoint). */
388
+ bound(client) {
389
+ const q = client;
390
+ return {
391
+ kind: this.kind,
392
+ name: this.name,
393
+ contractVersion: this.contractVersion,
394
+ capabilities: this.capabilities,
395
+ init: async () => {
396
+ },
397
+ migrate: () => {
398
+ throw new Error("migrate() is not available inside a transaction.");
399
+ },
400
+ find: (a) => this._find(q, a),
401
+ findByID: (a) => this._findByID(q, a),
402
+ create: (a) => this._create(q, a),
403
+ update: (a) => this._update(q, a),
404
+ delete: (a) => this._delete(q, a),
405
+ count: (a) => this._count(q, a),
406
+ transaction: (fn) => this.savepoint(client, fn),
407
+ health: async () => ({ status: "ok" }),
408
+ destroy: async () => {
409
+ }
410
+ };
411
+ }
412
+ async savepoint(client, fn) {
413
+ const name = `sp_${++this.savepointSeq}`;
414
+ await client.query(`SAVEPOINT ${name}`);
415
+ try {
416
+ const result = await fn(this.bound(client));
417
+ await client.query(`RELEASE SAVEPOINT ${name}`);
418
+ return result;
419
+ } catch (err) {
420
+ await client.query(`ROLLBACK TO SAVEPOINT ${name}`);
421
+ throw err;
422
+ }
423
+ }
424
+ async transaction(fn) {
425
+ const client = await this.requirePool().connect();
426
+ await client.query("BEGIN");
427
+ try {
428
+ const result = await fn(this.bound(client));
429
+ await client.query("COMMIT");
430
+ return result;
431
+ } catch (err) {
432
+ try {
433
+ await client.query("ROLLBACK");
434
+ } catch {
435
+ }
436
+ throw err;
437
+ } finally {
438
+ client.release();
439
+ }
440
+ }
441
+ async health() {
442
+ try {
443
+ await this.requirePool().query("SELECT 1");
444
+ return { status: "ok" };
445
+ } catch (err) {
446
+ return { status: "down", detail: err instanceof Error ? err.message : String(err) };
447
+ }
448
+ }
449
+ async destroy() {
450
+ await this.pool?.end();
451
+ this.pool = null;
452
+ }
453
+ };
454
+ function postgresAdapter(options = {}) {
455
+ return new PostgresAdapter(options);
456
+ }
457
+ export {
458
+ postgresAdapter
459
+ };
@@ -0,0 +1,27 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { g as AuthUser, K as Kernel } from './types-DMOX4FIP.js';
3
+ import { W as Where } from './index-BxvPeUO2.js';
4
+
5
+ interface HandlerOptions {
6
+ /** Shared secret; a request with `Authorization: Bearer <apiKey>` runs as a trusted system caller. */
7
+ apiKey?: string;
8
+ /** Resolve the authenticated user for a request (sessions, JWT, etc.). */
9
+ getUser?: (request: Request) => Promise<AuthUser | null> | AuthUser | null;
10
+ /** CORS: true reflects the request origin; an array allow-lists origins. */
11
+ cors?: boolean | string[];
12
+ }
13
+ type RequestHandler = (request: Request) => Promise<Response>;
14
+ declare function createRequestHandler(kernel: Kernel, options?: HandlerOptions): RequestHandler;
15
+ declare function parseWhere(params: URLSearchParams): Where | undefined;
16
+ declare function toNodeListener(handler: RequestHandler): (req: IncomingMessage, res: ServerResponse) => void;
17
+ interface ServeOptions extends HandlerOptions {
18
+ port?: number;
19
+ }
20
+ interface RunningServer {
21
+ url: string;
22
+ port: number;
23
+ close: () => Promise<void>;
24
+ }
25
+ declare function serve(kernel: Kernel, options?: ServeOptions): Promise<RunningServer>;
26
+
27
+ export { type HandlerOptions, type RunningServer, type ServeOptions, createRequestHandler, parseWhere, serve, toNodeListener };
package/dist/server.js ADDED
@@ -0,0 +1,13 @@
1
+ import {
2
+ createRequestHandler,
3
+ parseWhere,
4
+ serve,
5
+ toNodeListener
6
+ } from "./chunk-Z2RKB4LF.js";
7
+ import "./chunk-O5TO5JFA.js";
8
+ export {
9
+ createRequestHandler,
10
+ parseWhere,
11
+ serve,
12
+ toNodeListener
13
+ };
@@ -0,0 +1,14 @@
1
+ import { D as DatabaseAdapter } from './index-BxvPeUO2.js';
2
+
3
+ interface SQLiteAdapterOptions {
4
+ /** File path or ':memory:'. Accepts "file:./x.db" URLs too. Default ':memory:'. */
5
+ url?: string;
6
+ /** Set busy timeout in ms. Default 5000. */
7
+ busyTimeout?: number;
8
+ /** Use WAL journal mode for file databases. Default true. */
9
+ wal?: boolean;
10
+ }
11
+ /** Create a SQLite database adapter. */
12
+ declare function sqliteAdapter(options?: SQLiteAdapterOptions): DatabaseAdapter;
13
+
14
+ export { type SQLiteAdapterOptions, sqliteAdapter };