opacacms 0.3.18 → 0.3.19

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.
@@ -1,33 +1,1620 @@
1
- import {
2
- buildKyselyWhere,
3
- flattenPayload,
4
- pushSchema,
5
- unflattenRow
6
- } from "../chunk-re459gm9.js";
7
- import {
8
- requestContext
9
- } from "../chunk-q5sb5dcr.js";
10
- import {
11
- flattenFields,
12
- getRelationalFields,
13
- toSnakeCase
14
- } from "../chunk-5xpf5jxd.js";
15
- import {
16
- logger
17
- } from "../chunk-jq1drsen.js";
18
- import"../chunk-h8v093av.js";
19
- import {
20
- BaseDatabaseAdapter
21
- } from "../chunk-s8mqwnm1.js";
22
- import {
23
- __require
24
- } from "../chunk-8sqjbsgt.js";
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
+
19
+ // src/db/kysely/field-mapper.ts
20
+ var exports_field_mapper = {};
21
+ __export(exports_field_mapper, {
22
+ toSnakeCase: () => toSnakeCase,
23
+ mapFieldToSQLiteType: () => mapFieldToSQLiteType,
24
+ mapFieldToPostgresType: () => mapFieldToPostgresType,
25
+ getRelationalFields: () => getRelationalFields,
26
+ flattenFields: () => flattenFields
27
+ });
28
+ function toSnakeCase(str) {
29
+ const res = str.replace(/([A-Z])/g, "_$1").toLowerCase();
30
+ if (res.startsWith("_") && !str.startsWith("_")) {
31
+ return res.slice(1);
32
+ }
33
+ return res;
34
+ }
35
+ function mapFieldToPostgresType(field) {
36
+ switch (field.type) {
37
+ case "text":
38
+ case "richtext":
39
+ case "select":
40
+ case "radio":
41
+ case "relationship":
42
+ return "text";
43
+ case "number":
44
+ return "double precision";
45
+ case "boolean":
46
+ return "boolean";
47
+ case "date":
48
+ return "timestamptz";
49
+ case "json":
50
+ case "file":
51
+ return "jsonb";
52
+ default:
53
+ return "text";
54
+ }
55
+ }
56
+ function mapFieldToSQLiteType(field) {
57
+ switch (field.type) {
58
+ case "text":
59
+ case "richtext":
60
+ case "select":
61
+ case "radio":
62
+ case "relationship":
63
+ case "date":
64
+ case "json":
65
+ case "file":
66
+ return "text";
67
+ case "number":
68
+ return "numeric";
69
+ case "boolean":
70
+ return "integer";
71
+ default:
72
+ return "text";
73
+ }
74
+ }
75
+ function flattenFields(fields, prefix = "") {
76
+ const result = [];
77
+ for (const field of fields) {
78
+ if (field.type === "join" || field.type === "virtual" || field.type === "ui")
79
+ continue;
80
+ const currentName = field.name ? `${prefix}${field.name}` : undefined;
81
+ if (field.type === "group") {
82
+ if (field.fields && Array.isArray(field.fields)) {
83
+ const nextPrefix = currentName ? `${currentName}__` : "";
84
+ result.push(...flattenFields(field.fields, nextPrefix));
85
+ }
86
+ continue;
87
+ }
88
+ if (field.type === "blocks") {
89
+ continue;
90
+ }
91
+ if (field.type === "relationship" && "hasMany" in field && field.hasMany) {
92
+ continue;
93
+ }
94
+ if (currentName) {
95
+ result.push({ ...field, name: currentName });
96
+ }
97
+ if (field.type === "row" || field.type === "collapsible") {
98
+ if (field.fields && Array.isArray(field.fields)) {
99
+ result.push(...flattenFields(field.fields, prefix));
100
+ }
101
+ }
102
+ if (field.type === "tabs" && field.tabs && Array.isArray(field.tabs)) {
103
+ for (const tab of field.tabs) {
104
+ if (tab.fields && Array.isArray(tab.fields)) {
105
+ result.push(...flattenFields(tab.fields, prefix));
106
+ }
107
+ }
108
+ }
109
+ }
110
+ return result;
111
+ }
112
+ function getRelationalFields(fields, prefix = "") {
113
+ const result = [];
114
+ for (const field of fields) {
115
+ const currentName = field.name ? `${prefix}${field.name}` : undefined;
116
+ if (field.type === "relationship" && "hasMany" in field && field.hasMany || field.type === "blocks") {
117
+ if (currentName) {
118
+ result.push({ ...field, name: currentName });
119
+ }
120
+ continue;
121
+ }
122
+ if (field.type === "group" || field.type === "row" || field.type === "collapsible") {
123
+ if (field.fields && Array.isArray(field.fields)) {
124
+ const nextPrefix = field.type === "group" && field.name ? `${currentName}__` : prefix;
125
+ result.push(...getRelationalFields(field.fields, nextPrefix));
126
+ }
127
+ continue;
128
+ }
129
+ if (field.type === "tabs" && field.tabs && Array.isArray(field.tabs)) {
130
+ for (const tab of field.tabs) {
131
+ if (tab.fields && Array.isArray(tab.fields)) {
132
+ result.push(...getRelationalFields(tab.fields, prefix));
133
+ }
134
+ }
135
+ }
136
+ }
137
+ return result;
138
+ }
139
+
140
+ // src/db/system-schema.ts
141
+ var exports_system_schema = {};
142
+ __export(exports_system_schema, {
143
+ getSystemCollections: () => getSystemCollections
144
+ });
145
+ var getSystemCollections = () => [
146
+ {
147
+ slug: "_assets",
148
+ label: "Assets",
149
+ apiPath: "assets",
150
+ fields: [
151
+ { name: "id", type: "text", required: true },
152
+ { name: "key", type: "text", required: true },
153
+ { name: "filename", type: "text", required: true },
154
+ { name: "originalFilename", type: "text", required: true },
155
+ { name: "mimeType", type: "text", required: true },
156
+ { name: "filesize", type: "number", required: true },
157
+ { name: "bucket", type: "text", required: true },
158
+ { name: "folder", type: "text" },
159
+ { name: "altText", type: "text" },
160
+ { name: "caption", type: "text" },
161
+ { name: "uploadedBy", type: "text" }
162
+ ],
163
+ timestamps: true
164
+ },
165
+ {
166
+ slug: "_users",
167
+ apiPath: "users",
168
+ fields: [
169
+ { name: "id", type: "text", required: true },
170
+ { name: "name", type: "text", required: true },
171
+ { name: "email", type: "text", required: true, unique: true },
172
+ { name: "emailVerified", type: "boolean", required: true, defaultValue: false },
173
+ { name: "image", type: "text" },
174
+ { name: "role", type: "text" },
175
+ { name: "banned", type: "boolean" },
176
+ { name: "banReason", type: "text" },
177
+ { name: "banExpires", type: "date" }
178
+ ],
179
+ timestamps: true
180
+ },
181
+ {
182
+ slug: "_sessions",
183
+ fields: [
184
+ { name: "id", type: "text", required: true },
185
+ { name: "expiresAt", type: "date", required: true },
186
+ { name: "token", type: "text", required: true, unique: true },
187
+ { name: "ipAddress", type: "text" },
188
+ { name: "userAgent", type: "text" },
189
+ {
190
+ name: "userId",
191
+ type: "text",
192
+ required: true,
193
+ references: { table: "_users", column: "id", onDelete: "cascade" }
194
+ },
195
+ { name: "impersonatedBy", type: "text" }
196
+ ],
197
+ timestamps: true,
198
+ hidden: true
199
+ },
200
+ {
201
+ slug: "_accounts",
202
+ fields: [
203
+ { name: "id", type: "text", required: true },
204
+ { name: "accountId", type: "text", required: true },
205
+ { name: "providerId", type: "text", required: true },
206
+ {
207
+ name: "userId",
208
+ type: "text",
209
+ required: true,
210
+ references: { table: "_users", column: "id", onDelete: "cascade" }
211
+ },
212
+ { name: "accessToken", type: "text" },
213
+ { name: "refreshToken", type: "text" },
214
+ { name: "idToken", type: "text" },
215
+ { name: "accessTokenExpiresAt", type: "date" },
216
+ { name: "refreshTokenExpiresAt", type: "date" },
217
+ { name: "scope", type: "text" },
218
+ { name: "password", type: "text" }
219
+ ],
220
+ timestamps: true,
221
+ hidden: true
222
+ },
223
+ {
224
+ slug: "_verifications",
225
+ fields: [
226
+ { name: "id", type: "text", required: true },
227
+ { name: "identifier", type: "text", required: true },
228
+ { name: "value", type: "text", required: true },
229
+ { name: "expiresAt", type: "date", required: true }
230
+ ],
231
+ timestamps: true,
232
+ hidden: true
233
+ },
234
+ {
235
+ slug: "_api_keys",
236
+ fields: [
237
+ { name: "id", type: "text", required: true },
238
+ { name: "configId", type: "text", required: true },
239
+ { name: "name", type: "text" },
240
+ { name: "start", type: "text" },
241
+ { name: "prefix", type: "text" },
242
+ { name: "key", type: "text", required: true },
243
+ { name: "referenceId", type: "text", required: true },
244
+ { name: "refillInterval", type: "number" },
245
+ { name: "refillAmount", type: "number" },
246
+ { name: "lastRefillAt", type: "date" },
247
+ { name: "enabled", type: "boolean", required: true },
248
+ { name: "rateLimitEnabled", type: "boolean", required: true },
249
+ { name: "rateLimitTimeWindow", type: "number" },
250
+ { name: "rateLimitMax", type: "number" },
251
+ { name: "requestCount", type: "number", required: true },
252
+ { name: "remaining", type: "number" },
253
+ { name: "lastRequest", type: "date" },
254
+ { name: "expiresAt", type: "date" },
255
+ { name: "permissions", type: "text" },
256
+ { name: "metadata", type: "text" }
257
+ ],
258
+ timestamps: { createdAt: "createdAt", updatedAt: "updatedAt" },
259
+ hidden: true
260
+ },
261
+ {
262
+ slug: "_plugin_settings",
263
+ label: "Plugin Settings",
264
+ apiPath: "plugin-settings",
265
+ fields: [
266
+ { name: "pluginName", type: "text", required: true, unique: true },
267
+ { name: "config", type: "json", required: true }
268
+ ],
269
+ timestamps: true,
270
+ hidden: false,
271
+ admin: {
272
+ hidden: true,
273
+ disableAdmin: true
274
+ }
275
+ },
276
+ {
277
+ slug: "_audit_logs",
278
+ label: "Audit Logs",
279
+ apiPath: "audit-logs",
280
+ fields: [
281
+ { name: "id", type: "text", required: true },
282
+ { name: "operation", type: "text", required: true },
283
+ { name: "collection", type: "text", required: true },
284
+ { name: "entity_id", type: "text", required: true },
285
+ { name: "user_id", type: "text" },
286
+ { name: "previous_data", type: "json" },
287
+ { name: "new_data", type: "json" },
288
+ { name: "timestamp", type: "date" }
289
+ ],
290
+ timestamps: true,
291
+ admin: {
292
+ hidden: true,
293
+ disableAdmin: true
294
+ }
295
+ },
296
+ {
297
+ slug: "_doc_versions",
298
+ label: "Document Versions",
299
+ apiPath: "doc-versions",
300
+ fields: [
301
+ { name: "id", type: "text", required: true },
302
+ { name: "collection", type: "text", required: true },
303
+ { name: "entity_id", type: "text", required: true },
304
+ { name: "data", type: "json", required: true },
305
+ { name: "status", type: "text" },
306
+ { name: "autosave", type: "boolean", defaultValue: false },
307
+ { name: "version_name", type: "text" },
308
+ { name: "created_by", type: "text" }
309
+ ],
310
+ timestamps: true,
311
+ admin: {
312
+ hidden: true,
313
+ disableAdmin: true
314
+ }
315
+ }
316
+ ];
317
+
318
+ // src/utils/context.ts
319
+ import { AsyncLocalStorage } from "node:async_hooks";
320
+ function getUserIdFromContext() {
321
+ return requestContext.getStore()?.userId || null;
322
+ }
323
+ function getPreviousDataFromContext() {
324
+ return requestContext.getStore()?.previousData || null;
325
+ }
326
+ var GLOBAL_CONTEXT_KEY, requestContext;
327
+ var init_context = __esm(() => {
328
+ GLOBAL_CONTEXT_KEY = Symbol.for("opacacms.requestContext");
329
+ if (!globalThis[GLOBAL_CONTEXT_KEY]) {
330
+ globalThis[GLOBAL_CONTEXT_KEY] = new AsyncLocalStorage;
331
+ }
332
+ requestContext = globalThis[GLOBAL_CONTEXT_KEY];
333
+ });
334
+
335
+ // src/db/kysely/plugins/audit-logging.ts
336
+ class AuditLoggingPlugin {
337
+ auditTable;
338
+ getUserId;
339
+ db;
340
+ queryNodes = new WeakMap;
341
+ constructor(config = {}) {
342
+ this.auditTable = config.auditTable || "_audit_logs";
343
+ this.getUserId = config.getUserId;
344
+ }
345
+ setDb(db) {
346
+ this.db = db;
347
+ }
348
+ transformQuery(args) {
349
+ this.queryNodes.set(args.queryId, args.node);
350
+ return args.node;
351
+ }
352
+ async transformResult(args) {
353
+ const { result, queryId } = args;
354
+ const queryNode = this.queryNodes.get(queryId);
355
+ this.queryNodes.delete(queryId);
356
+ if (!queryNode || queryNode.kind === "SelectQueryNode")
357
+ return result;
358
+ let tableName = "";
359
+ let operation = "other";
360
+ if (queryNode.kind === "InsertQueryNode") {
361
+ tableName = this.getTableName(queryNode.into);
362
+ operation = "create";
363
+ } else if (queryNode.kind === "UpdateQueryNode") {
364
+ tableName = this.getTableName(queryNode.table);
365
+ operation = "update";
366
+ } else if (queryNode.kind === "DeleteQueryNode") {
367
+ tableName = this.getTableName(queryNode.table || queryNode.from?.froms?.[0]);
368
+ operation = "delete";
369
+ }
370
+ if (!tableName || tableName === this.auditTable)
371
+ return result;
372
+ const stashedPrevious = getPreviousDataFromContext();
373
+ this.logOperation(operation, tableName, queryNode, result, stashedPrevious).catch((err) => {
374
+ console.error("[OpacaCMS] Audit logging failed:", err);
375
+ });
376
+ return result;
377
+ }
378
+ getTableName(node) {
379
+ if (!node)
380
+ return "";
381
+ if (node.kind === "TableNode") {
382
+ return node.table?.identifier?.name || node.table?.name || "";
383
+ }
384
+ if (node.table?.kind === "TableNode") {
385
+ return node.table.table?.identifier?.name || node.table.table?.name || "";
386
+ }
387
+ return node.table?.name || node.table?.identifier?.name || node.name || "";
388
+ }
389
+ async logOperation(operation, collection, queryNode, result, stashedPrevious = null) {
390
+ if (!this.db) {
391
+ console.warn("[OpacaCMS] AuditLoggingPlugin: No database instance set, skipping log.");
392
+ return;
393
+ }
394
+ try {
395
+ let entityId = "unknown";
396
+ let newData = null;
397
+ let previousData = null;
398
+ if (queryNode.kind === "InsertQueryNode") {
399
+ entityId = result.insertId?.toString() || "new";
400
+ const columns = queryNode.columns;
401
+ const valuesNode = queryNode.values;
402
+ if (Array.isArray(columns) && valuesNode?.kind === "ValuesNode") {
403
+ const row = valuesNode.values?.[0]?.values;
404
+ if (Array.isArray(row)) {
405
+ newData = {};
406
+ for (let i = 0;i < columns.length; i++) {
407
+ const col = columns[i]?.column?.name || columns[i]?.name;
408
+ if (col) {
409
+ const node = row[i];
410
+ newData[col] = node?.value !== undefined ? node.value : node?.name || node?.identifier?.name || null;
411
+ }
412
+ }
413
+ }
414
+ }
415
+ if ((entityId === "new" || entityId === "unknown") && stashedPrevious?.id) {
416
+ entityId = stashedPrevious.id.toString();
417
+ }
418
+ } else if (queryNode.kind === "UpdateQueryNode") {
419
+ entityId = "updated";
420
+ const where = queryNode.where;
421
+ const filter = where?.where;
422
+ if (filter?.kind === "BinaryOperationNode") {
423
+ const op = filter;
424
+ const leftNode = op.left;
425
+ const leftName = leftNode?.column?.name || leftNode?.name || leftNode?.identifier?.name;
426
+ if (op.operator?.operator === "=" && leftName === "id") {
427
+ const right = op.right;
428
+ entityId = (right?.value !== undefined ? right.value : right?.name || right?.identifier?.name)?.toString() || "updated";
429
+ }
430
+ }
431
+ if (entityId === "updated" && stashedPrevious?.id) {
432
+ entityId = stashedPrevious.id.toString();
433
+ }
434
+ const updates = queryNode.updates;
435
+ if (Array.isArray(updates)) {
436
+ newData = {};
437
+ for (const upd of updates) {
438
+ const col = upd.column?.column?.name || upd.column?.name;
439
+ if (col) {
440
+ const valNode = upd.value;
441
+ newData[col] = valNode?.value !== undefined ? valNode.value : valNode?.name || valNode?.identifier?.name || null;
442
+ }
443
+ }
444
+ }
445
+ if (entityId !== "updated" && entityId !== "unknown") {
446
+ previousData = stashedPrevious;
447
+ if (previousData) {
448
+ console.debug(`[AuditLoggingPlugin] Used stashed previous data for ${entityId}`);
449
+ }
450
+ if (!previousData) {
451
+ previousData = getPreviousDataFromContext();
452
+ }
453
+ if (!previousData) {
454
+ try {
455
+ previousData = await this.db.selectFrom(collection).selectAll().where("id", "=", entityId).executeTakeFirst();
456
+ if (previousData) {
457
+ console.debug(`[AuditLoggingPlugin] Fallback SELECT fetched data for ${entityId}`);
458
+ }
459
+ } catch (e) {}
460
+ }
461
+ }
462
+ } else if (queryNode.kind === "DeleteQueryNode") {
463
+ entityId = "deleted";
464
+ }
465
+ await this.db.insertInto(this.auditTable).values({
466
+ id: crypto.randomUUID(),
467
+ operation,
468
+ collection,
469
+ entity_id: entityId,
470
+ user_id: (this.getUserId ? this.getUserId() : null) || getUserIdFromContext(),
471
+ previous_data: previousData ? JSON.stringify(previousData) : null,
472
+ new_data: newData ? JSON.stringify(newData) : "{}",
473
+ timestamp: new Date().toISOString(),
474
+ created_at: new Date().toISOString(),
475
+ updated_at: new Date().toISOString()
476
+ }).execute();
477
+ } catch (err) {
478
+ console.error("[OpacaCMS] Failed to write audit log:", err);
479
+ }
480
+ }
481
+ }
482
+ var init_audit_logging = __esm(() => {
483
+ init_context();
484
+ });
485
+
486
+ // src/db/kysely/plugins/auto-timestamps.ts
487
+ class AutoTimestampsPlugin {
488
+ createdAtColumn;
489
+ updatedAtColumn;
490
+ constructor(config = {}) {
491
+ this.createdAtColumn = config.createdAtColumn || "created_at";
492
+ this.updatedAtColumn = config.updatedAtColumn || "updated_at";
493
+ }
494
+ transformQuery(args) {
495
+ const { node } = args;
496
+ const now = new Date().toISOString();
497
+ if (node.kind === "InsertQueryNode") {} else if (node.kind === "UpdateQueryNode") {}
498
+ return node;
499
+ }
500
+ async transformResult(args) {
501
+ return args.result;
502
+ }
503
+ }
504
+
505
+ // src/db/kysely/plugins/cursor-pagination.ts
506
+ class CursorPaginationPlugin {
507
+ after;
508
+ cursorColumn;
509
+ constructor(config) {
510
+ this.after = config.after;
511
+ this.cursorColumn = config.cursorColumn || "id";
512
+ }
513
+ transformQuery(args) {
514
+ if (this.after === undefined)
515
+ return args.node;
516
+ const { node } = args;
517
+ if (node.kind === "SelectQueryNode") {}
518
+ return node;
519
+ }
520
+ async transformResult(args) {
521
+ return args.result;
522
+ }
523
+ }
524
+
525
+ // src/db/kysely/plugins/deadlock-handler.ts
526
+ class DeadlockRetryPlugin {
527
+ maxRetries;
528
+ initialDelay;
529
+ constructor(config) {
530
+ this.maxRetries = config.maxRetries ?? 3;
531
+ this.initialDelay = config.initialDelay ?? 50;
532
+ }
533
+ transformQuery(args) {
534
+ return args.node;
535
+ }
536
+ async transformResult(args) {
537
+ return args.result;
538
+ }
539
+ async retry(fn) {
540
+ let lastError;
541
+ for (let attempt = 0;attempt <= this.maxRetries; attempt++) {
542
+ try {
543
+ return await fn();
544
+ } catch (error) {
545
+ lastError = error;
546
+ const isLockError = error?.message?.includes("SQLITE_BUSY") || error?.message?.includes("database is locked") || error?.code === "SQLITE_BUSY";
547
+ if (!isLockError || attempt >= this.maxRetries) {
548
+ throw error;
549
+ }
550
+ const delay = this.initialDelay * 2 ** attempt;
551
+ await new Promise((resolve) => setTimeout(resolve, delay));
552
+ }
553
+ }
554
+ throw lastError;
555
+ }
556
+ }
557
+
558
+ // src/db/kysely/plugins/draft-swapper.ts
559
+ class DraftSwapperPlugin {
560
+ draftMode;
561
+ tables;
562
+ constructor(config) {
563
+ this.draftMode = config.draftMode ?? false;
564
+ const allSlugs = [
565
+ ...config.collections?.map((c) => c.slug) || [],
566
+ ...config.globals?.map((g) => g.slug) || []
567
+ ];
568
+ this.tables = new Set(allSlugs.map((s) => toSnakeCase(s)));
569
+ }
570
+ transformQuery(args) {
571
+ if (!this.draftMode)
572
+ return args.node;
573
+ const { node } = args;
574
+ if (node.kind === "SelectQueryNode") {
575
+ if (node.from && node.from.froms) {
576
+ let shouldSwap = false;
577
+ let originalTable = "";
578
+ const newFroms = node.from.froms.map((f) => {
579
+ if (f.kind === "TableNode" && f.table && f.table.kind === "IdentifierNode") {
580
+ const tableName = f.table.name;
581
+ if (this.tables?.has(tableName)) {
582
+ shouldSwap = true;
583
+ originalTable = tableName;
584
+ return {
585
+ ...f,
586
+ table: {
587
+ ...f.table,
588
+ name: "_doc_versions"
589
+ }
590
+ };
591
+ }
592
+ }
593
+ return f;
594
+ });
595
+ if (shouldSwap) {
596
+ node.from.froms = newFroms;
597
+ }
598
+ }
599
+ }
600
+ return node;
601
+ }
602
+ async transformResult(args) {
603
+ const { result } = args;
604
+ if (!this.draftMode || result.rows.length === 0)
605
+ return result;
606
+ if (result.rows[0] && "data" in result.rows[0] && "collection" in result.rows[0]) {
607
+ const mappedRows = result.rows.map((row) => {
608
+ try {
609
+ const parsedData = typeof row.data === "string" ? JSON.parse(row.data) : row.data;
610
+ return {
611
+ ...parsedData,
612
+ _version_id: row.id,
613
+ _version_status: row.status,
614
+ _version_createdAt: row.createdAt
615
+ };
616
+ } catch {
617
+ return row;
618
+ }
619
+ });
620
+ return { ...result, rows: mappedRows };
621
+ }
622
+ return result;
623
+ }
624
+ }
625
+ var init_draft_swapper = () => {};
626
+
627
+ // src/db/kysely/plugins/field-masking.ts
628
+ class FieldMaskingPlugin {
629
+ maskedFields = new Map;
630
+ disabled;
631
+ constructor(config) {
632
+ this.disabled = config.disabled ?? false;
633
+ this.parseConfig(config);
634
+ }
635
+ parseConfig(config) {
636
+ const allResources = [...config.collections, ...config.globals || []];
637
+ for (const res of allResources) {
638
+ if (res.slug.startsWith("_"))
639
+ continue;
640
+ const tableName = toSnakeCase(res.slug);
641
+ const fields = new Set;
642
+ const findMaskedFields = (fieldList) => {
643
+ for (const f of fieldList) {
644
+ if (f.name && (f.admin?.hidden === true || f.type === "password")) {
645
+ fields.add(toSnakeCase(f.name));
646
+ }
647
+ if ("fields" in f && Array.isArray(f.fields)) {
648
+ findMaskedFields(f.fields);
649
+ }
650
+ }
651
+ };
652
+ findMaskedFields(res.fields);
653
+ if (fields.size > 0) {
654
+ this.maskedFields.set(tableName, fields);
655
+ }
656
+ }
657
+ }
658
+ transformQuery(args) {
659
+ return args.node;
660
+ }
661
+ async transformResult(args) {
662
+ const { result } = args;
663
+ if (this.disabled)
664
+ return result;
665
+ return {
666
+ ...result,
667
+ rows: result.rows.map((row) => this.maskRow(row))
668
+ };
669
+ }
670
+ maskRow(row) {
671
+ if (!row)
672
+ return row;
673
+ const newRow = { ...row };
674
+ for (const [_, fields] of this.maskedFields.entries()) {
675
+ for (const field of fields) {
676
+ if (field in newRow) {
677
+ if (field.includes("password") || field.includes("hash")) {
678
+ newRow[field] = "********";
679
+ } else {
680
+ delete newRow[field];
681
+ }
682
+ }
683
+ }
684
+ }
685
+ return newRow;
686
+ }
687
+ }
688
+ var init_field_masking = () => {};
689
+
690
+ // src/db/kysely/plugins/fts-normalizer.ts
691
+ class FtsNormalizerPlugin {
692
+ dialect;
693
+ constructor(config) {
694
+ this.dialect = config.dialect;
695
+ }
696
+ transformQuery(args) {
697
+ const { node } = args;
698
+ return node;
699
+ }
700
+ async transformResult(args) {
701
+ return args.result;
702
+ }
703
+ }
704
+
705
+ // src/db/kysely/plugins/i18n-fallback.ts
706
+ class AutoTranslationFallbackPlugin {
707
+ localizedFields = new Map;
708
+ config;
709
+ constructor(config) {
710
+ this.config = config;
711
+ this.parseConfig(config);
712
+ }
713
+ parseConfig(config) {
714
+ const allResources = [...config.collections, ...config.globals || []];
715
+ for (const res of allResources) {
716
+ const tableName = toSnakeCase(res.slug);
717
+ const fields = new Set;
718
+ const findLocalizedFields = (fieldList) => {
719
+ for (const f of fieldList) {
720
+ if (f.name && f.localized) {
721
+ fields.add(toSnakeCase(f.name));
722
+ }
723
+ if ("fields" in f && Array.isArray(f.fields)) {
724
+ findLocalizedFields(f.fields);
725
+ }
726
+ }
727
+ };
728
+ findLocalizedFields(res.fields);
729
+ if (fields.size > 0) {
730
+ this.localizedFields.set(tableName, fields);
731
+ }
732
+ }
733
+ }
734
+ transformQuery(args) {
735
+ return args.node;
736
+ }
737
+ async transformResult(args) {
738
+ const { result } = args;
739
+ if (this.config.defaultLocale === this.config.currentLocale)
740
+ return result;
741
+ return {
742
+ ...result,
743
+ rows: result.rows.map((row) => this.fallbackRow(row))
744
+ };
745
+ }
746
+ fallbackRow(row) {
747
+ if (!row)
748
+ return row;
749
+ const newRow = { ...row };
750
+ const currentLocSuffix = `_${this.config.currentLocale}`;
751
+ const defaultLocSuffix = `_${this.config.defaultLocale}`;
752
+ for (const [key, value] of Object.entries(newRow)) {
753
+ if (key.endsWith(currentLocSuffix)) {
754
+ const baseName = key.slice(0, -currentLocSuffix.length);
755
+ const defaultValueKey = `${baseName}${defaultLocSuffix}`;
756
+ if ((value === null || value === "" || value === undefined) && newRow[defaultValueKey] !== undefined) {
757
+ newRow[key] = newRow[defaultValueKey];
758
+ }
759
+ }
760
+ }
761
+ return newRow;
762
+ }
763
+ }
764
+ var init_i18n_fallback = () => {};
765
+
766
+ // src/db/kysely/plugins/id-generation.ts
767
+ class IdGenerationPlugin {
768
+ generateId;
769
+ idColumn;
770
+ constructor(config = {}) {
771
+ this.generateId = config.generateId || (() => crypto.randomUUID());
772
+ this.idColumn = config.idColumn || "id";
773
+ }
774
+ transformQuery(args) {
775
+ const { node } = args;
776
+ if (node.kind === "InsertQueryNode") {}
777
+ return node;
778
+ }
779
+ async transformResult(args) {
780
+ return args.result;
781
+ }
782
+ }
783
+
784
+ // src/db/kysely/plugins/json-flattener.ts
785
+ class JsonFlattenerPlugin {
786
+ jsonFields = new Map;
787
+ constructor(config) {
788
+ this.parseConfig(config);
789
+ }
790
+ parseConfig(config) {
791
+ const allResources = [...config.collections, ...config.globals || []];
792
+ for (const res of allResources) {
793
+ const tableName = toSnakeCase(res.slug);
794
+ const fields = new Set;
795
+ const findJsonFields = (fieldList) => {
796
+ for (const f of fieldList) {
797
+ if (f.name && (["richtext", "json", "file", "blocks"].includes(f.type) || f.localized)) {
798
+ fields.add(toSnakeCase(f.name));
799
+ }
800
+ if ("fields" in f && Array.isArray(f.fields)) {
801
+ findJsonFields(f.fields);
802
+ }
803
+ if ("blocks" in f && Array.isArray(f.blocks)) {
804
+ for (const b of f.blocks) {
805
+ if (b.fields)
806
+ findJsonFields(b.fields);
807
+ }
808
+ }
809
+ if ("tabs" in f && Array.isArray(f.tabs)) {
810
+ for (const t of f.tabs) {
811
+ if (t.fields)
812
+ findJsonFields(t.fields);
813
+ }
814
+ }
815
+ }
816
+ };
817
+ findJsonFields(res.fields);
818
+ if (fields.size > 0) {
819
+ this.jsonFields.set(tableName, fields);
820
+ }
821
+ }
822
+ }
823
+ transformQuery(args) {
824
+ const { node } = args;
825
+ if (node.kind === "InsertQueryNode" || node.kind === "UpdateQueryNode") {}
826
+ return node;
827
+ }
828
+ async transformResult(args) {
829
+ const { result } = args;
830
+ return {
831
+ ...result,
832
+ rows: result.rows.map((row) => this.mapRow(row))
833
+ };
834
+ }
835
+ mapRow(row) {
836
+ if (!row)
837
+ return row;
838
+ const newRow = { ...row };
839
+ const isSystemTable = Object.keys(newRow).some((k) => k.startsWith("_"));
840
+ if (isSystemTable)
841
+ return newRow;
842
+ for (const [key, value] of Object.entries(newRow)) {
843
+ if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
844
+ try {
845
+ newRow[key] = JSON.parse(value);
846
+ } catch (_e) {}
847
+ }
848
+ }
849
+ return newRow;
850
+ }
851
+ }
852
+ var init_json_flattener = () => {};
853
+
854
+ // src/db/kysely/plugins/relationship-preloading.ts
855
+ class RelationshipPreloadingPlugin {
856
+ maxDepth;
857
+ constructor(config = {}) {
858
+ this.maxDepth = config.maxDepth ?? 2;
859
+ }
860
+ transformQuery(args) {
861
+ return args.node;
862
+ }
863
+ async transformResult(args) {
864
+ const { result } = args;
865
+ if (result.rows.length === 0)
866
+ return result;
867
+ return result;
868
+ }
869
+ }
870
+
871
+ // src/db/kysely/plugins/slug-generation.ts
872
+ class SlugGenerationPlugin {
873
+ sourceColumn;
874
+ slugColumn;
875
+ constructor(config = {}) {
876
+ this.sourceColumn = config.sourceColumn || "title";
877
+ this.slugColumn = config.slugColumn || "slug";
878
+ }
879
+ transformQuery(args) {
880
+ const { node } = args;
881
+ if (node.kind === "InsertQueryNode" || node.kind === "UpdateQueryNode") {}
882
+ return node;
883
+ }
884
+ async transformResult(args) {
885
+ return args.result;
886
+ }
887
+ slugify(text) {
888
+ return text.toString().toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
889
+ }
890
+ }
891
+
892
+ // src/db/kysely/plugins/soft-delete.ts
893
+ class SoftDeletePlugin {
894
+ deletedAtColumn;
895
+ tables;
896
+ constructor(config = {}) {
897
+ this.deletedAtColumn = config.deletedAtColumn || "deleted_at";
898
+ this.tables = config.tables ? new Set(config.tables) : null;
899
+ }
900
+ transformQuery(args) {
901
+ const { node } = args;
902
+ if (node.kind === "DeleteQueryNode") {} else if (node.kind === "SelectQueryNode") {}
903
+ return node;
904
+ }
905
+ async transformResult(args) {
906
+ return args.result;
907
+ }
908
+ }
909
+
910
+ // src/db/kysely/plugins/tree-resolver.ts
911
+ class TreeResolverPlugin {
912
+ parentColumn;
913
+ childrenProperty;
914
+ constructor(config) {
915
+ this.parentColumn = config.parentColumn || "parent_id";
916
+ this.childrenProperty = config.childrenProperty || "children";
917
+ }
918
+ transformQuery(args) {
919
+ return args.node;
920
+ }
921
+ async transformResult(args) {
922
+ const { result } = args;
923
+ if (result.rows.length === 0)
924
+ return result;
925
+ return {
926
+ ...result,
927
+ rows: this.buildTree(result.rows)
928
+ };
929
+ }
930
+ buildTree(rows) {
931
+ const map = new Map;
932
+ const roots = [];
933
+ for (const row of rows) {
934
+ map.set(row.id, { ...row, [this.childrenProperty]: [] });
935
+ }
936
+ for (const row of rows) {
937
+ const parentId = row[this.parentColumn];
938
+ const item = map.get(row.id);
939
+ if (parentId && map.has(parentId)) {
940
+ map.get(parentId)[this.childrenProperty].push(item);
941
+ } else {
942
+ roots.push(item);
943
+ }
944
+ }
945
+ return roots;
946
+ }
947
+ }
948
+
949
+ // src/db/kysely/plugins/virtual-field-resolver.ts
950
+ class VirtualFieldResolverPlugin {
951
+ resolvers = new Map;
952
+ config;
953
+ constructor(config) {
954
+ this.config = config;
955
+ this.parseConfig(config);
956
+ }
957
+ parseConfig(config) {
958
+ const allResources = [...config.collections, ...config.globals || []];
959
+ for (const res of allResources) {
960
+ const tableName = toSnakeCase(res.slug);
961
+ const resResolvers = new Map;
962
+ const findResolvers = (fieldList) => {
963
+ for (const f of fieldList) {
964
+ if (f.name && f.type === "virtual" && typeof f.resolve === "function") {
965
+ resResolvers.set(f.name, f.resolve);
966
+ } else if (f.name && f.resolve && typeof f.resolve === "function") {
967
+ resResolvers.set(f.name, f.resolve);
968
+ }
969
+ if ("fields" in f && Array.isArray(f.fields)) {
970
+ findResolvers(f.fields);
971
+ }
972
+ }
973
+ };
974
+ findResolvers(res.fields);
975
+ if (resResolvers.size > 0) {
976
+ this.resolvers.set(tableName, resResolvers);
977
+ }
978
+ }
979
+ }
980
+ transformQuery(args) {
981
+ return args.node;
982
+ }
983
+ async transformResult(args) {
984
+ const { result } = args;
985
+ if (result.rows.length === 0)
986
+ return result;
987
+ const rows = await Promise.all(result.rows.map(async (row) => {
988
+ const newRow = { ...row };
989
+ for (const [tableName, tableResolvers] of this.resolvers.entries()) {
990
+ for (const [fieldName, resolveFn] of tableResolvers.entries()) {
991
+ try {
992
+ newRow[fieldName] = await resolveFn({
993
+ data: row,
994
+ req: this.config.req,
995
+ user: this.config.user,
996
+ session: this.config.session,
997
+ apiKey: this.config.apiKey
998
+ });
999
+ } catch (e) {
1000
+ console.error(`Error resolving virtual field ${fieldName} for table ${tableName}:`, e);
1001
+ }
1002
+ }
1003
+ }
1004
+ return newRow;
1005
+ }));
1006
+ return {
1007
+ ...result,
1008
+ rows
1009
+ };
1010
+ }
1011
+ }
1012
+ var init_virtual_field_resolver = () => {};
1013
+
1014
+ // src/db/kysely/plugins/zod-coercion.ts
1015
+ class ZodCoercionPlugin {
1016
+ schemas;
1017
+ constructor(config) {
1018
+ this.schemas = config.schemas;
1019
+ }
1020
+ transformQuery(args) {
1021
+ return args.node;
1022
+ }
1023
+ async transformResult(args) {
1024
+ const { result } = args;
1025
+ if (result.rows.length === 0)
1026
+ return result;
1027
+ const coercedRows = result.rows.map((row) => {
1028
+ return row;
1029
+ });
1030
+ return {
1031
+ ...result,
1032
+ rows: coercedRows
1033
+ };
1034
+ }
1035
+ }
1036
+
1037
+ // src/db/kysely/plugins/index.ts
1038
+ var init_plugins = __esm(() => {
1039
+ init_audit_logging();
1040
+ init_draft_swapper();
1041
+ init_field_masking();
1042
+ init_i18n_fallback();
1043
+ init_json_flattener();
1044
+ init_virtual_field_resolver();
1045
+ });
1046
+
1047
+ // src/db/kysely/factory.ts
1048
+ var exports_factory = {};
1049
+ __export(exports_factory, {
1050
+ createOpacaKysely: () => createOpacaKysely
1051
+ });
1052
+ import { CamelCasePlugin, Kysely } from "kysely";
1053
+ function createOpacaKysely(options) {
1054
+ const { dialect, config, isAdmin = false } = options;
1055
+ const plugins = [
1056
+ new JsonFlattenerPlugin({
1057
+ collections: config.collections,
1058
+ globals: config.globals
1059
+ }),
1060
+ new ZodCoercionPlugin({
1061
+ schemas: {}
1062
+ }),
1063
+ new AutoTimestampsPlugin,
1064
+ new IdGenerationPlugin,
1065
+ new SlugGenerationPlugin,
1066
+ new AuditLoggingPlugin({
1067
+ auditTable: "_audit_logs",
1068
+ getUserId: options.getUserId
1069
+ }),
1070
+ new SoftDeletePlugin,
1071
+ new DraftSwapperPlugin({
1072
+ draftMode: false,
1073
+ collections: config.collections,
1074
+ globals: config.globals
1075
+ }),
1076
+ new AutoTranslationFallbackPlugin({
1077
+ collections: config.collections,
1078
+ globals: config.globals,
1079
+ defaultLocale: config.i18n?.defaultLocale || "en",
1080
+ currentLocale: config.i18n?.defaultLocale || "en"
1081
+ }),
1082
+ new CursorPaginationPlugin({}),
1083
+ new FtsNormalizerPlugin({
1084
+ dialect: config.db.name
1085
+ }),
1086
+ new TreeResolverPlugin({}),
1087
+ new VirtualFieldResolverPlugin({
1088
+ collections: config.collections,
1089
+ globals: config.globals
1090
+ }),
1091
+ new RelationshipPreloadingPlugin({}),
1092
+ new DeadlockRetryPlugin({ maxRetries: 5 })
1093
+ ];
1094
+ const db = new Kysely({
1095
+ dialect,
1096
+ plugins: [new CamelCasePlugin, ...plugins]
1097
+ });
1098
+ for (const plugin of plugins) {
1099
+ if (plugin && typeof plugin.setDb === "function") {
1100
+ plugin.setDb(db);
1101
+ }
1102
+ }
1103
+ return db;
1104
+ }
1105
+ var init_factory = __esm(() => {
1106
+ init_plugins();
1107
+ });
1108
+
1109
+ // src/db/bun-sqlite.ts
1110
+ import { FileMigrationProvider, Migrator, sql as sql2 } from "kysely";
1111
+
1112
+ // src/db/adapter.ts
1113
+ class BaseDatabaseAdapter {
1114
+ get raw() {
1115
+ return;
1116
+ }
1117
+ get db() {
1118
+ return;
1119
+ }
1120
+ push;
1121
+ pushDestructive;
1122
+ migrationDir;
1123
+ }
1124
+
1125
+ // src/db/kysely/data-mapper.ts
1126
+ function toCamelCase(str) {
1127
+ if (str.startsWith("_"))
1128
+ return str;
1129
+ return str.replace(/_+([a-z])/g, (_, letter) => letter.toUpperCase());
1130
+ }
1131
+ function flattenPayload(payload, prefix = "", excludeKeys = []) {
1132
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload))
1133
+ return payload;
1134
+ const result = {};
1135
+ for (const key in payload) {
1136
+ if (Object.hasOwn(payload, key)) {
1137
+ const value = payload[key];
1138
+ if (value === undefined)
1139
+ continue;
1140
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !excludeKeys.includes(key)) {
1141
+ if (key.startsWith("_") && prefix === "") {
1142
+ result[key] = value;
1143
+ } else {
1144
+ const flatNested = flattenPayload(value, `${prefix}${key}__`);
1145
+ Object.assign(result, flatNested);
1146
+ }
1147
+ } else {
1148
+ if (value === undefined)
1149
+ continue;
1150
+ const colName = `${prefix}${toSnakeCase(key)}`;
1151
+ if (typeof value === "object" && value !== null && !(value instanceof Date)) {
1152
+ result[colName] = JSON.stringify(value);
1153
+ } else {
1154
+ result[colName] = value;
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1159
+ return result;
1160
+ }
1161
+ function unflattenRow(row) {
1162
+ if (typeof row !== "object" || row === null || Array.isArray(row))
1163
+ return row;
1164
+ const result = {};
1165
+ for (const key in row) {
1166
+ if (Object.hasOwn(row, key)) {
1167
+ let value = row[key];
1168
+ if (typeof value === "string") {
1169
+ const trimmed = value.trim();
1170
+ if (trimmed.startsWith("[") && trimmed.endsWith("]") || trimmed.startsWith("{") && trimmed.endsWith("}")) {
1171
+ try {
1172
+ value = JSON.parse(value);
1173
+ } catch (e) {}
1174
+ }
1175
+ }
1176
+ if (key === "created_at" || key === "updated_at" || key === "createdAt" || key === "updatedAt") {
1177
+ if (typeof value === "string" && !value.includes("T") && value.includes(" ")) {
1178
+ value = `${value.replace(" ", "T")}Z`;
1179
+ }
1180
+ result[toCamelCase(key)] = value;
1181
+ continue;
1182
+ }
1183
+ if (key.startsWith("_")) {
1184
+ result[key] = value;
1185
+ continue;
1186
+ }
1187
+ const parts = key.split("__");
1188
+ if (parts.length === 1) {
1189
+ result[toCamelCase(key)] = value;
1190
+ continue;
1191
+ }
1192
+ let current = result;
1193
+ let collision = false;
1194
+ const path = [];
1195
+ for (let i = 0;i < parts.length - 1; i++) {
1196
+ const part = toCamelCase(parts[i]);
1197
+ if (current[part] !== undefined && (typeof current[part] !== "object" || current[part] === null)) {
1198
+ collision = true;
1199
+ break;
1200
+ }
1201
+ path.push({ obj: current, key: part });
1202
+ if (!current[part]) {
1203
+ current[part] = {};
1204
+ }
1205
+ current = current[part];
1206
+ }
1207
+ if (!collision) {
1208
+ const lastPart = toCamelCase(parts[parts.length - 1]);
1209
+ if (current[lastPart] !== undefined && typeof current[lastPart] === "object") {
1210
+ collision = true;
1211
+ } else {
1212
+ current[lastPart] = value;
1213
+ }
1214
+ }
1215
+ if (collision) {
1216
+ result[toCamelCase(key)] = value;
1217
+ }
1218
+ }
1219
+ }
1220
+ return result;
1221
+ }
1222
+ // src/db/kysely/query-builder.ts
1223
+ function buildKyselyWhere(eb, query) {
1224
+ if (!query || Object.keys(query).length === 0)
1225
+ return;
1226
+ const conditions = [];
1227
+ for (const [key, value] of Object.entries(query)) {
1228
+ if (value === undefined)
1229
+ continue;
1230
+ if (key === "or" && Array.isArray(value)) {
1231
+ const orConditions = value.map((v) => buildKyselyWhere(eb, v)).filter((c) => c !== undefined);
1232
+ if (orConditions.length > 0) {
1233
+ conditions.push(eb.or(orConditions));
1234
+ }
1235
+ continue;
1236
+ }
1237
+ if (key === "and" && Array.isArray(value)) {
1238
+ const andConditions = value.map((v) => buildKyselyWhere(eb, v)).filter((c) => c !== undefined);
1239
+ if (andConditions.length > 0) {
1240
+ conditions.push(eb.and(andConditions));
1241
+ }
1242
+ continue;
1243
+ }
1244
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
1245
+ for (const [op, val] of Object.entries(value)) {
1246
+ if (val === undefined)
1247
+ continue;
1248
+ switch (op) {
1249
+ case "gt":
1250
+ conditions.push(eb(key, ">", val));
1251
+ break;
1252
+ case "gte":
1253
+ conditions.push(eb(key, ">=", val));
1254
+ break;
1255
+ case "lt":
1256
+ conditions.push(eb(key, "<", val));
1257
+ break;
1258
+ case "lte":
1259
+ conditions.push(eb(key, "<=", val));
1260
+ break;
1261
+ case "like":
1262
+ conditions.push(eb(key, "like", String(val)));
1263
+ break;
1264
+ case "ne":
1265
+ if (val === null)
1266
+ conditions.push(eb(key, "is not", null));
1267
+ else
1268
+ conditions.push(eb(key, "!=", val));
1269
+ break;
1270
+ case "in":
1271
+ if (Array.isArray(val))
1272
+ conditions.push(eb(key, "in", val));
1273
+ break;
1274
+ case "is":
1275
+ if (val === null)
1276
+ conditions.push(eb(key, "is", null));
1277
+ break;
1278
+ default:
1279
+ if (val === null)
1280
+ conditions.push(eb(key, "is", null));
1281
+ else
1282
+ conditions.push(eb(key, "=", val));
1283
+ break;
1284
+ }
1285
+ }
1286
+ } else {
1287
+ if (value === null) {
1288
+ conditions.push(eb(key, "is", null));
1289
+ } else {
1290
+ conditions.push(eb(key, "=", value));
1291
+ }
1292
+ }
1293
+ }
1294
+ if (conditions.length === 0)
1295
+ return;
1296
+ if (conditions.length === 1)
1297
+ return conditions[0];
1298
+ return eb.and(conditions);
1299
+ }
1300
+
1301
+ // src/db/kysely/schema-builder.ts
1302
+ import { sql } from "kysely";
1303
+
1304
+ // src/db/kysely/sql-utils.ts
1305
+ function assertSafeIdentifier(identifier) {
1306
+ if (typeof identifier !== "string" || !identifier) {
1307
+ throw new Error(`Invalid identifier: must be a non-empty string. Got: ${identifier}`);
1308
+ }
1309
+ const isValid = /^[a-zA-Z0-9_-]+$/.test(identifier);
1310
+ if (!isValid) {
1311
+ throw new Error(`API Error: Unsafe SQL identifier detected: "${identifier}". Identifiers can only contain alphanumeric characters, underscores, and hyphens.`);
1312
+ }
1313
+ }
1314
+ // src/utils/logger.ts
1315
+ var RESET = "\x1B[0m";
1316
+ var BOLD = "\x1B[1m";
1317
+ var BLUE = "\x1B[34m";
1318
+ var GREEN = "\x1B[32m";
1319
+ var YELLOW = "\x1B[33m";
1320
+ var RED = "\x1B[31m";
1321
+ var GRAY = "\x1B[90m";
1322
+ var PREFIX = `${BLUE}${BOLD}[OpacaCMS]${RESET}`;
1323
+ var LOG_LEVELS = {
1324
+ debug: 0,
1325
+ info: 1,
1326
+ warn: 2,
1327
+ error: 3
1328
+ };
1329
+
1330
+ class OpacaLogger {
1331
+ config;
1332
+ constructor(config = {}) {
1333
+ this.config = config;
1334
+ }
1335
+ shouldLog(level) {
1336
+ if (this.config.disabled)
1337
+ return false;
1338
+ const configLevel = this.config.level || "info";
1339
+ return LOG_LEVELS[level] >= LOG_LEVELS[configLevel];
1340
+ }
1341
+ info(message, ...args) {
1342
+ if (!this.shouldLog("info"))
1343
+ return;
1344
+ console.log(`${PREFIX} ${message}`, ...args);
1345
+ }
1346
+ success(message, ...args) {
1347
+ if (!this.shouldLog("info"))
1348
+ return;
1349
+ console.log(`${PREFIX} ${GREEN}${message}${RESET}`, ...args);
1350
+ }
1351
+ debug(message, ...args) {
1352
+ if (!this.shouldLog("debug"))
1353
+ return;
1354
+ console.log(`${PREFIX} ${GRAY}${message}${RESET}`, ...args);
1355
+ }
1356
+ warn(message, ...args) {
1357
+ if (!this.shouldLog("warn"))
1358
+ return;
1359
+ console.warn(`${PREFIX} ${YELLOW}Warning: ${message}${RESET}`, ...args);
1360
+ }
1361
+ error(message, ...args) {
1362
+ if (!this.shouldLog("error"))
1363
+ return;
1364
+ console.error(`${PREFIX} ${RED}Error: ${message}${RESET}`, ...args);
1365
+ }
1366
+ log(message, ...args) {
1367
+ if (this.config.disabled)
1368
+ return;
1369
+ console.log(message, ...args);
1370
+ }
1371
+ bold(msg) {
1372
+ if (this.config.disableColors)
1373
+ return msg;
1374
+ return `${BOLD}${msg}${RESET}`;
1375
+ }
1376
+ format(color, msg) {
1377
+ if (this.config.disableColors)
1378
+ return msg;
1379
+ switch (color) {
1380
+ case "green":
1381
+ return `${GREEN}${msg}${RESET}`;
1382
+ case "red":
1383
+ return `${RED}${msg}${RESET}`;
1384
+ case "yellow":
1385
+ return `${YELLOW}${msg}${RESET}`;
1386
+ case "gray":
1387
+ return `${GRAY}${msg}${RESET}`;
1388
+ default:
1389
+ return msg;
1390
+ }
1391
+ }
1392
+ }
1393
+ var logger = new OpacaLogger;
1394
+
1395
+ // src/db/kysely/schema-builder.ts
1396
+ async function pushSchema(db, dialect, collections, globals = [], options = {}) {
1397
+ const rawSchemas = [...getSystemCollections(), ...collections, ...globals];
1398
+ const allSchemas = [];
1399
+ const seenSlugs = new Set;
1400
+ for (const schema of rawSchemas) {
1401
+ if (!seenSlugs.has(schema.slug)) {
1402
+ seenSlugs.add(schema.slug);
1403
+ allSchemas.push(schema);
1404
+ }
1405
+ }
1406
+ const { pushDestructive } = options;
1407
+ logger.debug(`Starting schema push via Kysely (${dialect})...`);
1408
+ const mapType = dialect === "postgres" ? mapFieldToPostgresType : mapFieldToSQLiteType;
1409
+ let tablesMetadata = [];
1410
+ try {
1411
+ tablesMetadata = await db.introspection.getTables();
1412
+ } catch (e) {
1413
+ logger.warn("Failed to fetch database introspection. Falling back to empty state.", e);
1414
+ }
1415
+ const existingTableMap = new Map(tablesMetadata.map((t) => [t.name, t]));
1416
+ const existingTables = Array.from(existingTableMap.keys());
1417
+ logger.debug(`Existing tables found: ${existingTables.join(", ")}`);
1418
+ const expectedTableNames = new Set;
1419
+ for (const collection of allSchemas) {
1420
+ const slug = collection.slug;
1421
+ const tableName = toSnakeCase(slug);
1422
+ assertSafeIdentifier(tableName);
1423
+ expectedTableNames.add(tableName);
1424
+ const flattenedFields = flattenFields(collection.fields);
1425
+ const hasTimestamps = true;
1426
+ const versions = collection.versions;
1427
+ const existingTable = existingTableMap.get(tableName);
1428
+ if (!existingTable) {
1429
+ logger.info(` ${logger.format("green", "✔")} Creating table: ${logger.bold(`"${tableName}"`)}`);
1430
+ let builder = db.schema.createTable(tableName).ifNotExists();
1431
+ builder = builder.addColumn("id", "text", (col) => col.primaryKey());
1432
+ for (const field of flattenedFields) {
1433
+ if (field.type === "relationship" && "hasMany" in field && field.hasMany)
1434
+ continue;
1435
+ const colName = toSnakeCase(field.name);
1436
+ if (colName === "id")
1437
+ continue;
1438
+ assertSafeIdentifier(colName);
1439
+ const colType = mapType(field);
1440
+ builder = builder.addColumn(colName, colType, (col) => {
1441
+ let c = col;
1442
+ if (field.unique)
1443
+ c = c.unique();
1444
+ if (field.required)
1445
+ c = c.notNull();
1446
+ if (field.defaultValue !== undefined)
1447
+ c = c.defaultTo(field.defaultValue);
1448
+ return c;
1449
+ });
1450
+ }
1451
+ if (versions?.drafts) {
1452
+ builder = builder.addColumn("_status", "text", (col) => col.defaultTo("draft"));
1453
+ }
1454
+ if (hasTimestamps) {
1455
+ const ts = collection.timestamps ?? {};
1456
+ const config = typeof ts === "object" ? ts : {};
1457
+ const createdField = toSnakeCase(config.createdAt ?? "createdAt");
1458
+ const updatedField = toSnakeCase(config.updatedAt ?? "updatedAt");
1459
+ const tsType = dialect === "postgres" ? "timestamptz" : "text";
1460
+ builder = builder.addColumn(createdField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
1461
+ builder = builder.addColumn(updatedField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
1462
+ }
1463
+ await builder.execute();
1464
+ } else {
1465
+ const existingColSet = new Set(existingTable.columns.map((c) => c.name));
1466
+ for (const field of flattenedFields) {
1467
+ if (field.type === "relationship" && "hasMany" in field && field.hasMany)
1468
+ continue;
1469
+ const colName = toSnakeCase(field.name);
1470
+ if (colName === "id")
1471
+ continue;
1472
+ assertSafeIdentifier(colName);
1473
+ if (!existingColSet.has(colName)) {
1474
+ logger.info(` ${logger.format("green", "✔")} Adding column: ${logger.bold(`${tableName}.${colName}`)}`);
1475
+ const colType = mapType(field);
1476
+ await db.schema.alterTable(tableName).addColumn(colName, colType).execute();
1477
+ }
1478
+ }
1479
+ if (versions?.drafts && !existingColSet.has("_status")) {
1480
+ logger.info(` ${logger.format("green", "✔")} Adding status column to: ${tableName}`);
1481
+ await db.schema.alterTable(tableName).addColumn("_status", "text", (col) => col.defaultTo("draft")).execute();
1482
+ }
1483
+ if (hasTimestamps) {
1484
+ const ts = collection.timestamps ?? {};
1485
+ const config = typeof ts === "object" ? ts : {};
1486
+ const createdField = toSnakeCase(config.createdAt ?? "createdAt");
1487
+ const updatedField = toSnakeCase(config.updatedAt ?? "updatedAt");
1488
+ const tsType = dialect === "postgres" ? "timestamptz" : "text";
1489
+ if (!existingColSet.has(createdField)) {
1490
+ logger.info(` -> Adding missing timestamp column: ${tableName}.${createdField}`);
1491
+ await db.schema.alterTable(tableName).addColumn(createdField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
1492
+ }
1493
+ if (!existingColSet.has(updatedField)) {
1494
+ logger.info(` -> Adding missing timestamp column: ${tableName}.${updatedField}`);
1495
+ await db.schema.alterTable(tableName).addColumn(updatedField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
1496
+ }
1497
+ }
1498
+ if (pushDestructive) {
1499
+ const expectedCols = new Set(flattenedFields.filter((f) => !(f.type === "relationship" && f.hasMany)).map((f) => toSnakeCase(f.name)));
1500
+ if (hasTimestamps) {
1501
+ const ts = collection.timestamps ?? {};
1502
+ const config = typeof ts === "object" ? ts : {};
1503
+ expectedCols.add(toSnakeCase(config.createdAt ?? "createdAt"));
1504
+ expectedCols.add(toSnakeCase(config.updatedAt ?? "updatedAt"));
1505
+ }
1506
+ if (versions?.drafts)
1507
+ expectedCols.add("_status");
1508
+ expectedCols.add("id");
1509
+ for (const existingCol of existingColSet) {
1510
+ const colName = existingCol;
1511
+ if (!expectedCols.has(colName)) {
1512
+ logger.info(` -> Dropping stale column: ${logger.format("red", `${tableName}.${colName}`)}`);
1513
+ try {
1514
+ await db.schema.alterTable(tableName).dropColumn(colName).execute();
1515
+ } catch (_) {
1516
+ logger.warn(`Failed to drop column ${colName}.`);
1517
+ }
1518
+ }
1519
+ }
1520
+ }
1521
+ }
1522
+ const relationalFields = getRelationalFields(collection.fields);
1523
+ for (const field of relationalFields) {
1524
+ if (field.type === "relationship" && "hasMany" in field && field.hasMany) {
1525
+ const colName = toSnakeCase(field.name);
1526
+ const joinTableName = `${tableName}_${colName}_relations`.toLowerCase();
1527
+ assertSafeIdentifier(joinTableName);
1528
+ expectedTableNames.add(joinTableName);
1529
+ if (!existingTableMap.has(joinTableName)) {
1530
+ logger.info(` -> Creating relation table: ${logger.format("green", `"${joinTableName}"`)}`);
1531
+ await db.schema.createTable(joinTableName).ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("source_id", "text", (col) => col.notNull()).addColumn("target_id", "text", (col) => col.notNull()).addColumn("order", "integer", (col) => col.defaultTo(0)).execute();
1532
+ }
1533
+ }
1534
+ }
1535
+ for (const field of relationalFields) {
1536
+ if (field.type === "blocks" && field.blocks && Array.isArray(field.blocks)) {
1537
+ for (const block of field.blocks) {
1538
+ const blockName = toSnakeCase(field.name);
1539
+ const blockSlug = toSnakeCase(block.slug);
1540
+ assertSafeIdentifier(blockName);
1541
+ assertSafeIdentifier(blockSlug);
1542
+ const blockTableName = `${tableName}_${blockName}_${blockSlug}`.toLowerCase();
1543
+ assertSafeIdentifier(blockTableName);
1544
+ expectedTableNames.add(blockTableName);
1545
+ const existingBlockTable = existingTableMap.get(blockTableName);
1546
+ if (!existingBlockTable) {
1547
+ logger.info(` -> Creating block table: ${logger.format("green", `"${blockTableName}"`)}`);
1548
+ let bBuilder = db.schema.createTable(blockTableName).ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("_parent_id", "text", (col) => col.notNull()).addColumn("_order", "integer", (col) => col.defaultTo(0)).addColumn("block_type", "text", (col) => col.notNull().defaultTo(block.slug));
1549
+ const blockFlattened = flattenFields(block.fields);
1550
+ for (const bField of blockFlattened) {
1551
+ const bColName = toSnakeCase(bField.name);
1552
+ if (bColName === "id")
1553
+ continue;
1554
+ assertSafeIdentifier(bColName);
1555
+ const bColType = mapType(bField);
1556
+ bBuilder = bBuilder.addColumn(bColName, bColType, (col) => {
1557
+ let c = col;
1558
+ if (bField.unique)
1559
+ c = c.unique();
1560
+ if (bField.required)
1561
+ c = c.notNull();
1562
+ if (bField.defaultValue !== undefined)
1563
+ c = c.defaultTo(bField.defaultValue);
1564
+ return c;
1565
+ });
1566
+ }
1567
+ const tsType = dialect === "postgres" ? "timestamptz" : "text";
1568
+ bBuilder = bBuilder.addColumn("created_at", tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
1569
+ bBuilder = bBuilder.addColumn("updated_at", tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
1570
+ await bBuilder.execute();
1571
+ } else {
1572
+ const existingBColSet = new Set(existingBlockTable.columns.map((c) => c.name));
1573
+ const blockFlattened = flattenFields(block.fields);
1574
+ for (const bField of blockFlattened) {
1575
+ const bColName = toSnakeCase(bField.name);
1576
+ if (bColName === "id")
1577
+ continue;
1578
+ if (!existingBColSet.has(bColName)) {
1579
+ const bColType = mapType(bField);
1580
+ await db.schema.alterTable(blockTableName).addColumn(bColName, bColType).execute();
1581
+ }
1582
+ }
1583
+ if (!existingBColSet.has("_parent_id")) {
1584
+ await db.schema.alterTable(blockTableName).addColumn("_parent_id", "text", (col) => col.notNull().defaultTo("")).execute();
1585
+ }
1586
+ if (!existingBColSet.has("_order")) {
1587
+ await db.schema.alterTable(blockTableName).addColumn("_order", "integer", (col) => col.defaultTo(0)).execute();
1588
+ }
1589
+ if (!existingBColSet.has("block_type")) {
1590
+ await db.schema.alterTable(blockTableName).addColumn("block_type", "text", (col) => col.notNull().defaultTo(block.slug)).execute();
1591
+ }
1592
+ }
1593
+ }
1594
+ }
1595
+ }
1596
+ }
1597
+ if (pushDestructive) {
1598
+ for (const tableName of existingTables) {
1599
+ if (!expectedTableNames.has(tableName) && !tableName.startsWith("better_auth") && !tableName.startsWith("__kysely")) {
1600
+ logger.info(` -> Dropping stale table: ${logger.format("red", `"${tableName}"`)}`);
1601
+ try {
1602
+ await db.schema.dropTable(tableName).ifExists().cascade().execute();
1603
+ } catch (_) {
1604
+ try {
1605
+ await db.schema.dropTable(tableName).ifExists().execute();
1606
+ } catch (_2) {
1607
+ logger.warn(`Failed to drop table ${tableName}`);
1608
+ }
1609
+ }
1610
+ }
1611
+ }
1612
+ }
1613
+ logger.info(`${logger.format("green", "✔")} Schema synchronization complete.`);
1614
+ }
25
1615
 
26
1616
  // src/db/bun-sqlite.ts
27
- import fs from "node:fs/promises";
28
- import path from "node:path";
29
- import { pathToFileURL } from "node:url";
30
- import { FileMigrationProvider, Migrator, sql } from "kysely";
1617
+ init_context();
31
1618
  class BunSQLiteAdapter extends BaseDatabaseAdapter {
32
1619
  path;
33
1620
  name = "bun-sqlite";
@@ -46,9 +1633,9 @@ class BunSQLiteAdapter extends BaseDatabaseAdapter {
46
1633
  throw new Error("Database not connected. Call connect() first.");
47
1634
  return this._db;
48
1635
  }
49
- constructor(path2, options) {
1636
+ constructor(path, options) {
50
1637
  super();
51
- this.path = path2;
1638
+ this.path = path;
52
1639
  this.push = options?.push ?? true;
53
1640
  this.pushDestructive = options?.pushDestructive ?? false;
54
1641
  this.migrationDir = options?.migrationDir ?? "./migrations";
@@ -59,8 +1646,8 @@ class BunSQLiteAdapter extends BaseDatabaseAdapter {
59
1646
  const { Database } = await import("bun:sqlite");
60
1647
  const { BunSqliteDialect } = await import("kysely-bun-sqlite");
61
1648
  this._rawDb = new Database(this.path);
62
- const { createOpacaKysely } = await import("../chunk-gzbz5jwy.js");
63
- this._db = createOpacaKysely({
1649
+ const { createOpacaKysely: createOpacaKysely2 } = await Promise.resolve().then(() => (init_factory(), exports_factory));
1650
+ this._db = createOpacaKysely2({
64
1651
  dialect: new BunSqliteDialect({
65
1652
  database: this._rawDb
66
1653
  }),
@@ -76,7 +1663,7 @@ class BunSQLiteAdapter extends BaseDatabaseAdapter {
76
1663
  await this.db.destroy();
77
1664
  }
78
1665
  async unsafe(query, params) {
79
- const compiled = sql.raw(query);
1666
+ const compiled = sql2.raw(query);
80
1667
  const result = await compiled.execute(this.db);
81
1668
  return result.rows;
82
1669
  }
@@ -557,11 +2144,14 @@ class BunSQLiteAdapter extends BaseDatabaseAdapter {
557
2144
  return this.findGlobal(slug);
558
2145
  }
559
2146
  async runMigrations() {
2147
+ const fs = await import("node:fs/promises");
2148
+ const path = await import("node:path");
2149
+ const { pathToFileURL } = await import("node:url");
560
2150
  const migrator = new Migrator({
561
2151
  db: this.db,
562
2152
  provider: new FileMigrationProvider({
563
- fs,
564
- path,
2153
+ fs: fs.default || fs,
2154
+ path: path.default || path,
565
2155
  migrationFolder: pathToFileURL(path.resolve(process.cwd(), this.migrationDir)).href
566
2156
  })
567
2157
  });
@@ -587,8 +2177,8 @@ class BunSQLiteAdapter extends BaseDatabaseAdapter {
587
2177
  }
588
2178
  }
589
2179
  }
590
- function createBunSQLiteAdapter(path2, options) {
591
- return new BunSQLiteAdapter(path2, options);
2180
+ function createBunSQLiteAdapter(path, options) {
2181
+ return new BunSQLiteAdapter(path, options);
592
2182
  }
593
2183
  export {
594
2184
  createBunSQLiteAdapter,