opacacms 0.3.17 → 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.
Files changed (42) hide show
  1. package/dist/admin/webcomponent.js +14 -14
  2. package/dist/admin.css +1 -1
  3. package/dist/{chunk-1bd7fz7n.js → chunk-8mqs2q7j.js} +1 -1
  4. package/dist/{chunk-2abqb0h6.js → chunk-9dsw6x4x.js} +23 -16
  5. package/dist/{chunk-b1g8jmth.js → chunk-mvz5hmdb.js} +263 -8
  6. package/dist/cli/core/seeding/auto-seed.d.ts +1 -1
  7. package/dist/cli/index.js +3 -2
  8. package/dist/config.d.ts +2 -6
  9. package/dist/db/adapter.d.ts +1 -1
  10. package/dist/db/better-sqlite.d.ts +1 -1
  11. package/dist/db/better-sqlite.js +1633 -39
  12. package/dist/db/bun-sqlite.js +1627 -37
  13. package/dist/db/d1.d.ts +1 -1
  14. package/dist/db/d1.js +2326 -31
  15. package/dist/db/index.js +29 -4
  16. package/dist/db/kysely/plugins/draft-swapper.d.ts +2 -2
  17. package/dist/db/kysely/plugins/i18n-fallback.d.ts +2 -2
  18. package/dist/db/kysely/plugins/json-flattener.d.ts +2 -2
  19. package/dist/db/kysely/plugins/virtual-field-resolver.d.ts +2 -2
  20. package/dist/db/kysely/schema-builder.d.ts +1 -1
  21. package/dist/db/kysely/snapshot/snapshot-manager.d.ts +1 -1
  22. package/dist/db/postgres.js +1623 -32
  23. package/dist/db/sqlite.d.ts +1 -1
  24. package/dist/db/sqlite.js +1632 -38
  25. package/dist/index.js +7 -9
  26. package/dist/runtimes/bun.js +3 -7
  27. package/dist/runtimes/cloudflare-workers.js +3068 -13
  28. package/dist/runtimes/next.js +3 -7
  29. package/dist/runtimes/node.js +3 -7
  30. package/dist/server.js +18 -13
  31. package/dist/storage/index.js +6 -3
  32. package/dist/types.d.ts +3 -3
  33. package/package.json +1 -1
  34. package/dist/chunk-40tky6qh.js +0 -10
  35. package/dist/chunk-5xpf5jxd.js +0 -114
  36. package/dist/chunk-gzbz5jwy.js +0 -700
  37. package/dist/chunk-h8v093av.js +0 -185
  38. package/dist/chunk-jq1drsen.js +0 -82
  39. package/dist/chunk-q5sb5dcr.js +0 -15
  40. package/dist/chunk-re459gm9.js +0 -429
  41. package/dist/chunk-s8mqwnm1.js +0 -14
  42. package/dist/chunk-z9ek88xr.js +0 -15
@@ -1,38 +1,1625 @@
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
+ });
25
1108
 
26
1109
  // src/db/better-sqlite.ts
27
- import fs from "node:fs/promises";
28
- import path from "node:path";
29
- import { pathToFileURL } from "node:url";
30
1110
  import {
31
1111
  CompiledQuery,
32
1112
  FileMigrationProvider,
33
1113
  Migrator,
34
1114
  SqliteDialect
35
1115
  } from "kysely";
1116
+
1117
+ // src/db/adapter.ts
1118
+ class BaseDatabaseAdapter {
1119
+ get raw() {
1120
+ return;
1121
+ }
1122
+ get db() {
1123
+ return;
1124
+ }
1125
+ push;
1126
+ pushDestructive;
1127
+ migrationDir;
1128
+ }
1129
+
1130
+ // src/db/kysely/data-mapper.ts
1131
+ function toCamelCase(str) {
1132
+ if (str.startsWith("_"))
1133
+ return str;
1134
+ return str.replace(/_+([a-z])/g, (_, letter) => letter.toUpperCase());
1135
+ }
1136
+ function flattenPayload(payload, prefix = "", excludeKeys = []) {
1137
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload))
1138
+ return payload;
1139
+ const result = {};
1140
+ for (const key in payload) {
1141
+ if (Object.hasOwn(payload, key)) {
1142
+ const value = payload[key];
1143
+ if (value === undefined)
1144
+ continue;
1145
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !excludeKeys.includes(key)) {
1146
+ if (key.startsWith("_") && prefix === "") {
1147
+ result[key] = value;
1148
+ } else {
1149
+ const flatNested = flattenPayload(value, `${prefix}${key}__`);
1150
+ Object.assign(result, flatNested);
1151
+ }
1152
+ } else {
1153
+ if (value === undefined)
1154
+ continue;
1155
+ const colName = `${prefix}${toSnakeCase(key)}`;
1156
+ if (typeof value === "object" && value !== null && !(value instanceof Date)) {
1157
+ result[colName] = JSON.stringify(value);
1158
+ } else {
1159
+ result[colName] = value;
1160
+ }
1161
+ }
1162
+ }
1163
+ }
1164
+ return result;
1165
+ }
1166
+ function unflattenRow(row) {
1167
+ if (typeof row !== "object" || row === null || Array.isArray(row))
1168
+ return row;
1169
+ const result = {};
1170
+ for (const key in row) {
1171
+ if (Object.hasOwn(row, key)) {
1172
+ let value = row[key];
1173
+ if (typeof value === "string") {
1174
+ const trimmed = value.trim();
1175
+ if (trimmed.startsWith("[") && trimmed.endsWith("]") || trimmed.startsWith("{") && trimmed.endsWith("}")) {
1176
+ try {
1177
+ value = JSON.parse(value);
1178
+ } catch (e) {}
1179
+ }
1180
+ }
1181
+ if (key === "created_at" || key === "updated_at" || key === "createdAt" || key === "updatedAt") {
1182
+ if (typeof value === "string" && !value.includes("T") && value.includes(" ")) {
1183
+ value = `${value.replace(" ", "T")}Z`;
1184
+ }
1185
+ result[toCamelCase(key)] = value;
1186
+ continue;
1187
+ }
1188
+ if (key.startsWith("_")) {
1189
+ result[key] = value;
1190
+ continue;
1191
+ }
1192
+ const parts = key.split("__");
1193
+ if (parts.length === 1) {
1194
+ result[toCamelCase(key)] = value;
1195
+ continue;
1196
+ }
1197
+ let current = result;
1198
+ let collision = false;
1199
+ const path = [];
1200
+ for (let i = 0;i < parts.length - 1; i++) {
1201
+ const part = toCamelCase(parts[i]);
1202
+ if (current[part] !== undefined && (typeof current[part] !== "object" || current[part] === null)) {
1203
+ collision = true;
1204
+ break;
1205
+ }
1206
+ path.push({ obj: current, key: part });
1207
+ if (!current[part]) {
1208
+ current[part] = {};
1209
+ }
1210
+ current = current[part];
1211
+ }
1212
+ if (!collision) {
1213
+ const lastPart = toCamelCase(parts[parts.length - 1]);
1214
+ if (current[lastPart] !== undefined && typeof current[lastPart] === "object") {
1215
+ collision = true;
1216
+ } else {
1217
+ current[lastPart] = value;
1218
+ }
1219
+ }
1220
+ if (collision) {
1221
+ result[toCamelCase(key)] = value;
1222
+ }
1223
+ }
1224
+ }
1225
+ return result;
1226
+ }
1227
+ // src/db/kysely/query-builder.ts
1228
+ function buildKyselyWhere(eb, query) {
1229
+ if (!query || Object.keys(query).length === 0)
1230
+ return;
1231
+ const conditions = [];
1232
+ for (const [key, value] of Object.entries(query)) {
1233
+ if (value === undefined)
1234
+ continue;
1235
+ if (key === "or" && Array.isArray(value)) {
1236
+ const orConditions = value.map((v) => buildKyselyWhere(eb, v)).filter((c) => c !== undefined);
1237
+ if (orConditions.length > 0) {
1238
+ conditions.push(eb.or(orConditions));
1239
+ }
1240
+ continue;
1241
+ }
1242
+ if (key === "and" && Array.isArray(value)) {
1243
+ const andConditions = value.map((v) => buildKyselyWhere(eb, v)).filter((c) => c !== undefined);
1244
+ if (andConditions.length > 0) {
1245
+ conditions.push(eb.and(andConditions));
1246
+ }
1247
+ continue;
1248
+ }
1249
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
1250
+ for (const [op, val] of Object.entries(value)) {
1251
+ if (val === undefined)
1252
+ continue;
1253
+ switch (op) {
1254
+ case "gt":
1255
+ conditions.push(eb(key, ">", val));
1256
+ break;
1257
+ case "gte":
1258
+ conditions.push(eb(key, ">=", val));
1259
+ break;
1260
+ case "lt":
1261
+ conditions.push(eb(key, "<", val));
1262
+ break;
1263
+ case "lte":
1264
+ conditions.push(eb(key, "<=", val));
1265
+ break;
1266
+ case "like":
1267
+ conditions.push(eb(key, "like", String(val)));
1268
+ break;
1269
+ case "ne":
1270
+ if (val === null)
1271
+ conditions.push(eb(key, "is not", null));
1272
+ else
1273
+ conditions.push(eb(key, "!=", val));
1274
+ break;
1275
+ case "in":
1276
+ if (Array.isArray(val))
1277
+ conditions.push(eb(key, "in", val));
1278
+ break;
1279
+ case "is":
1280
+ if (val === null)
1281
+ conditions.push(eb(key, "is", null));
1282
+ break;
1283
+ default:
1284
+ if (val === null)
1285
+ conditions.push(eb(key, "is", null));
1286
+ else
1287
+ conditions.push(eb(key, "=", val));
1288
+ break;
1289
+ }
1290
+ }
1291
+ } else {
1292
+ if (value === null) {
1293
+ conditions.push(eb(key, "is", null));
1294
+ } else {
1295
+ conditions.push(eb(key, "=", value));
1296
+ }
1297
+ }
1298
+ }
1299
+ if (conditions.length === 0)
1300
+ return;
1301
+ if (conditions.length === 1)
1302
+ return conditions[0];
1303
+ return eb.and(conditions);
1304
+ }
1305
+
1306
+ // src/db/kysely/schema-builder.ts
1307
+ import { sql } from "kysely";
1308
+
1309
+ // src/db/kysely/sql-utils.ts
1310
+ function assertSafeIdentifier(identifier) {
1311
+ if (typeof identifier !== "string" || !identifier) {
1312
+ throw new Error(`Invalid identifier: must be a non-empty string. Got: ${identifier}`);
1313
+ }
1314
+ const isValid = /^[a-zA-Z0-9_-]+$/.test(identifier);
1315
+ if (!isValid) {
1316
+ throw new Error(`API Error: Unsafe SQL identifier detected: "${identifier}". Identifiers can only contain alphanumeric characters, underscores, and hyphens.`);
1317
+ }
1318
+ }
1319
+ // src/utils/logger.ts
1320
+ var RESET = "\x1B[0m";
1321
+ var BOLD = "\x1B[1m";
1322
+ var BLUE = "\x1B[34m";
1323
+ var GREEN = "\x1B[32m";
1324
+ var YELLOW = "\x1B[33m";
1325
+ var RED = "\x1B[31m";
1326
+ var GRAY = "\x1B[90m";
1327
+ var PREFIX = `${BLUE}${BOLD}[OpacaCMS]${RESET}`;
1328
+ var LOG_LEVELS = {
1329
+ debug: 0,
1330
+ info: 1,
1331
+ warn: 2,
1332
+ error: 3
1333
+ };
1334
+
1335
+ class OpacaLogger {
1336
+ config;
1337
+ constructor(config = {}) {
1338
+ this.config = config;
1339
+ }
1340
+ shouldLog(level) {
1341
+ if (this.config.disabled)
1342
+ return false;
1343
+ const configLevel = this.config.level || "info";
1344
+ return LOG_LEVELS[level] >= LOG_LEVELS[configLevel];
1345
+ }
1346
+ info(message, ...args) {
1347
+ if (!this.shouldLog("info"))
1348
+ return;
1349
+ console.log(`${PREFIX} ${message}`, ...args);
1350
+ }
1351
+ success(message, ...args) {
1352
+ if (!this.shouldLog("info"))
1353
+ return;
1354
+ console.log(`${PREFIX} ${GREEN}${message}${RESET}`, ...args);
1355
+ }
1356
+ debug(message, ...args) {
1357
+ if (!this.shouldLog("debug"))
1358
+ return;
1359
+ console.log(`${PREFIX} ${GRAY}${message}${RESET}`, ...args);
1360
+ }
1361
+ warn(message, ...args) {
1362
+ if (!this.shouldLog("warn"))
1363
+ return;
1364
+ console.warn(`${PREFIX} ${YELLOW}Warning: ${message}${RESET}`, ...args);
1365
+ }
1366
+ error(message, ...args) {
1367
+ if (!this.shouldLog("error"))
1368
+ return;
1369
+ console.error(`${PREFIX} ${RED}Error: ${message}${RESET}`, ...args);
1370
+ }
1371
+ log(message, ...args) {
1372
+ if (this.config.disabled)
1373
+ return;
1374
+ console.log(message, ...args);
1375
+ }
1376
+ bold(msg) {
1377
+ if (this.config.disableColors)
1378
+ return msg;
1379
+ return `${BOLD}${msg}${RESET}`;
1380
+ }
1381
+ format(color, msg) {
1382
+ if (this.config.disableColors)
1383
+ return msg;
1384
+ switch (color) {
1385
+ case "green":
1386
+ return `${GREEN}${msg}${RESET}`;
1387
+ case "red":
1388
+ return `${RED}${msg}${RESET}`;
1389
+ case "yellow":
1390
+ return `${YELLOW}${msg}${RESET}`;
1391
+ case "gray":
1392
+ return `${GRAY}${msg}${RESET}`;
1393
+ default:
1394
+ return msg;
1395
+ }
1396
+ }
1397
+ }
1398
+ var logger = new OpacaLogger;
1399
+
1400
+ // src/db/kysely/schema-builder.ts
1401
+ async function pushSchema(db, dialect, collections, globals = [], options = {}) {
1402
+ const rawSchemas = [...getSystemCollections(), ...collections, ...globals];
1403
+ const allSchemas = [];
1404
+ const seenSlugs = new Set;
1405
+ for (const schema of rawSchemas) {
1406
+ if (!seenSlugs.has(schema.slug)) {
1407
+ seenSlugs.add(schema.slug);
1408
+ allSchemas.push(schema);
1409
+ }
1410
+ }
1411
+ const { pushDestructive } = options;
1412
+ logger.debug(`Starting schema push via Kysely (${dialect})...`);
1413
+ const mapType = dialect === "postgres" ? mapFieldToPostgresType : mapFieldToSQLiteType;
1414
+ let tablesMetadata = [];
1415
+ try {
1416
+ tablesMetadata = await db.introspection.getTables();
1417
+ } catch (e) {
1418
+ logger.warn("Failed to fetch database introspection. Falling back to empty state.", e);
1419
+ }
1420
+ const existingTableMap = new Map(tablesMetadata.map((t) => [t.name, t]));
1421
+ const existingTables = Array.from(existingTableMap.keys());
1422
+ logger.debug(`Existing tables found: ${existingTables.join(", ")}`);
1423
+ const expectedTableNames = new Set;
1424
+ for (const collection of allSchemas) {
1425
+ const slug = collection.slug;
1426
+ const tableName = toSnakeCase(slug);
1427
+ assertSafeIdentifier(tableName);
1428
+ expectedTableNames.add(tableName);
1429
+ const flattenedFields = flattenFields(collection.fields);
1430
+ const hasTimestamps = true;
1431
+ const versions = collection.versions;
1432
+ const existingTable = existingTableMap.get(tableName);
1433
+ if (!existingTable) {
1434
+ logger.info(` ${logger.format("green", "✔")} Creating table: ${logger.bold(`"${tableName}"`)}`);
1435
+ let builder = db.schema.createTable(tableName).ifNotExists();
1436
+ builder = builder.addColumn("id", "text", (col) => col.primaryKey());
1437
+ for (const field of flattenedFields) {
1438
+ if (field.type === "relationship" && "hasMany" in field && field.hasMany)
1439
+ continue;
1440
+ const colName = toSnakeCase(field.name);
1441
+ if (colName === "id")
1442
+ continue;
1443
+ assertSafeIdentifier(colName);
1444
+ const colType = mapType(field);
1445
+ builder = builder.addColumn(colName, colType, (col) => {
1446
+ let c = col;
1447
+ if (field.unique)
1448
+ c = c.unique();
1449
+ if (field.required)
1450
+ c = c.notNull();
1451
+ if (field.defaultValue !== undefined)
1452
+ c = c.defaultTo(field.defaultValue);
1453
+ return c;
1454
+ });
1455
+ }
1456
+ if (versions?.drafts) {
1457
+ builder = builder.addColumn("_status", "text", (col) => col.defaultTo("draft"));
1458
+ }
1459
+ if (hasTimestamps) {
1460
+ const ts = collection.timestamps ?? {};
1461
+ const config = typeof ts === "object" ? ts : {};
1462
+ const createdField = toSnakeCase(config.createdAt ?? "createdAt");
1463
+ const updatedField = toSnakeCase(config.updatedAt ?? "updatedAt");
1464
+ const tsType = dialect === "postgres" ? "timestamptz" : "text";
1465
+ builder = builder.addColumn(createdField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
1466
+ builder = builder.addColumn(updatedField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
1467
+ }
1468
+ await builder.execute();
1469
+ } else {
1470
+ const existingColSet = new Set(existingTable.columns.map((c) => c.name));
1471
+ for (const field of flattenedFields) {
1472
+ if (field.type === "relationship" && "hasMany" in field && field.hasMany)
1473
+ continue;
1474
+ const colName = toSnakeCase(field.name);
1475
+ if (colName === "id")
1476
+ continue;
1477
+ assertSafeIdentifier(colName);
1478
+ if (!existingColSet.has(colName)) {
1479
+ logger.info(` ${logger.format("green", "✔")} Adding column: ${logger.bold(`${tableName}.${colName}`)}`);
1480
+ const colType = mapType(field);
1481
+ await db.schema.alterTable(tableName).addColumn(colName, colType).execute();
1482
+ }
1483
+ }
1484
+ if (versions?.drafts && !existingColSet.has("_status")) {
1485
+ logger.info(` ${logger.format("green", "✔")} Adding status column to: ${tableName}`);
1486
+ await db.schema.alterTable(tableName).addColumn("_status", "text", (col) => col.defaultTo("draft")).execute();
1487
+ }
1488
+ if (hasTimestamps) {
1489
+ const ts = collection.timestamps ?? {};
1490
+ const config = typeof ts === "object" ? ts : {};
1491
+ const createdField = toSnakeCase(config.createdAt ?? "createdAt");
1492
+ const updatedField = toSnakeCase(config.updatedAt ?? "updatedAt");
1493
+ const tsType = dialect === "postgres" ? "timestamptz" : "text";
1494
+ if (!existingColSet.has(createdField)) {
1495
+ logger.info(` -> Adding missing timestamp column: ${tableName}.${createdField}`);
1496
+ await db.schema.alterTable(tableName).addColumn(createdField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
1497
+ }
1498
+ if (!existingColSet.has(updatedField)) {
1499
+ logger.info(` -> Adding missing timestamp column: ${tableName}.${updatedField}`);
1500
+ await db.schema.alterTable(tableName).addColumn(updatedField, tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
1501
+ }
1502
+ }
1503
+ if (pushDestructive) {
1504
+ const expectedCols = new Set(flattenedFields.filter((f) => !(f.type === "relationship" && f.hasMany)).map((f) => toSnakeCase(f.name)));
1505
+ if (hasTimestamps) {
1506
+ const ts = collection.timestamps ?? {};
1507
+ const config = typeof ts === "object" ? ts : {};
1508
+ expectedCols.add(toSnakeCase(config.createdAt ?? "createdAt"));
1509
+ expectedCols.add(toSnakeCase(config.updatedAt ?? "updatedAt"));
1510
+ }
1511
+ if (versions?.drafts)
1512
+ expectedCols.add("_status");
1513
+ expectedCols.add("id");
1514
+ for (const existingCol of existingColSet) {
1515
+ const colName = existingCol;
1516
+ if (!expectedCols.has(colName)) {
1517
+ logger.info(` -> Dropping stale column: ${logger.format("red", `${tableName}.${colName}`)}`);
1518
+ try {
1519
+ await db.schema.alterTable(tableName).dropColumn(colName).execute();
1520
+ } catch (_) {
1521
+ logger.warn(`Failed to drop column ${colName}.`);
1522
+ }
1523
+ }
1524
+ }
1525
+ }
1526
+ }
1527
+ const relationalFields = getRelationalFields(collection.fields);
1528
+ for (const field of relationalFields) {
1529
+ if (field.type === "relationship" && "hasMany" in field && field.hasMany) {
1530
+ const colName = toSnakeCase(field.name);
1531
+ const joinTableName = `${tableName}_${colName}_relations`.toLowerCase();
1532
+ assertSafeIdentifier(joinTableName);
1533
+ expectedTableNames.add(joinTableName);
1534
+ if (!existingTableMap.has(joinTableName)) {
1535
+ logger.info(` -> Creating relation table: ${logger.format("green", `"${joinTableName}"`)}`);
1536
+ 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();
1537
+ }
1538
+ }
1539
+ }
1540
+ for (const field of relationalFields) {
1541
+ if (field.type === "blocks" && field.blocks && Array.isArray(field.blocks)) {
1542
+ for (const block of field.blocks) {
1543
+ const blockName = toSnakeCase(field.name);
1544
+ const blockSlug = toSnakeCase(block.slug);
1545
+ assertSafeIdentifier(blockName);
1546
+ assertSafeIdentifier(blockSlug);
1547
+ const blockTableName = `${tableName}_${blockName}_${blockSlug}`.toLowerCase();
1548
+ assertSafeIdentifier(blockTableName);
1549
+ expectedTableNames.add(blockTableName);
1550
+ const existingBlockTable = existingTableMap.get(blockTableName);
1551
+ if (!existingBlockTable) {
1552
+ logger.info(` -> Creating block table: ${logger.format("green", `"${blockTableName}"`)}`);
1553
+ 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));
1554
+ const blockFlattened = flattenFields(block.fields);
1555
+ for (const bField of blockFlattened) {
1556
+ const bColName = toSnakeCase(bField.name);
1557
+ if (bColName === "id")
1558
+ continue;
1559
+ assertSafeIdentifier(bColName);
1560
+ const bColType = mapType(bField);
1561
+ bBuilder = bBuilder.addColumn(bColName, bColType, (col) => {
1562
+ let c = col;
1563
+ if (bField.unique)
1564
+ c = c.unique();
1565
+ if (bField.required)
1566
+ c = c.notNull();
1567
+ if (bField.defaultValue !== undefined)
1568
+ c = c.defaultTo(bField.defaultValue);
1569
+ return c;
1570
+ });
1571
+ }
1572
+ const tsType = dialect === "postgres" ? "timestamptz" : "text";
1573
+ bBuilder = bBuilder.addColumn("created_at", tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
1574
+ bBuilder = bBuilder.addColumn("updated_at", tsType, (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`));
1575
+ await bBuilder.execute();
1576
+ } else {
1577
+ const existingBColSet = new Set(existingBlockTable.columns.map((c) => c.name));
1578
+ const blockFlattened = flattenFields(block.fields);
1579
+ for (const bField of blockFlattened) {
1580
+ const bColName = toSnakeCase(bField.name);
1581
+ if (bColName === "id")
1582
+ continue;
1583
+ if (!existingBColSet.has(bColName)) {
1584
+ const bColType = mapType(bField);
1585
+ await db.schema.alterTable(blockTableName).addColumn(bColName, bColType).execute();
1586
+ }
1587
+ }
1588
+ if (!existingBColSet.has("_parent_id")) {
1589
+ await db.schema.alterTable(blockTableName).addColumn("_parent_id", "text", (col) => col.notNull().defaultTo("")).execute();
1590
+ }
1591
+ if (!existingBColSet.has("_order")) {
1592
+ await db.schema.alterTable(blockTableName).addColumn("_order", "integer", (col) => col.defaultTo(0)).execute();
1593
+ }
1594
+ if (!existingBColSet.has("block_type")) {
1595
+ await db.schema.alterTable(blockTableName).addColumn("block_type", "text", (col) => col.notNull().defaultTo(block.slug)).execute();
1596
+ }
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+ if (pushDestructive) {
1603
+ for (const tableName of existingTables) {
1604
+ if (!expectedTableNames.has(tableName) && !tableName.startsWith("better_auth") && !tableName.startsWith("__kysely")) {
1605
+ logger.info(` -> Dropping stale table: ${logger.format("red", `"${tableName}"`)}`);
1606
+ try {
1607
+ await db.schema.dropTable(tableName).ifExists().cascade().execute();
1608
+ } catch (_) {
1609
+ try {
1610
+ await db.schema.dropTable(tableName).ifExists().execute();
1611
+ } catch (_2) {
1612
+ logger.warn(`Failed to drop table ${tableName}`);
1613
+ }
1614
+ }
1615
+ }
1616
+ }
1617
+ }
1618
+ logger.info(`${logger.format("green", "✔")} Schema synchronization complete.`);
1619
+ }
1620
+
1621
+ // src/db/better-sqlite.ts
1622
+ init_context();
36
1623
  class BetterSQLiteAdapter extends BaseDatabaseAdapter {
37
1624
  path;
38
1625
  name = "better-sqlite3";
@@ -51,9 +1638,9 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
51
1638
  throw new Error("Database not connected. Call connect() first.");
52
1639
  return this._db;
53
1640
  }
54
- constructor(path2, options) {
1641
+ constructor(path, options) {
55
1642
  super();
56
- this.path = path2;
1643
+ this.path = path;
57
1644
  this.push = options?.push ?? true;
58
1645
  this.pushDestructive = options?.pushDestructive ?? false;
59
1646
  this.migrationDir = options?.migrationDir ?? "./migrations";
@@ -61,12 +1648,16 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
61
1648
  async connect() {
62
1649
  if (this._db)
63
1650
  return;
64
- const { createRequire } = await import("node:module");
65
- const require2 = createRequire(import.meta.url);
66
- const Database = require2("better-sqlite3");
1651
+ let Database;
1652
+ try {
1653
+ const mod = await import("better-sqlite3");
1654
+ Database = mod.default || mod;
1655
+ } catch (e) {
1656
+ throw new Error("Cannot use BetterSQLiteAdapter: better-sqlite3 is not installed or could not be loaded. If you are using Cloudflare Workers, please use D1Adapter instead.");
1657
+ }
67
1658
  this._rawDb = new Database(this.path);
68
- const { createOpacaKysely } = await import("../chunk-gzbz5jwy.js");
69
- this._db = createOpacaKysely({
1659
+ const { createOpacaKysely: createOpacaKysely2 } = await Promise.resolve().then(() => (init_factory(), exports_factory));
1660
+ this._db = createOpacaKysely2({
70
1661
  dialect: new SqliteDialect({
71
1662
  database: this._rawDb
72
1663
  }),
@@ -184,7 +1775,7 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
184
1775
  const unflattened = unflattenRow(row);
185
1776
  const colDef = this._collections.find((c) => c.slug === collection);
186
1777
  if (colDef) {
187
- const { getRelationalFields: getRelationalFields2, toSnakeCase: toSnakeCase2 } = await import("../chunk-z9ek88xr.js");
1778
+ const { getRelationalFields: getRelationalFields2, toSnakeCase: toSnakeCase2 } = await Promise.resolve().then(() => exports_field_mapper);
188
1779
  const relationalFields = getRelationalFields2(colDef.fields);
189
1780
  for (const field of relationalFields) {
190
1781
  if (!field.name)
@@ -550,11 +2141,14 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
550
2141
  return this.findGlobal(slug);
551
2142
  }
552
2143
  async runMigrations() {
2144
+ const fs = await import("node:fs/promises");
2145
+ const path = await import("node:path");
2146
+ const { pathToFileURL } = await import("node:url");
553
2147
  const migrator = new Migrator({
554
2148
  db: this.db,
555
2149
  provider: new FileMigrationProvider({
556
- fs,
557
- path,
2150
+ fs: fs.default || fs,
2151
+ path: path.default || path,
558
2152
  migrationFolder: pathToFileURL(path.resolve(process.cwd(), this.migrationDir)).href
559
2153
  })
560
2154
  });
@@ -580,8 +2174,8 @@ class BetterSQLiteAdapter extends BaseDatabaseAdapter {
580
2174
  }
581
2175
  }
582
2176
  }
583
- function createBetterSQLiteAdapter(path2, options) {
584
- return new BetterSQLiteAdapter(path2, options);
2177
+ function createBetterSQLiteAdapter(path, options) {
2178
+ return new BetterSQLiteAdapter(path, options);
585
2179
  }
586
2180
  export {
587
2181
  createBetterSQLiteAdapter,