@momentumcms/db-drizzle 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.
package/index.cjs ADDED
@@ -0,0 +1,1886 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // libs/db-drizzle/src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ postgresAdapter: () => postgresAdapter,
34
+ sqliteAdapter: () => sqliteAdapter
35
+ });
36
+ module.exports = __toCommonJS(src_exports);
37
+
38
+ // libs/db-drizzle/src/lib/db-drizzle.ts
39
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"));
40
+ var import_node_crypto = require("node:crypto");
41
+ var import_node_fs = require("node:fs");
42
+ var import_node_path = require("node:path");
43
+
44
+ // libs/core/src/lib/collections/define-collection.ts
45
+ function defineCollection(config) {
46
+ const collection = {
47
+ timestamps: true,
48
+ // Enable timestamps by default
49
+ ...config
50
+ };
51
+ if (!collection.slug) {
52
+ throw new Error("Collection must have a slug");
53
+ }
54
+ if (!collection.fields || collection.fields.length === 0) {
55
+ throw new Error(`Collection "${collection.slug}" must have at least one field`);
56
+ }
57
+ if (!/^[a-z][a-z0-9-]*$/.test(collection.slug)) {
58
+ throw new Error(
59
+ `Collection slug "${collection.slug}" must be kebab-case (lowercase letters, numbers, and hyphens, starting with a letter)`
60
+ );
61
+ }
62
+ return collection;
63
+ }
64
+ function getSoftDeleteField(config) {
65
+ if (!config.softDelete)
66
+ return null;
67
+ if (config.softDelete === true)
68
+ return "deletedAt";
69
+ const sdConfig = config.softDelete;
70
+ return sdConfig.field ?? "deletedAt";
71
+ }
72
+
73
+ // libs/core/src/lib/fields/field.types.ts
74
+ var ReferentialIntegrityError = class extends Error {
75
+ constructor(table, constraint) {
76
+ super(`Cannot delete from "${table}": referenced by foreign key constraint "${constraint}"`);
77
+ this.name = "ReferentialIntegrityError";
78
+ this.table = table;
79
+ this.constraint = constraint;
80
+ }
81
+ };
82
+ function flattenDataFields(fields) {
83
+ const result = [];
84
+ for (const field of fields) {
85
+ if (field.type === "tabs") {
86
+ for (const tab of field.tabs) {
87
+ result.push(...flattenDataFields(tab.fields));
88
+ }
89
+ } else if (field.type === "collapsible" || field.type === "row") {
90
+ result.push(...flattenDataFields(field.fields));
91
+ } else {
92
+ result.push(field);
93
+ }
94
+ }
95
+ return result;
96
+ }
97
+
98
+ // libs/core/src/lib/fields/field-builders.ts
99
+ function text(name, options = {}) {
100
+ return {
101
+ name,
102
+ type: "text",
103
+ ...options
104
+ };
105
+ }
106
+ function number(name, options = {}) {
107
+ return {
108
+ name,
109
+ type: "number",
110
+ ...options
111
+ };
112
+ }
113
+ function json(name, options = {}) {
114
+ return {
115
+ name,
116
+ type: "json",
117
+ ...options
118
+ };
119
+ }
120
+
121
+ // libs/core/src/lib/collections/media.collection.ts
122
+ var MediaCollection = defineCollection({
123
+ slug: "media",
124
+ labels: {
125
+ singular: "Media",
126
+ plural: "Media"
127
+ },
128
+ admin: {
129
+ useAsTitle: "filename",
130
+ defaultColumns: ["filename", "mimeType", "filesize", "createdAt"]
131
+ },
132
+ fields: [
133
+ text("filename", {
134
+ required: true,
135
+ label: "Filename",
136
+ description: "Original filename of the uploaded file"
137
+ }),
138
+ text("mimeType", {
139
+ required: true,
140
+ label: "MIME Type",
141
+ description: "File MIME type (e.g., image/jpeg, application/pdf)"
142
+ }),
143
+ number("filesize", {
144
+ label: "File Size",
145
+ description: "File size in bytes"
146
+ }),
147
+ text("path", {
148
+ required: true,
149
+ label: "Storage Path",
150
+ description: "Path/key where the file is stored",
151
+ admin: {
152
+ hidden: true
153
+ }
154
+ }),
155
+ text("url", {
156
+ label: "URL",
157
+ description: "Public URL to access the file"
158
+ }),
159
+ text("alt", {
160
+ label: "Alt Text",
161
+ description: "Alternative text for accessibility"
162
+ }),
163
+ number("width", {
164
+ label: "Width",
165
+ description: "Image width in pixels (for images only)"
166
+ }),
167
+ number("height", {
168
+ label: "Height",
169
+ description: "Image height in pixels (for images only)"
170
+ }),
171
+ json("focalPoint", {
172
+ label: "Focal Point",
173
+ description: "Focal point coordinates for image cropping",
174
+ admin: {
175
+ hidden: true
176
+ }
177
+ })
178
+ ],
179
+ access: {
180
+ // Media is readable by anyone by default
181
+ read: () => true,
182
+ // Only authenticated users can create/update/delete
183
+ create: ({ req }) => !!req?.user,
184
+ update: ({ req }) => !!req?.user,
185
+ delete: ({ req }) => !!req?.user
186
+ }
187
+ });
188
+
189
+ // libs/db-drizzle/src/lib/db-drizzle.ts
190
+ function isRecord(value) {
191
+ return typeof value === "object" && value !== null;
192
+ }
193
+ function isDocumentStatus(value) {
194
+ return value === "draft" || value === "published";
195
+ }
196
+ function getStatusFromRow(row2) {
197
+ const status = row2["_status"];
198
+ if (isDocumentStatus(status)) {
199
+ return status;
200
+ }
201
+ return "draft";
202
+ }
203
+ function parseJsonToRecord(jsonString) {
204
+ try {
205
+ const parsed = JSON.parse(jsonString);
206
+ if (isRecord(parsed)) {
207
+ return parsed;
208
+ }
209
+ return {};
210
+ } catch {
211
+ return {};
212
+ }
213
+ }
214
+ function validateCollectionSlug(slug2) {
215
+ const validSlug = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
216
+ if (!validSlug.test(slug2)) {
217
+ throw new Error(
218
+ `Invalid collection slug: "${slug2}". Slugs must start with a letter or underscore and contain only alphanumeric characters, underscores, and hyphens.`
219
+ );
220
+ }
221
+ }
222
+ function validateColumnName(name) {
223
+ const validColumnName = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
224
+ if (!validColumnName.test(name)) {
225
+ throw new Error(`Invalid column name: "${name}"`);
226
+ }
227
+ }
228
+ var AsyncQueue = class {
229
+ constructor() {
230
+ this.queue = Promise.resolve();
231
+ }
232
+ /**
233
+ * Add an operation to the queue. Operations execute sequentially.
234
+ */
235
+ enqueue(operation) {
236
+ const result = this.queue.then(() => operation());
237
+ this.queue = result.catch(() => void 0);
238
+ return result;
239
+ }
240
+ };
241
+ function getColumnType(field) {
242
+ switch (field.type) {
243
+ case "text":
244
+ case "textarea":
245
+ case "richText":
246
+ case "email":
247
+ case "slug":
248
+ case "select":
249
+ return "TEXT";
250
+ case "number":
251
+ return "REAL";
252
+ case "checkbox":
253
+ return "INTEGER";
254
+ case "date":
255
+ return "TEXT";
256
+ case "relationship":
257
+ case "upload":
258
+ return "TEXT";
259
+ case "array":
260
+ case "group":
261
+ case "blocks":
262
+ case "json":
263
+ return "TEXT";
264
+ default:
265
+ return "TEXT";
266
+ }
267
+ }
268
+ function createTableSql(collection) {
269
+ const columns = [
270
+ "id TEXT PRIMARY KEY",
271
+ "createdAt TEXT NOT NULL",
272
+ "updatedAt TEXT NOT NULL"
273
+ ];
274
+ if (hasVersionDrafts(collection)) {
275
+ columns.push(`"_status" TEXT NOT NULL DEFAULT 'draft'`);
276
+ }
277
+ const softDeleteCol = getSoftDeleteField(collection);
278
+ if (softDeleteCol) {
279
+ validateColumnName(softDeleteCol);
280
+ columns.push(`"${softDeleteCol}" TEXT`);
281
+ }
282
+ const dataFields = flattenDataFields(collection.fields);
283
+ for (const field of dataFields) {
284
+ const colType = getColumnType(field);
285
+ const notNull = field.required ? " NOT NULL" : "";
286
+ columns.push(`"${field.name}" ${colType}${notNull}`);
287
+ }
288
+ const tableName = collection.dbName ?? collection.slug;
289
+ return `CREATE TABLE IF NOT EXISTS "${tableName}" (${columns.join(", ")})`;
290
+ }
291
+ function getTableName(collection) {
292
+ return collection.dbName ?? collection.slug;
293
+ }
294
+ function hasVersionDrafts(collection) {
295
+ const versions = collection.versions;
296
+ if (!versions)
297
+ return false;
298
+ if (typeof versions === "boolean")
299
+ return false;
300
+ return !!versions.drafts;
301
+ }
302
+ function createVersionTableSql(collection) {
303
+ if (!collection.versions) {
304
+ return null;
305
+ }
306
+ const baseTable = getTableName(collection);
307
+ const tableName = `${baseTable}_versions`;
308
+ return `
309
+ CREATE TABLE IF NOT EXISTS "${tableName}" (
310
+ "id" TEXT PRIMARY KEY NOT NULL,
311
+ "parent" TEXT NOT NULL,
312
+ "version" TEXT NOT NULL,
313
+ "_status" TEXT NOT NULL DEFAULT 'draft',
314
+ "autosave" INTEGER NOT NULL DEFAULT 0,
315
+ "publishedAt" TEXT,
316
+ "createdAt" TEXT NOT NULL,
317
+ "updatedAt" TEXT NOT NULL,
318
+ FOREIGN KEY ("parent") REFERENCES "${baseTable}"("id") ON DELETE CASCADE
319
+ );
320
+
321
+ CREATE INDEX IF NOT EXISTS "idx_${tableName}_parent" ON "${tableName}"("parent");
322
+ CREATE INDEX IF NOT EXISTS "idx_${tableName}_status" ON "${tableName}"("_status");
323
+ CREATE INDEX IF NOT EXISTS "idx_${tableName}_createdAt" ON "${tableName}"("createdAt");
324
+ `;
325
+ }
326
+ function ensureDirectoryExists(filePath) {
327
+ if (filePath === ":memory:") {
328
+ return;
329
+ }
330
+ const dir = (0, import_node_path.dirname)(filePath);
331
+ if (dir && dir !== "." && !(0, import_node_fs.existsSync)(dir)) {
332
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
333
+ }
334
+ }
335
+ var SEED_TRACKING_TABLE_SQL = `
336
+ CREATE TABLE IF NOT EXISTS "_momentum_seeds" (
337
+ "id" TEXT PRIMARY KEY NOT NULL,
338
+ "seedId" TEXT NOT NULL UNIQUE,
339
+ "collection" TEXT NOT NULL,
340
+ "documentId" TEXT NOT NULL,
341
+ "checksum" TEXT NOT NULL,
342
+ "createdAt" TEXT NOT NULL,
343
+ "updatedAt" TEXT NOT NULL
344
+ );
345
+
346
+ CREATE INDEX IF NOT EXISTS "idx_momentum_seeds_seedId" ON "_momentum_seeds"("seedId");
347
+ `;
348
+ function sqliteAdapter(options) {
349
+ ensureDirectoryExists(options.filename);
350
+ const sqlite = new import_better_sqlite3.default(options.filename);
351
+ sqlite.pragma("journal_mode = WAL");
352
+ const writeQueue = new AsyncQueue();
353
+ const tableNameMap = /* @__PURE__ */ new Map();
354
+ function resolveTableName(slug2) {
355
+ return tableNameMap.get(slug2) ?? slug2;
356
+ }
357
+ function findSync(collection, query) {
358
+ validateCollectionSlug(collection);
359
+ const limitValue = typeof query["limit"] === "number" ? query["limit"] : 100;
360
+ const pageValue = typeof query["page"] === "number" ? query["page"] : 1;
361
+ const offset = (pageValue - 1) * limitValue;
362
+ const whereClauses = [];
363
+ const whereValues = [];
364
+ const reservedParams = /* @__PURE__ */ new Set(["limit", "page", "sort", "order"]);
365
+ const validColumnName = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
366
+ for (const [key, value] of Object.entries(query)) {
367
+ if (reservedParams.has(key))
368
+ continue;
369
+ if (value === void 0)
370
+ continue;
371
+ if (!validColumnName.test(key)) {
372
+ throw new Error(`Invalid column name: ${key}`);
373
+ }
374
+ if (value === null) {
375
+ whereClauses.push(`"${key}" IS NULL`);
376
+ } else if (typeof value === "object" && value !== null && "$ne" in value && value["$ne"] === null) {
377
+ whereClauses.push(`"${key}" IS NOT NULL`);
378
+ } else {
379
+ whereClauses.push(`"${key}" = ?`);
380
+ whereValues.push(value);
381
+ }
382
+ }
383
+ const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
384
+ let rows;
385
+ if (limitValue === 0) {
386
+ const sql = `SELECT * FROM "${resolveTableName(collection)}" ${whereClause}`;
387
+ rows = sqlite.prepare(sql).all(...whereValues);
388
+ } else {
389
+ const sql = `SELECT * FROM "${resolveTableName(collection)}" ${whereClause} LIMIT ? OFFSET ?`;
390
+ rows = sqlite.prepare(sql).all(...whereValues, limitValue, offset);
391
+ }
392
+ return rows.filter(
393
+ (row2) => typeof row2 === "object" && row2 !== null
394
+ );
395
+ }
396
+ function findByIdSync(collection, id) {
397
+ validateCollectionSlug(collection);
398
+ const row2 = sqlite.prepare(`SELECT * FROM "${resolveTableName(collection)}" WHERE id = ?`).get(id);
399
+ return isRecord(row2) ? row2 : null;
400
+ }
401
+ function createSync(collection, data) {
402
+ validateCollectionSlug(collection);
403
+ const id = (0, import_node_crypto.randomUUID)();
404
+ const now = (/* @__PURE__ */ new Date()).toISOString();
405
+ const doc = { ...data, id, createdAt: now, updatedAt: now };
406
+ const columns = Object.keys(doc);
407
+ const placeholders = columns.map(() => "?").join(", ");
408
+ const values = columns.map((col) => {
409
+ const val = doc[col];
410
+ if (val === void 0)
411
+ return null;
412
+ if (typeof val === "boolean")
413
+ return val ? 1 : 0;
414
+ if (typeof val === "object" && val !== null)
415
+ return JSON.stringify(val);
416
+ return val;
417
+ });
418
+ const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
419
+ sqlite.prepare(
420
+ `INSERT INTO "${resolveTableName(collection)}" (${quotedColumns}) VALUES (${placeholders})`
421
+ ).run(...values);
422
+ return doc;
423
+ }
424
+ function updateSync(collection, id, data) {
425
+ validateCollectionSlug(collection);
426
+ const now = (/* @__PURE__ */ new Date()).toISOString();
427
+ const updateData = { ...data, updatedAt: now };
428
+ delete updateData["id"];
429
+ delete updateData["createdAt"];
430
+ const setClauses = [];
431
+ const values = [];
432
+ for (const [key, value] of Object.entries(updateData)) {
433
+ setClauses.push(`"${key}" = ?`);
434
+ if (value === void 0)
435
+ values.push(null);
436
+ else if (typeof value === "boolean")
437
+ values.push(value ? 1 : 0);
438
+ else if (typeof value === "object" && value !== null)
439
+ values.push(JSON.stringify(value));
440
+ else
441
+ values.push(value);
442
+ }
443
+ values.push(id);
444
+ sqlite.prepare(`UPDATE "${resolveTableName(collection)}" SET ${setClauses.join(", ")} WHERE id = ?`).run(...values);
445
+ const updated = sqlite.prepare(`SELECT * FROM "${resolveTableName(collection)}" WHERE id = ?`).get(id);
446
+ if (!isRecord(updated)) {
447
+ throw new Error("Failed to fetch updated document");
448
+ }
449
+ return updated;
450
+ }
451
+ function deleteSync(collection, id) {
452
+ validateCollectionSlug(collection);
453
+ const result = sqlite.prepare(`DELETE FROM "${resolveTableName(collection)}" WHERE id = ?`).run(id);
454
+ return result.changes > 0;
455
+ }
456
+ function softDeleteSync(collection, id, field = "deletedAt") {
457
+ validateCollectionSlug(collection);
458
+ validateColumnName(field);
459
+ const now = (/* @__PURE__ */ new Date()).toISOString();
460
+ const result = sqlite.prepare(
461
+ `UPDATE "${resolveTableName(collection)}" SET "${field}" = ?, "updatedAt" = ? WHERE "id" = ?`
462
+ ).run(now, now, id);
463
+ return result.changes > 0;
464
+ }
465
+ function restoreSync(collection, id, field = "deletedAt") {
466
+ validateCollectionSlug(collection);
467
+ validateColumnName(field);
468
+ const now = (/* @__PURE__ */ new Date()).toISOString();
469
+ sqlite.prepare(
470
+ `UPDATE "${resolveTableName(collection)}" SET "${field}" = NULL, "updatedAt" = ? WHERE "id" = ?`
471
+ ).run(now, id);
472
+ const row2 = sqlite.prepare(`SELECT * FROM "${resolveTableName(collection)}" WHERE "id" = ?`).get(id);
473
+ if (!isRecord(row2)) {
474
+ throw new Error("Failed to fetch restored document");
475
+ }
476
+ return row2;
477
+ }
478
+ function createVersionSync(collection, parentId, data, options2) {
479
+ validateCollectionSlug(collection);
480
+ const id = (0, import_node_crypto.randomUUID)();
481
+ const now = (/* @__PURE__ */ new Date()).toISOString();
482
+ const tableName = `${resolveTableName(collection)}_versions`;
483
+ const status = options2?.status ?? "draft";
484
+ const autosave = options2?.autosave ? 1 : 0;
485
+ const doc = {
486
+ id,
487
+ parent: parentId,
488
+ version: JSON.stringify(data),
489
+ _status: status,
490
+ autosave: autosave === 1,
491
+ publishedAt: status === "published" ? now : void 0,
492
+ createdAt: now,
493
+ updatedAt: now
494
+ };
495
+ sqlite.prepare(
496
+ `INSERT INTO "${tableName}" ("id", "parent", "version", "_status", "autosave", "publishedAt", "createdAt", "updatedAt")
497
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
498
+ ).run(
499
+ doc.id,
500
+ doc.parent,
501
+ doc.version,
502
+ doc._status,
503
+ autosave,
504
+ doc.publishedAt ?? null,
505
+ doc.createdAt,
506
+ doc.updatedAt
507
+ );
508
+ return doc;
509
+ }
510
+ function findVersionsSync(collection, parentId, options2) {
511
+ validateCollectionSlug(collection);
512
+ const tableName = `${resolveTableName(collection)}_versions`;
513
+ const limit = options2?.limit ?? 10;
514
+ const page = options2?.page ?? 1;
515
+ const offset = (page - 1) * limit;
516
+ const sortOrder = options2?.sort === "asc" ? "ASC" : "DESC";
517
+ const whereClauses = ['"parent" = ?'];
518
+ const whereValues = [parentId];
519
+ if (!options2?.includeAutosave)
520
+ whereClauses.push('"autosave" = 0');
521
+ if (options2?.status) {
522
+ whereClauses.push('"_status" = ?');
523
+ whereValues.push(options2.status);
524
+ }
525
+ const whereClause = whereClauses.join(" AND ");
526
+ const sql = `SELECT * FROM "${tableName}" WHERE ${whereClause} ORDER BY "createdAt" ${sortOrder} LIMIT ? OFFSET ?`;
527
+ const rows = sqlite.prepare(sql).all(...whereValues, limit, offset);
528
+ return rows.filter(isRecord).map((row2) => ({
529
+ id: String(row2["id"]),
530
+ parent: String(row2["parent"]),
531
+ version: String(row2["version"]),
532
+ _status: getStatusFromRow(row2),
533
+ autosave: row2["autosave"] === 1,
534
+ publishedAt: row2["publishedAt"] ? String(row2["publishedAt"]) : void 0,
535
+ createdAt: String(row2["createdAt"]),
536
+ updatedAt: String(row2["updatedAt"])
537
+ }));
538
+ }
539
+ function findVersionByIdSync(collection, versionId) {
540
+ validateCollectionSlug(collection);
541
+ const tableName = `${resolveTableName(collection)}_versions`;
542
+ const row2 = sqlite.prepare(`SELECT * FROM "${tableName}" WHERE "id" = ?`).get(versionId);
543
+ if (!isRecord(row2))
544
+ return null;
545
+ return {
546
+ id: String(row2["id"]),
547
+ parent: String(row2["parent"]),
548
+ version: String(row2["version"]),
549
+ _status: getStatusFromRow(row2),
550
+ autosave: row2["autosave"] === 1,
551
+ publishedAt: row2["publishedAt"] ? String(row2["publishedAt"]) : void 0,
552
+ createdAt: String(row2["createdAt"]),
553
+ updatedAt: String(row2["updatedAt"])
554
+ };
555
+ }
556
+ function restoreVersionSync(collection, versionId) {
557
+ validateCollectionSlug(collection);
558
+ const tableName = `${resolveTableName(collection)}_versions`;
559
+ const versionRow = sqlite.prepare(`SELECT * FROM "${tableName}" WHERE "id" = ?`).get(versionId);
560
+ if (!isRecord(versionRow)) {
561
+ throw new Error(
562
+ `Version "${versionId}" not found in collection "${resolveTableName(collection)}"`
563
+ );
564
+ }
565
+ const parentId = String(versionRow["parent"]);
566
+ const versionData = parseJsonToRecord(String(versionRow["version"]));
567
+ const originalStatus = getStatusFromRow(versionRow);
568
+ const now = (/* @__PURE__ */ new Date()).toISOString();
569
+ const setClauses = [];
570
+ const values = [];
571
+ const validColumnName = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
572
+ for (const [key, value] of Object.entries(versionData)) {
573
+ if (key === "id" || key === "createdAt")
574
+ continue;
575
+ if (!validColumnName.test(key)) {
576
+ throw new Error(`Invalid column name in version data: ${key}`);
577
+ }
578
+ setClauses.push(`"${key}" = ?`);
579
+ if (value === void 0)
580
+ values.push(null);
581
+ else if (typeof value === "boolean")
582
+ values.push(value ? 1 : 0);
583
+ else if (typeof value === "object" && value !== null)
584
+ values.push(JSON.stringify(value));
585
+ else
586
+ values.push(value);
587
+ }
588
+ setClauses.push('"updatedAt" = ?');
589
+ values.push(now);
590
+ values.push(parentId);
591
+ sqlite.prepare(
592
+ `UPDATE "${resolveTableName(collection)}" SET ${setClauses.join(", ")} WHERE "id" = ?`
593
+ ).run(...values);
594
+ const newVersionId = (0, import_node_crypto.randomUUID)();
595
+ sqlite.prepare(
596
+ `INSERT INTO "${tableName}" ("id", "parent", "version", "_status", "autosave", "createdAt", "updatedAt")
597
+ VALUES (?, ?, ?, ?, 0, ?, ?)`
598
+ ).run(newVersionId, parentId, String(versionRow["version"]), originalStatus, now, now);
599
+ const updated = sqlite.prepare(`SELECT * FROM "${resolveTableName(collection)}" WHERE "id" = ?`).get(parentId);
600
+ if (!isRecord(updated)) {
601
+ throw new Error("Failed to fetch restored document");
602
+ }
603
+ return updated;
604
+ }
605
+ function deleteVersionsSync(collection, parentId, keepLatest) {
606
+ validateCollectionSlug(collection);
607
+ const tableName = `${resolveTableName(collection)}_versions`;
608
+ if (keepLatest === void 0 || keepLatest <= 0) {
609
+ const result2 = sqlite.prepare(`DELETE FROM "${tableName}" WHERE "parent" = ?`).run(parentId);
610
+ return result2.changes;
611
+ }
612
+ const keepIds = sqlite.prepare(
613
+ `SELECT "id" FROM "${tableName}" WHERE "parent" = ? ORDER BY "createdAt" DESC LIMIT ?`
614
+ ).all(parentId, keepLatest).filter(isRecord).map((row2) => String(row2["id"]));
615
+ if (keepIds.length === 0)
616
+ return 0;
617
+ const placeholders = keepIds.map(() => "?").join(", ");
618
+ const result = sqlite.prepare(`DELETE FROM "${tableName}" WHERE "parent" = ? AND "id" NOT IN (${placeholders})`).run(parentId, ...keepIds);
619
+ return result.changes;
620
+ }
621
+ function countVersionsSync(collection, parentId, options2) {
622
+ validateCollectionSlug(collection);
623
+ const tableName = `${resolveTableName(collection)}_versions`;
624
+ const whereClauses = ['"parent" = ?'];
625
+ const whereValues = [parentId];
626
+ if (!options2?.includeAutosave)
627
+ whereClauses.push('"autosave" = 0');
628
+ if (options2?.status) {
629
+ whereClauses.push('"_status" = ?');
630
+ whereValues.push(options2.status);
631
+ }
632
+ const whereClause = whereClauses.join(" AND ");
633
+ const result = sqlite.prepare(`SELECT COUNT(*) as count FROM "${tableName}" WHERE ${whereClause}`).get(...whereValues);
634
+ if (isRecord(result) && typeof result["count"] === "number")
635
+ return result["count"];
636
+ return 0;
637
+ }
638
+ function updateStatusSync(collection, id, status) {
639
+ validateCollectionSlug(collection);
640
+ const now = (/* @__PURE__ */ new Date()).toISOString();
641
+ sqlite.prepare(
642
+ `UPDATE "${resolveTableName(collection)}" SET "_status" = ?, "updatedAt" = ? WHERE "id" = ?`
643
+ ).run(status, now, id);
644
+ }
645
+ return {
646
+ getRawDatabase() {
647
+ return sqlite;
648
+ },
649
+ async initialize(collections) {
650
+ for (const collection of collections) {
651
+ const tbl = getTableName(collection);
652
+ tableNameMap.set(collection.slug, tbl);
653
+ }
654
+ sqlite.exec(SEED_TRACKING_TABLE_SQL);
655
+ for (const collection of collections) {
656
+ const tbl = getTableName(collection);
657
+ sqlite.exec(createTableSql(collection));
658
+ const versionTableSql = createVersionTableSql(collection);
659
+ if (versionTableSql)
660
+ sqlite.exec(versionTableSql);
661
+ const sdField = getSoftDeleteField(collection);
662
+ if (sdField) {
663
+ try {
664
+ sqlite.exec(`ALTER TABLE "${tbl}" ADD COLUMN "${sdField}" TEXT`);
665
+ } catch {
666
+ }
667
+ sqlite.exec(
668
+ `CREATE INDEX IF NOT EXISTS "idx_${tbl}_${sdField}" ON "${tbl}"("${sdField}")`
669
+ );
670
+ }
671
+ if (collection.indexes) {
672
+ for (const idx of collection.indexes) {
673
+ for (const col of idx.columns) {
674
+ validateColumnName(col);
675
+ }
676
+ const idxName = idx.name ?? `idx_${tbl}_${idx.columns.join("_")}`;
677
+ const uniqueStr = idx.unique ? "UNIQUE " : "";
678
+ const colList = idx.columns.map((c) => `"${c}"`).join(", ");
679
+ sqlite.exec(
680
+ `CREATE ${uniqueStr}INDEX IF NOT EXISTS "${idxName}" ON "${tbl}"(${colList})`
681
+ );
682
+ }
683
+ }
684
+ }
685
+ },
686
+ async find(collection, query) {
687
+ return findSync(collection, query);
688
+ },
689
+ async findById(collection, id) {
690
+ return findByIdSync(collection, id);
691
+ },
692
+ async create(collection, data) {
693
+ return writeQueue.enqueue(() => createSync(collection, data));
694
+ },
695
+ async update(collection, id, data) {
696
+ return writeQueue.enqueue(() => updateSync(collection, id, data));
697
+ },
698
+ async delete(collection, id) {
699
+ return writeQueue.enqueue(() => deleteSync(collection, id));
700
+ },
701
+ async softDelete(collection, id, field) {
702
+ return writeQueue.enqueue(() => softDeleteSync(collection, id, field));
703
+ },
704
+ async restore(collection, id, field) {
705
+ return writeQueue.enqueue(() => restoreSync(collection, id, field));
706
+ },
707
+ async createVersion(collection, parentId, data, options2) {
708
+ return writeQueue.enqueue(() => createVersionSync(collection, parentId, data, options2));
709
+ },
710
+ async findVersions(collection, parentId, options2) {
711
+ return findVersionsSync(collection, parentId, options2);
712
+ },
713
+ async findVersionById(collection, versionId) {
714
+ return findVersionByIdSync(collection, versionId);
715
+ },
716
+ async restoreVersion(collection, versionId) {
717
+ return writeQueue.enqueue(() => restoreVersionSync(collection, versionId));
718
+ },
719
+ async deleteVersions(collection, parentId, keepLatest) {
720
+ return writeQueue.enqueue(() => deleteVersionsSync(collection, parentId, keepLatest));
721
+ },
722
+ async countVersions(collection, parentId, options2) {
723
+ return countVersionsSync(collection, parentId, options2);
724
+ },
725
+ async updateStatus(collection, id, status) {
726
+ return writeQueue.enqueue(() => updateStatusSync(collection, id, status));
727
+ },
728
+ // ============================================
729
+ // Globals Support
730
+ // ============================================
731
+ async initializeGlobals(_globals) {
732
+ sqlite.exec(`
733
+ CREATE TABLE IF NOT EXISTS "_globals" (
734
+ "slug" TEXT PRIMARY KEY,
735
+ "data" TEXT NOT NULL DEFAULT '{}',
736
+ "createdAt" TEXT NOT NULL DEFAULT (datetime('now')),
737
+ "updatedAt" TEXT NOT NULL DEFAULT (datetime('now'))
738
+ )
739
+ `);
740
+ },
741
+ async findGlobal(slug2) {
742
+ const row2 = sqlite.prepare('SELECT * FROM "_globals" WHERE "slug" = ?').get(slug2);
743
+ if (!row2 || !isRecord(row2))
744
+ return null;
745
+ const data = JSON.parse(String(row2["data"]));
746
+ return {
747
+ ...data,
748
+ slug: row2["slug"],
749
+ createdAt: row2["createdAt"],
750
+ updatedAt: row2["updatedAt"]
751
+ };
752
+ },
753
+ async updateGlobal(slug2, data) {
754
+ return writeQueue.enqueue(() => {
755
+ const now = (/* @__PURE__ */ new Date()).toISOString();
756
+ const jsonData = JSON.stringify(data);
757
+ sqlite.prepare(
758
+ `
759
+ INSERT INTO "_globals" ("slug", "data", "createdAt", "updatedAt")
760
+ VALUES (?, ?, ?, ?)
761
+ ON CONFLICT ("slug") DO UPDATE SET "data" = excluded."data", "updatedAt" = excluded."updatedAt"
762
+ `
763
+ ).run(slug2, jsonData, now, now);
764
+ const row2 = sqlite.prepare('SELECT * FROM "_globals" WHERE "slug" = ?').get(slug2);
765
+ if (!row2 || !isRecord(row2)) {
766
+ return { slug: slug2, ...data, createdAt: now, updatedAt: now };
767
+ }
768
+ const returned = JSON.parse(String(row2["data"]));
769
+ return {
770
+ ...returned,
771
+ slug: row2["slug"],
772
+ createdAt: row2["createdAt"],
773
+ updatedAt: row2["updatedAt"]
774
+ };
775
+ });
776
+ },
777
+ async transaction(callback) {
778
+ return writeQueue.enqueue(async () => {
779
+ sqlite.exec("BEGIN IMMEDIATE");
780
+ try {
781
+ const txAdapter = {
782
+ async find(c, q) {
783
+ return findSync(c, q);
784
+ },
785
+ async findById(c, id) {
786
+ return findByIdSync(c, id);
787
+ },
788
+ async create(c, d) {
789
+ return createSync(c, d);
790
+ },
791
+ async update(c, id, d) {
792
+ return updateSync(c, id, d);
793
+ },
794
+ async delete(c, id) {
795
+ return deleteSync(c, id);
796
+ },
797
+ async softDelete(c, id, f) {
798
+ return softDeleteSync(c, id, f);
799
+ },
800
+ async restore(c, id, f) {
801
+ return restoreSync(c, id, f);
802
+ },
803
+ async createVersion(c, pid, d, o) {
804
+ return createVersionSync(c, pid, d, o);
805
+ },
806
+ async findVersions(c, pid, o) {
807
+ return findVersionsSync(c, pid, o);
808
+ },
809
+ async findVersionById(c, vid) {
810
+ return findVersionByIdSync(c, vid);
811
+ },
812
+ async restoreVersion(c, vid) {
813
+ return restoreVersionSync(c, vid);
814
+ },
815
+ async deleteVersions(c, pid, k) {
816
+ return deleteVersionsSync(c, pid, k);
817
+ },
818
+ async countVersions(c, pid, o) {
819
+ return countVersionsSync(c, pid, o);
820
+ },
821
+ async updateStatus(c, id, s) {
822
+ updateStatusSync(c, id, s);
823
+ }
824
+ };
825
+ const result = await callback(txAdapter);
826
+ sqlite.exec("COMMIT");
827
+ return result;
828
+ } catch (error) {
829
+ sqlite.exec("ROLLBACK");
830
+ throw error;
831
+ }
832
+ });
833
+ }
834
+ };
835
+ }
836
+
837
+ // libs/db-drizzle/src/lib/db-postgres.ts
838
+ var import_pg = require("pg");
839
+ var import_node_crypto2 = require("node:crypto");
840
+
841
+ // libs/logger/src/lib/log-level.ts
842
+ var LOG_LEVEL_VALUES = {
843
+ debug: 0,
844
+ info: 1,
845
+ warn: 2,
846
+ error: 3,
847
+ fatal: 4,
848
+ silent: 5
849
+ };
850
+ function shouldLog(messageLevel, configuredLevel) {
851
+ return LOG_LEVEL_VALUES[messageLevel] >= LOG_LEVEL_VALUES[configuredLevel];
852
+ }
853
+
854
+ // libs/logger/src/lib/ansi-colors.ts
855
+ var ANSI = {
856
+ reset: "\x1B[0m",
857
+ bold: "\x1B[1m",
858
+ dim: "\x1B[2m",
859
+ // Foreground colors
860
+ red: "\x1B[31m",
861
+ green: "\x1B[32m",
862
+ yellow: "\x1B[33m",
863
+ blue: "\x1B[34m",
864
+ magenta: "\x1B[35m",
865
+ cyan: "\x1B[36m",
866
+ white: "\x1B[37m",
867
+ gray: "\x1B[90m",
868
+ // Background colors
869
+ bgRed: "\x1B[41m",
870
+ bgYellow: "\x1B[43m"
871
+ };
872
+ function colorize(text2, ...codes) {
873
+ if (codes.length === 0)
874
+ return text2;
875
+ return `${codes.join("")}${text2}${ANSI.reset}`;
876
+ }
877
+ function supportsColor() {
878
+ if (process.env["FORCE_COLOR"] === "1")
879
+ return true;
880
+ if (process.env["NO_COLOR"] !== void 0)
881
+ return false;
882
+ if (process.env["TERM"] === "dumb")
883
+ return false;
884
+ return process.stdout.isTTY === true;
885
+ }
886
+
887
+ // libs/logger/src/lib/formatters.ts
888
+ var LEVEL_COLORS = {
889
+ debug: [ANSI.dim, ANSI.gray],
890
+ info: [ANSI.cyan],
891
+ warn: [ANSI.yellow],
892
+ error: [ANSI.red],
893
+ fatal: [ANSI.bold, ANSI.white, ANSI.bgRed]
894
+ };
895
+ function padLevel(level) {
896
+ return level.toUpperCase().padEnd(5);
897
+ }
898
+ function formatTimestamp(date2) {
899
+ const y = date2.getFullYear();
900
+ const mo = String(date2.getMonth() + 1).padStart(2, "0");
901
+ const d = String(date2.getDate()).padStart(2, "0");
902
+ const h = String(date2.getHours()).padStart(2, "0");
903
+ const mi = String(date2.getMinutes()).padStart(2, "0");
904
+ const s = String(date2.getSeconds()).padStart(2, "0");
905
+ const ms = String(date2.getMilliseconds()).padStart(3, "0");
906
+ return `${y}-${mo}-${d} ${h}:${mi}:${s}.${ms}`;
907
+ }
908
+ function formatData(data) {
909
+ const entries = Object.entries(data);
910
+ if (entries.length === 0)
911
+ return "";
912
+ return " " + entries.map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
913
+ }
914
+ function prettyFormatter(entry) {
915
+ const useColor = supportsColor();
916
+ const level = entry.level;
917
+ const ts = formatTimestamp(entry.timestamp);
918
+ const levelStr = padLevel(entry.level);
919
+ const ctx = `[${entry.context}]`;
920
+ const msg = entry.message;
921
+ const enrichmentStr = entry.enrichments ? formatData(entry.enrichments) : "";
922
+ const dataStr = entry.data ? formatData(entry.data) : "";
923
+ const extra = `${enrichmentStr}${dataStr}`;
924
+ if (useColor) {
925
+ const colors = LEVEL_COLORS[level];
926
+ const coloredLevel = colorize(levelStr, ...colors);
927
+ const coloredCtx = colorize(ctx, ANSI.magenta);
928
+ const coloredTs = colorize(ts, ANSI.gray);
929
+ return `${coloredTs} ${coloredLevel} ${coloredCtx} ${msg}${extra}
930
+ `;
931
+ }
932
+ return `${ts} ${levelStr} ${ctx} ${msg}${extra}
933
+ `;
934
+ }
935
+ function jsonFormatter(entry) {
936
+ const output = {
937
+ timestamp: entry.timestamp.toISOString(),
938
+ level: entry.level,
939
+ context: entry.context,
940
+ message: entry.message
941
+ };
942
+ if (entry.enrichments && Object.keys(entry.enrichments).length > 0) {
943
+ Object.assign(output, entry.enrichments);
944
+ }
945
+ if (entry.data && Object.keys(entry.data).length > 0) {
946
+ output["data"] = entry.data;
947
+ }
948
+ return JSON.stringify(output) + "\n";
949
+ }
950
+
951
+ // libs/logger/src/lib/logger-config.types.ts
952
+ function resolveLoggingConfig(config) {
953
+ return {
954
+ level: config?.level ?? "info",
955
+ format: config?.format ?? "pretty",
956
+ timestamps: config?.timestamps ?? true,
957
+ output: config?.output ?? ((msg) => {
958
+ process.stdout.write(msg);
959
+ }),
960
+ errorOutput: config?.errorOutput ?? ((msg) => {
961
+ process.stderr.write(msg);
962
+ })
963
+ };
964
+ }
965
+
966
+ // libs/logger/src/lib/logger.ts
967
+ var ERROR_LEVELS = /* @__PURE__ */ new Set(["warn", "error", "fatal"]);
968
+ var MomentumLogger = class _MomentumLogger {
969
+ static {
970
+ this.enrichers = [];
971
+ }
972
+ constructor(context, config) {
973
+ this.context = context;
974
+ this.config = isResolvedConfig(config) ? config : resolveLoggingConfig(config);
975
+ this.formatter = this.config.format === "json" ? jsonFormatter : prettyFormatter;
976
+ }
977
+ debug(message, data) {
978
+ this.log("debug", message, data);
979
+ }
980
+ info(message, data) {
981
+ this.log("info", message, data);
982
+ }
983
+ warn(message, data) {
984
+ this.log("warn", message, data);
985
+ }
986
+ error(message, data) {
987
+ this.log("error", message, data);
988
+ }
989
+ fatal(message, data) {
990
+ this.log("fatal", message, data);
991
+ }
992
+ /**
993
+ * Creates a child logger with a sub-context.
994
+ * e.g., `Momentum:DB` → `Momentum:DB:Migrate`
995
+ */
996
+ child(subContext) {
997
+ return new _MomentumLogger(`${this.context}:${subContext}`, this.config);
998
+ }
999
+ /**
1000
+ * Registers a global enricher that adds extra fields to all log entries.
1001
+ */
1002
+ static registerEnricher(enricher) {
1003
+ _MomentumLogger.enrichers.push(enricher);
1004
+ }
1005
+ /**
1006
+ * Removes a previously registered enricher.
1007
+ */
1008
+ static removeEnricher(enricher) {
1009
+ const index = _MomentumLogger.enrichers.indexOf(enricher);
1010
+ if (index >= 0) {
1011
+ _MomentumLogger.enrichers.splice(index, 1);
1012
+ }
1013
+ }
1014
+ /**
1015
+ * Clears all registered enrichers. Primarily for testing.
1016
+ */
1017
+ static clearEnrichers() {
1018
+ _MomentumLogger.enrichers.length = 0;
1019
+ }
1020
+ log(level, message, data) {
1021
+ if (!shouldLog(level, this.config.level))
1022
+ return;
1023
+ const enrichments = this.collectEnrichments();
1024
+ const entry = {
1025
+ timestamp: /* @__PURE__ */ new Date(),
1026
+ level,
1027
+ context: this.context,
1028
+ message,
1029
+ data,
1030
+ enrichments: Object.keys(enrichments).length > 0 ? enrichments : void 0
1031
+ };
1032
+ const formatted = this.formatter(entry);
1033
+ if (ERROR_LEVELS.has(level)) {
1034
+ this.config.errorOutput(formatted);
1035
+ } else {
1036
+ this.config.output(formatted);
1037
+ }
1038
+ }
1039
+ collectEnrichments() {
1040
+ const result = {};
1041
+ for (const enricher of _MomentumLogger.enrichers) {
1042
+ Object.assign(result, enricher.enrich());
1043
+ }
1044
+ return result;
1045
+ }
1046
+ };
1047
+ function isResolvedConfig(config) {
1048
+ if (!config)
1049
+ return false;
1050
+ return typeof config.level === "string" && typeof config.format === "string" && typeof config.timestamps === "boolean" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- type guard narrows union
1051
+ typeof config.output === "function" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- type guard narrows union
1052
+ typeof config.errorOutput === "function";
1053
+ }
1054
+
1055
+ // libs/logger/src/lib/logger-singleton.ts
1056
+ var loggerInstance = null;
1057
+ var ROOT_CONTEXT = "Momentum";
1058
+ function getMomentumLogger() {
1059
+ if (!loggerInstance) {
1060
+ loggerInstance = new MomentumLogger(ROOT_CONTEXT);
1061
+ }
1062
+ return loggerInstance;
1063
+ }
1064
+ function createLogger(context) {
1065
+ return getMomentumLogger().child(context);
1066
+ }
1067
+
1068
+ // libs/db-drizzle/src/lib/db-postgres.ts
1069
+ function getColumnType2(field) {
1070
+ switch (field.type) {
1071
+ case "text":
1072
+ case "textarea":
1073
+ case "richText":
1074
+ case "password":
1075
+ case "radio":
1076
+ case "point":
1077
+ return "TEXT";
1078
+ case "email":
1079
+ case "slug":
1080
+ case "select":
1081
+ return "VARCHAR(255)";
1082
+ case "number":
1083
+ return "NUMERIC";
1084
+ case "checkbox":
1085
+ return "BOOLEAN";
1086
+ case "date":
1087
+ return "TIMESTAMPTZ";
1088
+ case "relationship":
1089
+ case "upload":
1090
+ return "VARCHAR(36)";
1091
+ case "array":
1092
+ case "group":
1093
+ case "blocks":
1094
+ case "json":
1095
+ return "JSONB";
1096
+ case "tabs":
1097
+ case "collapsible":
1098
+ case "row":
1099
+ return "TEXT";
1100
+ default:
1101
+ return "TEXT";
1102
+ }
1103
+ }
1104
+ function resolveRelationshipSlug(field) {
1105
+ try {
1106
+ const config = field.collection();
1107
+ if (config && typeof config === "object" && "slug" in config) {
1108
+ const { slug: slug2 } = config;
1109
+ if (typeof slug2 === "string")
1110
+ return slug2;
1111
+ }
1112
+ } catch {
1113
+ return void 0;
1114
+ }
1115
+ return void 0;
1116
+ }
1117
+ function mapOnDelete(onDelete, required) {
1118
+ const effective = required && (!onDelete || onDelete === "set-null") ? "restrict" : onDelete;
1119
+ switch (effective) {
1120
+ case "restrict":
1121
+ return "ON DELETE RESTRICT";
1122
+ case "cascade":
1123
+ return "ON DELETE CASCADE";
1124
+ default:
1125
+ return "ON DELETE SET NULL";
1126
+ }
1127
+ }
1128
+ function rethrowIfFkViolation(error, table) {
1129
+ if (error instanceof Error && "code" in error && "constraint" in error) {
1130
+ const { code, constraint } = error;
1131
+ if (code === "23503") {
1132
+ throw new ReferentialIntegrityError(table, constraint);
1133
+ }
1134
+ }
1135
+ }
1136
+ function createTableSql2(collection) {
1137
+ const columns = [
1138
+ '"id" VARCHAR(36) PRIMARY KEY',
1139
+ '"createdAt" TIMESTAMPTZ NOT NULL',
1140
+ '"updatedAt" TIMESTAMPTZ NOT NULL'
1141
+ ];
1142
+ if (hasVersionDrafts2(collection)) {
1143
+ columns.push(`"_status" VARCHAR(20) DEFAULT 'draft'`);
1144
+ columns.push('"scheduledPublishAt" TIMESTAMPTZ');
1145
+ }
1146
+ const softDeleteCol = getSoftDeleteField(collection);
1147
+ if (softDeleteCol) {
1148
+ validateColumnName2(softDeleteCol);
1149
+ columns.push(`"${softDeleteCol}" TIMESTAMPTZ`);
1150
+ }
1151
+ const dataFields = flattenDataFields(collection.fields);
1152
+ for (const field of dataFields) {
1153
+ const colType = getColumnType2(field);
1154
+ const notNull = field.required ? " NOT NULL" : "";
1155
+ columns.push(`"${field.name}" ${colType}${notNull}`);
1156
+ }
1157
+ const tableName = collection.dbName ?? collection.slug;
1158
+ return `CREATE TABLE IF NOT EXISTS "${tableName}" (${columns.join(", ")})`;
1159
+ }
1160
+ function getTableName2(collection) {
1161
+ return collection.dbName ?? collection.slug;
1162
+ }
1163
+ function hasVersionDrafts2(collection) {
1164
+ const versions = collection.versions;
1165
+ if (!versions)
1166
+ return false;
1167
+ if (typeof versions === "boolean")
1168
+ return false;
1169
+ return !!versions.drafts;
1170
+ }
1171
+ function validateCollectionSlug2(slug2) {
1172
+ const validSlug = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
1173
+ if (!validSlug.test(slug2)) {
1174
+ throw new Error(
1175
+ `Invalid collection slug: "${slug2}". Slugs must start with a letter or underscore and contain only alphanumeric characters, underscores, and hyphens.`
1176
+ );
1177
+ }
1178
+ }
1179
+ function validateColumnName2(name) {
1180
+ const validColumnName = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
1181
+ if (!validColumnName.test(name)) {
1182
+ throw new Error(`Invalid column name: "${name}"`);
1183
+ }
1184
+ }
1185
+ function createVersionTableSql2(collection) {
1186
+ if (!collection.versions)
1187
+ return null;
1188
+ const baseTable = getTableName2(collection);
1189
+ const tableName = `${baseTable}_versions`;
1190
+ return `
1191
+ CREATE TABLE IF NOT EXISTS "${tableName}" (
1192
+ "id" VARCHAR(36) PRIMARY KEY,
1193
+ "parent" VARCHAR(36) NOT NULL,
1194
+ "version" TEXT NOT NULL,
1195
+ "_status" VARCHAR(20) DEFAULT 'draft',
1196
+ "autosave" BOOLEAN DEFAULT false,
1197
+ "publishedAt" TIMESTAMPTZ,
1198
+ "createdAt" TIMESTAMPTZ NOT NULL,
1199
+ "updatedAt" TIMESTAMPTZ NOT NULL,
1200
+ CONSTRAINT "fk_${tableName}_parent" FOREIGN KEY ("parent") REFERENCES "${baseTable}"("id") ON DELETE CASCADE
1201
+ );
1202
+ CREATE INDEX IF NOT EXISTS "idx_${tableName}_parent" ON "${tableName}"("parent");
1203
+ CREATE INDEX IF NOT EXISTS "idx_${tableName}_createdAt" ON "${tableName}"("createdAt");
1204
+ `;
1205
+ }
1206
+ function isDocumentStatus2(value) {
1207
+ return value === "draft" || value === "published";
1208
+ }
1209
+ function getStatusFromRow2(row2) {
1210
+ const status = row2["_status"];
1211
+ return isDocumentStatus2(status) ? status : "draft";
1212
+ }
1213
+ function parseJsonToRecord2(jsonString) {
1214
+ try {
1215
+ const parsed = JSON.parse(jsonString);
1216
+ if (typeof parsed === "object" && parsed !== null) {
1217
+ return parsed;
1218
+ }
1219
+ return {};
1220
+ } catch {
1221
+ return {};
1222
+ }
1223
+ }
1224
+ var SEED_TRACKING_TABLE_SQL2 = `
1225
+ CREATE TABLE IF NOT EXISTS "_momentum_seeds" (
1226
+ "id" VARCHAR(36) PRIMARY KEY NOT NULL,
1227
+ "seedId" VARCHAR(255) NOT NULL UNIQUE,
1228
+ "collection" VARCHAR(255) NOT NULL,
1229
+ "documentId" VARCHAR(36) NOT NULL,
1230
+ "checksum" VARCHAR(64) NOT NULL,
1231
+ "createdAt" TIMESTAMPTZ NOT NULL,
1232
+ "updatedAt" TIMESTAMPTZ NOT NULL
1233
+ );
1234
+
1235
+ CREATE INDEX IF NOT EXISTS "idx_momentum_seeds_seedId" ON "_momentum_seeds"("seedId");
1236
+ `;
1237
+ function extractDatabaseName(connectionString) {
1238
+ try {
1239
+ const url = new URL(connectionString);
1240
+ const dbName = url.pathname.slice(1);
1241
+ return dbName || null;
1242
+ } catch {
1243
+ const match = connectionString.match(/\/([^/?]+)(\?|$)/);
1244
+ return match?.[1] ?? null;
1245
+ }
1246
+ }
1247
+ async function ensureDatabaseExists(connectionString) {
1248
+ const dbName = extractDatabaseName(connectionString);
1249
+ if (!dbName) {
1250
+ return;
1251
+ }
1252
+ const testClient = new import_pg.Client({ connectionString });
1253
+ try {
1254
+ await testClient.connect();
1255
+ await testClient.end();
1256
+ return;
1257
+ } catch (error) {
1258
+ await testClient.end().catch(() => {
1259
+ });
1260
+ const pgError = error instanceof Object && "code" in error ? error : null;
1261
+ if (!pgError || pgError.code !== "3D000") {
1262
+ throw error;
1263
+ }
1264
+ }
1265
+ let adminConnString;
1266
+ try {
1267
+ const url = new URL(connectionString);
1268
+ url.pathname = "/postgres";
1269
+ adminConnString = url.toString();
1270
+ } catch {
1271
+ adminConnString = connectionString.replace(`/${dbName}`, "/postgres");
1272
+ }
1273
+ const adminClient = new import_pg.Client({ connectionString: adminConnString });
1274
+ try {
1275
+ await adminClient.connect();
1276
+ const safeName = dbName.replace(/"/g, '""');
1277
+ await adminClient.query(`CREATE DATABASE "${safeName}"`);
1278
+ createLogger("DB").info(`Created database "${dbName}"`);
1279
+ } catch (createError) {
1280
+ const pgCreateError = createError instanceof Object && "code" in createError ? createError : null;
1281
+ if (pgCreateError && pgCreateError.code === "42P04") {
1282
+ return;
1283
+ }
1284
+ throw createError;
1285
+ } finally {
1286
+ await adminClient.end();
1287
+ }
1288
+ }
1289
+ function postgresAdapter(options) {
1290
+ const pool = new import_pg.Pool({
1291
+ connectionString: options.connectionString,
1292
+ max: options.max ?? 10
1293
+ });
1294
+ const tableNameMap = /* @__PURE__ */ new Map();
1295
+ function resolveTableName(slug2) {
1296
+ return tableNameMap.get(slug2) ?? slug2;
1297
+ }
1298
+ function createHelpers(conn) {
1299
+ async function query(sql, params = []) {
1300
+ const result = await conn.query(sql, params);
1301
+ return result.rows.filter((row2) => typeof row2 === "object" && row2 !== null);
1302
+ }
1303
+ async function queryOne(sql, params = []) {
1304
+ const rows = await query(sql, params);
1305
+ return rows[0] ?? null;
1306
+ }
1307
+ async function execute(sql, params = []) {
1308
+ const result = await conn.query(sql, params);
1309
+ return result.rowCount ?? 0;
1310
+ }
1311
+ return { query, queryOne, execute };
1312
+ }
1313
+ function buildMethods(h) {
1314
+ return {
1315
+ async find(collection, queryParams) {
1316
+ validateCollectionSlug2(collection);
1317
+ const limitValue = typeof queryParams["limit"] === "number" ? queryParams["limit"] : 100;
1318
+ const pageValue = typeof queryParams["page"] === "number" ? queryParams["page"] : 1;
1319
+ const offset = (pageValue - 1) * limitValue;
1320
+ const whereClauses = [];
1321
+ const whereValues = [];
1322
+ const reservedParams = /* @__PURE__ */ new Set(["limit", "page", "sort", "order"]);
1323
+ let paramIndex = 1;
1324
+ const validColumnName = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
1325
+ for (const [key, value] of Object.entries(queryParams)) {
1326
+ if (reservedParams.has(key)) {
1327
+ continue;
1328
+ }
1329
+ if (value === void 0)
1330
+ continue;
1331
+ if (!validColumnName.test(key)) {
1332
+ throw new Error(`Invalid column name: ${key}`);
1333
+ }
1334
+ if (value === null) {
1335
+ whereClauses.push(`"${key}" IS NULL`);
1336
+ } else if (typeof value === "object" && value !== null && "$ne" in value && value["$ne"] === null) {
1337
+ whereClauses.push(`"${key}" IS NOT NULL`);
1338
+ } else {
1339
+ whereClauses.push(`"${key}" = $${paramIndex}`);
1340
+ whereValues.push(value);
1341
+ paramIndex++;
1342
+ }
1343
+ }
1344
+ const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
1345
+ if (limitValue === 0) {
1346
+ return h.query(
1347
+ `SELECT * FROM "${resolveTableName(collection)}" ${whereClause}`,
1348
+ whereValues
1349
+ );
1350
+ }
1351
+ return h.query(
1352
+ `SELECT * FROM "${resolveTableName(collection)}" ${whereClause} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
1353
+ [...whereValues, limitValue, offset]
1354
+ );
1355
+ },
1356
+ async search(collection, searchQuery, fields, options2) {
1357
+ validateCollectionSlug2(collection);
1358
+ if (!searchQuery || fields.length === 0)
1359
+ return [];
1360
+ const limitValue = options2?.limit ?? 20;
1361
+ const pageValue = options2?.page ?? 1;
1362
+ const offset = (pageValue - 1) * limitValue;
1363
+ const validColumnName = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
1364
+ for (const field of fields) {
1365
+ if (!validColumnName.test(field)) {
1366
+ throw new Error(`Invalid column name: ${field}`);
1367
+ }
1368
+ }
1369
+ const tsvectorParts = fields.map((f) => `coalesce("${f}"::text, '')`).join(" || ' ' || ");
1370
+ const tsvectorExpr = `to_tsvector('simple', ${tsvectorParts})`;
1371
+ const tsqueryExpr = `plainto_tsquery('simple', $1)`;
1372
+ const ilikeClauses = fields.map((f, i) => `"${f}"::text ILIKE $${i + 2}`).join(" OR ");
1373
+ const escapedQuery = searchQuery.replace(/[%_\\]/g, "\\$&");
1374
+ const ilikePattern = `%${escapedQuery}%`;
1375
+ const ilikeParams = fields.map(() => ilikePattern);
1376
+ const sql = `
1377
+ SELECT *, ts_rank(${tsvectorExpr}, ${tsqueryExpr}) AS _search_rank
1378
+ FROM "${resolveTableName(collection)}"
1379
+ WHERE ${tsvectorExpr} @@ ${tsqueryExpr} OR ${ilikeClauses}
1380
+ ORDER BY _search_rank DESC
1381
+ LIMIT $${fields.length + 2} OFFSET $${fields.length + 3}
1382
+ `;
1383
+ const results = await h.query(sql, [searchQuery, ...ilikeParams, limitValue, offset]);
1384
+ return results.map((row2) => {
1385
+ const { _search_rank, ...doc } = row2;
1386
+ return doc;
1387
+ });
1388
+ },
1389
+ async findById(collection, id) {
1390
+ validateCollectionSlug2(collection);
1391
+ return h.queryOne(`SELECT * FROM "${resolveTableName(collection)}" WHERE id = $1`, [id]);
1392
+ },
1393
+ async create(collection, data) {
1394
+ validateCollectionSlug2(collection);
1395
+ const id = (0, import_node_crypto2.randomUUID)();
1396
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1397
+ const doc = {
1398
+ ...data,
1399
+ id,
1400
+ createdAt: now,
1401
+ updatedAt: now
1402
+ };
1403
+ const columns = Object.keys(doc);
1404
+ const placeholders = columns.map((_, i) => `$${i + 1}`).join(", ");
1405
+ const values = columns.map((col) => {
1406
+ const val = doc[col];
1407
+ if (val === void 0) {
1408
+ return null;
1409
+ }
1410
+ if (typeof val === "object" && val !== null && !(val instanceof Date)) {
1411
+ return JSON.stringify(val);
1412
+ }
1413
+ return val;
1414
+ });
1415
+ const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
1416
+ await h.execute(
1417
+ `INSERT INTO "${resolveTableName(collection)}" (${quotedColumns}) VALUES (${placeholders})`,
1418
+ values
1419
+ );
1420
+ return doc;
1421
+ },
1422
+ async update(collection, id, data) {
1423
+ validateCollectionSlug2(collection);
1424
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1425
+ const updateData = { ...data, updatedAt: now };
1426
+ delete updateData["id"];
1427
+ delete updateData["createdAt"];
1428
+ const setClauses = [];
1429
+ const values = [];
1430
+ let paramIndex = 1;
1431
+ for (const [key, value] of Object.entries(updateData)) {
1432
+ setClauses.push(`"${key}" = $${paramIndex}`);
1433
+ paramIndex++;
1434
+ if (value === void 0) {
1435
+ values.push(null);
1436
+ } else if (typeof value === "object" && value !== null && !(value instanceof Date)) {
1437
+ values.push(JSON.stringify(value));
1438
+ } else {
1439
+ values.push(value);
1440
+ }
1441
+ }
1442
+ values.push(id);
1443
+ await h.execute(
1444
+ `UPDATE "${resolveTableName(collection)}" SET ${setClauses.join(", ")} WHERE id = $${paramIndex}`,
1445
+ values
1446
+ );
1447
+ const updated = await h.queryOne(
1448
+ `SELECT * FROM "${resolveTableName(collection)}" WHERE id = $1`,
1449
+ [id]
1450
+ );
1451
+ if (!updated) {
1452
+ throw new Error("Failed to fetch updated document");
1453
+ }
1454
+ return updated;
1455
+ },
1456
+ async delete(collection, id) {
1457
+ validateCollectionSlug2(collection);
1458
+ try {
1459
+ const rowCount = await h.execute(
1460
+ `DELETE FROM "${resolveTableName(collection)}" WHERE id = $1`,
1461
+ [id]
1462
+ );
1463
+ return rowCount > 0;
1464
+ } catch (error) {
1465
+ rethrowIfFkViolation(error, collection);
1466
+ throw error;
1467
+ }
1468
+ },
1469
+ async softDelete(collection, id, field = "deletedAt") {
1470
+ validateCollectionSlug2(collection);
1471
+ validateColumnName2(field);
1472
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1473
+ const rowCount = await h.execute(
1474
+ `UPDATE "${resolveTableName(collection)}" SET "${field}" = $1, "updatedAt" = $2 WHERE "id" = $3`,
1475
+ [now, now, id]
1476
+ );
1477
+ return rowCount > 0;
1478
+ },
1479
+ async restore(collection, id, field = "deletedAt") {
1480
+ validateCollectionSlug2(collection);
1481
+ validateColumnName2(field);
1482
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1483
+ await h.execute(
1484
+ `UPDATE "${resolveTableName(collection)}" SET "${field}" = NULL, "updatedAt" = $1 WHERE "id" = $2`,
1485
+ [now, id]
1486
+ );
1487
+ const restored = await h.queryOne(
1488
+ `SELECT * FROM "${resolveTableName(collection)}" WHERE "id" = $1`,
1489
+ [id]
1490
+ );
1491
+ if (!restored) {
1492
+ throw new Error("Failed to fetch restored document");
1493
+ }
1494
+ return restored;
1495
+ },
1496
+ // ==========================================
1497
+ // Version Operations
1498
+ // ==========================================
1499
+ async createVersion(collection, parentId, data, options2) {
1500
+ validateCollectionSlug2(collection);
1501
+ const id = (0, import_node_crypto2.randomUUID)();
1502
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1503
+ const status = options2?.status ?? "draft";
1504
+ const autosave = options2?.autosave ?? false;
1505
+ const publishedAt = status === "published" ? now : null;
1506
+ const versionData = JSON.stringify(data);
1507
+ const tableName = `${resolveTableName(collection)}_versions`;
1508
+ await h.execute(
1509
+ `INSERT INTO "${tableName}" ("id", "parent", "version", "_status", "autosave", "publishedAt", "createdAt", "updatedAt")
1510
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
1511
+ [id, parentId, versionData, status, autosave, publishedAt, now, now]
1512
+ );
1513
+ return {
1514
+ id,
1515
+ parent: parentId,
1516
+ version: versionData,
1517
+ _status: status,
1518
+ autosave,
1519
+ publishedAt: publishedAt ?? void 0,
1520
+ createdAt: now,
1521
+ updatedAt: now
1522
+ };
1523
+ },
1524
+ async findVersions(collection, parentId, options2) {
1525
+ validateCollectionSlug2(collection);
1526
+ const tableName = `${resolveTableName(collection)}_versions`;
1527
+ const limit = options2?.limit ?? 10;
1528
+ const page = options2?.page ?? 1;
1529
+ const offset = (page - 1) * limit;
1530
+ const sortDirection = options2?.sort === "asc" ? "ASC" : "DESC";
1531
+ const whereClauses = ['"parent" = $1'];
1532
+ const params = [parentId];
1533
+ let paramIndex = 2;
1534
+ if (!options2?.includeAutosave) {
1535
+ whereClauses.push('"autosave" = false');
1536
+ }
1537
+ if (options2?.status) {
1538
+ whereClauses.push(`"_status" = $${paramIndex}`);
1539
+ params.push(options2.status);
1540
+ paramIndex++;
1541
+ }
1542
+ const whereClause = whereClauses.join(" AND ");
1543
+ params.push(limit, offset);
1544
+ const rows = await h.query(
1545
+ `SELECT * FROM "${tableName}" WHERE ${whereClause} ORDER BY "createdAt" ${sortDirection} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
1546
+ params
1547
+ );
1548
+ return rows.map((row2) => ({
1549
+ id: String(row2["id"]),
1550
+ parent: String(row2["parent"]),
1551
+ version: String(row2["version"]),
1552
+ _status: getStatusFromRow2(row2),
1553
+ autosave: Boolean(row2["autosave"]),
1554
+ publishedAt: row2["publishedAt"] ? String(row2["publishedAt"]) : void 0,
1555
+ createdAt: String(row2["createdAt"]),
1556
+ updatedAt: String(row2["updatedAt"])
1557
+ }));
1558
+ },
1559
+ async findVersionById(collection, versionId) {
1560
+ validateCollectionSlug2(collection);
1561
+ const tableName = `${resolveTableName(collection)}_versions`;
1562
+ const row2 = await h.queryOne(
1563
+ `SELECT * FROM "${tableName}" WHERE "id" = $1`,
1564
+ [versionId]
1565
+ );
1566
+ if (!row2)
1567
+ return null;
1568
+ return {
1569
+ id: String(row2["id"]),
1570
+ parent: String(row2["parent"]),
1571
+ version: String(row2["version"]),
1572
+ _status: getStatusFromRow2(row2),
1573
+ autosave: Boolean(row2["autosave"]),
1574
+ publishedAt: row2["publishedAt"] ? String(row2["publishedAt"]) : void 0,
1575
+ createdAt: String(row2["createdAt"]),
1576
+ updatedAt: String(row2["updatedAt"])
1577
+ };
1578
+ },
1579
+ async restoreVersion(collection, versionId) {
1580
+ validateCollectionSlug2(collection);
1581
+ const tableName = `${resolveTableName(collection)}_versions`;
1582
+ const version = await h.queryOne(
1583
+ `SELECT * FROM "${tableName}" WHERE "id" = $1`,
1584
+ [versionId]
1585
+ );
1586
+ if (!version) {
1587
+ throw new Error("Version not found");
1588
+ }
1589
+ const parentId = String(version["parent"]);
1590
+ const versionData = parseJsonToRecord2(String(version["version"]));
1591
+ const originalStatus = getStatusFromRow2(version);
1592
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1593
+ const updateData = { ...versionData, updatedAt: now };
1594
+ delete updateData["id"];
1595
+ delete updateData["createdAt"];
1596
+ const setClauses = [];
1597
+ const values = [];
1598
+ let paramIndex = 1;
1599
+ const validColumnName = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
1600
+ for (const [key, value] of Object.entries(updateData)) {
1601
+ if (!validColumnName.test(key)) {
1602
+ throw new Error(`Invalid column name in version data: ${key}`);
1603
+ }
1604
+ setClauses.push(`"${key}" = $${paramIndex}`);
1605
+ paramIndex++;
1606
+ if (value === void 0) {
1607
+ values.push(null);
1608
+ } else if (typeof value === "object" && value !== null && !(value instanceof Date)) {
1609
+ values.push(JSON.stringify(value));
1610
+ } else {
1611
+ values.push(value);
1612
+ }
1613
+ }
1614
+ values.push(parentId);
1615
+ await h.execute(
1616
+ `UPDATE "${resolveTableName(collection)}" SET ${setClauses.join(", ")} WHERE "id" = $${paramIndex}`,
1617
+ values
1618
+ );
1619
+ const newVersionId = (0, import_node_crypto2.randomUUID)();
1620
+ await h.execute(
1621
+ `INSERT INTO "${tableName}" ("id", "parent", "version", "_status", "autosave", "createdAt", "updatedAt")
1622
+ VALUES ($1, $2, $3, $4, false, $5, $6)`,
1623
+ [newVersionId, parentId, String(version["version"]), originalStatus, now, now]
1624
+ );
1625
+ const updated = await h.queryOne(
1626
+ `SELECT * FROM "${resolveTableName(collection)}" WHERE "id" = $1`,
1627
+ [parentId]
1628
+ );
1629
+ if (!updated) {
1630
+ throw new Error("Failed to fetch restored document");
1631
+ }
1632
+ return updated;
1633
+ },
1634
+ async deleteVersions(collection, parentId, keepLatest) {
1635
+ validateCollectionSlug2(collection);
1636
+ const tableName = `${resolveTableName(collection)}_versions`;
1637
+ if (keepLatest && keepLatest > 0) {
1638
+ const toKeep = await h.query(
1639
+ `SELECT "id" FROM "${tableName}" WHERE "parent" = $1 ORDER BY "createdAt" DESC LIMIT $2`,
1640
+ [parentId, keepLatest]
1641
+ );
1642
+ const keepIds = toKeep.map((r) => String(r["id"]));
1643
+ if (keepIds.length === 0) {
1644
+ return 0;
1645
+ }
1646
+ const placeholders = keepIds.map((_, i) => `$${i + 2}`).join(", ");
1647
+ return h.execute(
1648
+ `DELETE FROM "${tableName}" WHERE "parent" = $1 AND "id" NOT IN (${placeholders})`,
1649
+ [parentId, ...keepIds]
1650
+ );
1651
+ }
1652
+ return h.execute(`DELETE FROM "${tableName}" WHERE "parent" = $1`, [parentId]);
1653
+ },
1654
+ async countVersions(collection, parentId, options2) {
1655
+ validateCollectionSlug2(collection);
1656
+ const tableName = `${resolveTableName(collection)}_versions`;
1657
+ const whereClauses = ['"parent" = $1'];
1658
+ const params = [parentId];
1659
+ if (!options2?.includeAutosave) {
1660
+ whereClauses.push('"autosave" = false');
1661
+ }
1662
+ if (options2?.status) {
1663
+ whereClauses.push(`"_status" = $${params.length + 1}`);
1664
+ params.push(options2.status);
1665
+ }
1666
+ const whereClause = whereClauses.join(" AND ");
1667
+ const result = await h.queryOne(
1668
+ `SELECT COUNT(*) as count FROM "${tableName}" WHERE ${whereClause}`,
1669
+ params
1670
+ );
1671
+ return Number(result?.["count"] ?? 0);
1672
+ },
1673
+ async updateStatus(collection, id, status) {
1674
+ validateCollectionSlug2(collection);
1675
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1676
+ await h.execute(
1677
+ `UPDATE "${resolveTableName(collection)}" SET "_status" = $1, "updatedAt" = $2 WHERE "id" = $3`,
1678
+ [status, now, id]
1679
+ );
1680
+ },
1681
+ async setScheduledPublishAt(collection, id, publishAt) {
1682
+ validateCollectionSlug2(collection);
1683
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1684
+ await h.execute(
1685
+ `UPDATE "${resolveTableName(collection)}" SET "scheduledPublishAt" = $1, "updatedAt" = $2 WHERE "id" = $3`,
1686
+ [publishAt, now, id]
1687
+ );
1688
+ },
1689
+ async findScheduledDocuments(collection, before) {
1690
+ validateCollectionSlug2(collection);
1691
+ const rows = await h.query(
1692
+ `SELECT "id", "scheduledPublishAt" FROM "${resolveTableName(collection)}" WHERE "scheduledPublishAt" IS NOT NULL AND "scheduledPublishAt" <= $1`,
1693
+ [before]
1694
+ );
1695
+ return rows.map((row2) => ({
1696
+ id: String(row2["id"]),
1697
+ scheduledPublishAt: String(row2["scheduledPublishAt"])
1698
+ }));
1699
+ }
1700
+ };
1701
+ }
1702
+ const helpers = createHelpers(pool);
1703
+ const methods = buildMethods(helpers);
1704
+ return {
1705
+ /**
1706
+ * Get the pg Pool instance for Better Auth integration.
1707
+ */
1708
+ getPool() {
1709
+ return pool;
1710
+ },
1711
+ /**
1712
+ * Execute a raw SQL query.
1713
+ */
1714
+ query: helpers.query,
1715
+ /**
1716
+ * Execute a raw SQL query and return a single row.
1717
+ */
1718
+ queryOne: helpers.queryOne,
1719
+ /**
1720
+ * Execute a raw SQL statement.
1721
+ */
1722
+ execute: helpers.execute,
1723
+ // Spread all CRUD + version methods
1724
+ ...methods,
1725
+ async initialize(collections) {
1726
+ await ensureDatabaseExists(options.connectionString);
1727
+ for (const collection of collections) {
1728
+ const tbl = getTableName2(collection);
1729
+ tableNameMap.set(collection.slug, tbl);
1730
+ }
1731
+ await pool.query(SEED_TRACKING_TABLE_SQL2);
1732
+ for (const collection of collections) {
1733
+ const tbl = getTableName2(collection);
1734
+ const createSql = createTableSql2(collection);
1735
+ await pool.query(createSql);
1736
+ const dataFields = flattenDataFields(collection.fields);
1737
+ for (const field of dataFields) {
1738
+ const colType = getColumnType2(field);
1739
+ await pool.query(
1740
+ `ALTER TABLE "${tbl}" ADD COLUMN IF NOT EXISTS "${field.name}" ${colType}`
1741
+ );
1742
+ }
1743
+ if (hasVersionDrafts2(collection)) {
1744
+ await pool.query(
1745
+ `ALTER TABLE "${tbl}" ADD COLUMN IF NOT EXISTS "_status" VARCHAR(20) DEFAULT 'draft'`
1746
+ );
1747
+ await pool.query(
1748
+ `ALTER TABLE "${tbl}" ADD COLUMN IF NOT EXISTS "scheduledPublishAt" TIMESTAMPTZ`
1749
+ );
1750
+ }
1751
+ const sdField = getSoftDeleteField(collection);
1752
+ if (sdField) {
1753
+ await pool.query(
1754
+ `ALTER TABLE "${tbl}" ADD COLUMN IF NOT EXISTS "${sdField}" TIMESTAMPTZ`
1755
+ );
1756
+ await pool.query(
1757
+ `CREATE INDEX IF NOT EXISTS "idx_${tbl}_${sdField}" ON "${tbl}"("${sdField}")`
1758
+ );
1759
+ }
1760
+ if (collection.indexes) {
1761
+ for (const idx of collection.indexes) {
1762
+ for (const col of idx.columns) {
1763
+ validateColumnName2(col);
1764
+ }
1765
+ const idxName = idx.name ?? `idx_${tbl}_${idx.columns.join("_")}`;
1766
+ const uniqueStr = idx.unique ? "UNIQUE " : "";
1767
+ const colList = idx.columns.map((c) => `"${c}"`).join(", ");
1768
+ await pool.query(
1769
+ `CREATE ${uniqueStr}INDEX IF NOT EXISTS "${idxName}" ON "${tbl}"(${colList})`
1770
+ );
1771
+ }
1772
+ }
1773
+ const versionTableSql = createVersionTableSql2(collection);
1774
+ if (versionTableSql) {
1775
+ await pool.query(versionTableSql);
1776
+ }
1777
+ }
1778
+ for (const collection of collections) {
1779
+ const tbl = getTableName2(collection);
1780
+ validateCollectionSlug2(collection.slug);
1781
+ const dataFields = flattenDataFields(collection.fields);
1782
+ for (const field of dataFields) {
1783
+ if (field.type === "relationship" && !field.hasMany && !field.relationTo) {
1784
+ const targetSlug = resolveRelationshipSlug(field);
1785
+ if (targetSlug) {
1786
+ const targetTable = resolveTableName(targetSlug);
1787
+ validateCollectionSlug2(targetSlug);
1788
+ validateCollectionSlug2(field.name);
1789
+ const constraintName = `fk_${tbl}_${field.name}`;
1790
+ const isRequired = !!field.required;
1791
+ const onDeleteSql = mapOnDelete(field.onDelete, isRequired);
1792
+ if (isRequired) {
1793
+ await pool.query(
1794
+ `DELETE FROM "${tbl}" WHERE "${field.name}" IS NOT NULL AND "${field.name}" NOT IN (SELECT "id" FROM "${targetTable}")`
1795
+ );
1796
+ } else {
1797
+ await pool.query(
1798
+ `UPDATE "${tbl}" SET "${field.name}" = NULL WHERE "${field.name}" IS NOT NULL AND "${field.name}" NOT IN (SELECT "id" FROM "${targetTable}")`
1799
+ );
1800
+ }
1801
+ await pool.query(`
1802
+ DO $$ BEGIN
1803
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = '${constraintName}') THEN
1804
+ ALTER TABLE "${tbl}"
1805
+ ADD CONSTRAINT "${constraintName}"
1806
+ FOREIGN KEY ("${field.name}")
1807
+ REFERENCES "${targetTable}"("id")
1808
+ ${onDeleteSql};
1809
+ END IF;
1810
+ END $$;
1811
+ `);
1812
+ }
1813
+ }
1814
+ }
1815
+ }
1816
+ },
1817
+ // ============================================
1818
+ // Globals Support
1819
+ // ============================================
1820
+ async initializeGlobals(_globals) {
1821
+ await pool.query(`
1822
+ CREATE TABLE IF NOT EXISTS "_globals" (
1823
+ "slug" VARCHAR(255) PRIMARY KEY,
1824
+ "data" JSONB NOT NULL DEFAULT '{}',
1825
+ "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1826
+ "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1827
+ )
1828
+ `);
1829
+ },
1830
+ async findGlobal(slug2) {
1831
+ const result = await pool.query('SELECT * FROM "_globals" WHERE "slug" = $1', [
1832
+ slug2
1833
+ ]);
1834
+ if (result.rows.length === 0)
1835
+ return null;
1836
+ const row2 = result.rows[0];
1837
+ const data = typeof row2["data"] === "string" ? JSON.parse(row2["data"]) : row2["data"];
1838
+ return {
1839
+ ...data,
1840
+ slug: row2["slug"],
1841
+ createdAt: row2["createdAt"],
1842
+ updatedAt: row2["updatedAt"]
1843
+ };
1844
+ },
1845
+ async updateGlobal(slug2, data) {
1846
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1847
+ const jsonData = JSON.stringify(data);
1848
+ const result = await pool.query(
1849
+ `INSERT INTO "_globals" ("slug", "data", "createdAt", "updatedAt")
1850
+ VALUES ($1, $2::jsonb, $3, $4)
1851
+ ON CONFLICT ("slug") DO UPDATE SET "data" = $2::jsonb, "updatedAt" = $4
1852
+ RETURNING *`,
1853
+ [slug2, jsonData, now, now]
1854
+ );
1855
+ const row2 = result.rows[0];
1856
+ const returned = typeof row2["data"] === "string" ? JSON.parse(row2["data"]) : row2["data"];
1857
+ return {
1858
+ ...returned,
1859
+ slug: row2["slug"],
1860
+ createdAt: row2["createdAt"],
1861
+ updatedAt: row2["updatedAt"]
1862
+ };
1863
+ },
1864
+ async transaction(callback) {
1865
+ const client = await pool.connect();
1866
+ try {
1867
+ await client.query("BEGIN");
1868
+ const txHelpers = createHelpers(client);
1869
+ const txAdapter = buildMethods(txHelpers);
1870
+ const result = await callback(txAdapter);
1871
+ await client.query("COMMIT");
1872
+ return result;
1873
+ } catch (error) {
1874
+ await client.query("ROLLBACK");
1875
+ throw error;
1876
+ } finally {
1877
+ client.release();
1878
+ }
1879
+ }
1880
+ };
1881
+ }
1882
+ // Annotate the CommonJS export names for ESM import in node:
1883
+ 0 && (module.exports = {
1884
+ postgresAdapter,
1885
+ sqliteAdapter
1886
+ });