opacacms 0.1.21 → 0.2.1

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 (138) hide show
  1. package/README.md +792 -50
  2. package/dist/admin/auth-client.d.ts +39 -39
  3. package/dist/admin/index.js +2360 -1392
  4. package/dist/admin/react.d.ts +1 -1
  5. package/dist/admin/react.js +8 -0
  6. package/dist/admin/router.d.ts +1 -0
  7. package/dist/admin/stores/ui.d.ts +10 -0
  8. package/dist/admin/ui/admin-layout.d.ts +4 -4
  9. package/dist/admin/ui/components/DataDetailView.d.ts +1 -1
  10. package/dist/admin/ui/components/DetailSheet.d.ts +19 -0
  11. package/dist/admin/ui/components/PluginSettingsForm.d.ts +11 -0
  12. package/dist/admin/ui/components/fields/BooleanField.d.ts +2 -1
  13. package/dist/admin/ui/components/fields/DateField.d.ts +1 -1
  14. package/dist/admin/ui/components/fields/FieldLabel.d.ts +11 -0
  15. package/dist/admin/ui/components/fields/FileField.d.ts +1 -1
  16. package/dist/admin/ui/components/fields/NumberField.d.ts +1 -1
  17. package/dist/admin/ui/components/fields/RadioField.d.ts +1 -1
  18. package/dist/admin/ui/components/fields/RelationshipField.d.ts +3 -1
  19. package/dist/admin/ui/components/fields/SelectField.d.ts +1 -1
  20. package/dist/admin/ui/components/fields/TextAreaField.d.ts +1 -1
  21. package/dist/admin/ui/components/fields/TextField.d.ts +1 -1
  22. package/dist/admin/ui/components/fields/VirtualField.d.ts +1 -0
  23. package/dist/admin/ui/components/fields/index.d.ts +16 -16
  24. package/dist/admin/ui/components/fields/richtext-editor/index.d.ts +1 -1
  25. package/dist/admin/ui/components/media/AssetManagerModal.d.ts +1 -1
  26. package/dist/admin/ui/components/toast.d.ts +1 -1
  27. package/dist/admin/ui/components/ui/accordion.d.ts +1 -1
  28. package/dist/admin/ui/components/ui/button.d.ts +1 -1
  29. package/dist/admin/ui/components/ui/collapsible.d.ts +1 -1
  30. package/dist/admin/ui/components/ui/dialog.d.ts +1 -1
  31. package/dist/admin/ui/components/ui/group.d.ts +1 -1
  32. package/dist/admin/ui/components/ui/index.d.ts +17 -17
  33. package/dist/admin/ui/components/ui/input.d.ts +1 -1
  34. package/dist/admin/ui/components/ui/label.d.ts +1 -1
  35. package/dist/admin/ui/components/ui/radio-group.d.ts +1 -1
  36. package/dist/admin/ui/components/ui/relationship.d.ts +4 -4
  37. package/dist/admin/ui/components/ui/select.d.ts +1 -1
  38. package/dist/admin/ui/components/ui/sheet.d.ts +1 -1
  39. package/dist/admin/ui/components/ui/tabs.d.ts +1 -1
  40. package/dist/admin/ui/components/versions-sheet.d.ts +11 -0
  41. package/dist/admin/ui/views/media-registry-view.d.ts +1 -1
  42. package/dist/admin/ui/views/settings-view.d.ts +2 -2
  43. package/dist/admin/vue.js +8 -0
  44. package/dist/admin/webcomponent.js +20 -2
  45. package/dist/admin.css +1 -1
  46. package/dist/auth/index.d.ts +101 -41
  47. package/dist/{chunk-fqastxq9.js → chunk-06ks4ggh.js} +133 -44
  48. package/dist/{chunk-xrfhhz85.js → chunk-2es275xs.js} +480 -85
  49. package/dist/{chunk-v521d72w.js → chunk-3rdhbedb.js} +1 -1
  50. package/dist/chunk-51z3x7kq.js +20 -0
  51. package/dist/{chunk-7fyepksb.js → chunk-526a3gqx.js} +1 -1
  52. package/dist/{chunk-0sdceeys.js → chunk-6d1vdfwa.js} +121 -31
  53. package/dist/{chunk-wmvjvn7b.js → chunk-6qq3ne6b.js} +39 -1
  54. package/dist/{chunk-0am1m47g.js → chunk-6v1fw7q7.js} +5 -5
  55. package/dist/{chunk-t9v845m2.js → chunk-7y1nbmw6.js} +34 -3
  56. package/dist/chunk-8scgdznr.js +44 -0
  57. package/dist/{chunk-mycmsjd9.js → chunk-b3kr8w41.js} +57 -6
  58. package/dist/chunk-bexcv7xe.js +36 -0
  59. package/dist/{chunk-ekxkvqjm.js → chunk-bygjkgrx.js} +124 -34
  60. package/dist/{chunk-16vgcf3k.js → chunk-byq8g0rd.js} +1 -1
  61. package/dist/{chunk-cpw2y3pn.js → chunk-dykn5hr6.js} +7 -7
  62. package/dist/chunk-fj19qccp.js +78 -0
  63. package/dist/{chunk-n1xraw7j.js → chunk-g1jb60xd.js} +1 -1
  64. package/dist/{chunk-xa7rjsn2.js → chunk-j53pz21t.js} +2 -2
  65. package/dist/{chunk-nb7ctdg8.js → chunk-jdfw4v3r.js} +1 -1
  66. package/dist/chunk-mkn49zmy.js +102 -0
  67. package/dist/{chunk-59sg3pw9.js → chunk-n133qpsm.js} +128 -34
  68. package/dist/{chunk-2kyhqvhc.js → chunk-qxt9vge8.js} +1 -1
  69. package/dist/chunk-r39em4yj.js +29 -0
  70. package/dist/chunk-rqyjjqgy.js +91 -0
  71. package/dist/chunk-rsf0tpy1.js +8 -0
  72. package/dist/chunk-t0zg026p.js +71 -0
  73. package/dist/{chunk-61kwqve4.js → chunk-tfnaf41w.js} +118 -37
  74. package/dist/chunk-twpvxfce.js +64 -0
  75. package/dist/{chunk-ybbbqj63.js → chunk-v9z61v3g.js} +15 -0
  76. package/dist/{chunk-jwjk85ze.js → chunk-ywm4t2gm.js} +6 -2
  77. package/dist/cli/commands/plugin-build.d.ts +1 -0
  78. package/dist/cli/commands/plugin-init.d.ts +1 -0
  79. package/dist/cli/commands/plugin-sync.d.ts +1 -0
  80. package/dist/cli/index.js +24 -6
  81. package/dist/config-utils.d.ts +1 -1
  82. package/dist/config.d.ts +21 -4
  83. package/dist/db/adapter.d.ts +2 -2
  84. package/dist/db/better-sqlite.d.ts +2 -1
  85. package/dist/db/better-sqlite.js +5 -5
  86. package/dist/db/bun-sqlite.d.ts +2 -1
  87. package/dist/db/bun-sqlite.js +5 -5
  88. package/dist/db/d1.d.ts +1 -1
  89. package/dist/db/d1.js +5 -5
  90. package/dist/db/index.js +9 -9
  91. package/dist/db/postgres.d.ts +3 -3
  92. package/dist/db/postgres.js +5 -5
  93. package/dist/db/sqlite.d.ts +2 -1
  94. package/dist/db/sqlite.js +5 -5
  95. package/dist/index.js +4 -3
  96. package/dist/plugins/index.d.ts +1 -0
  97. package/dist/plugins/ui-bridge.d.ts +12 -0
  98. package/dist/plugins/utils.d.ts +5 -0
  99. package/dist/runtimes/bun.js +13 -7
  100. package/dist/runtimes/cloudflare-workers.js +5 -5
  101. package/dist/runtimes/next.js +5 -5
  102. package/dist/runtimes/node.js +13 -7
  103. package/dist/schema/collection.d.ts +9 -26
  104. package/dist/schema/fields/base.d.ts +3 -2
  105. package/dist/schema/fields/index.d.ts +12 -0
  106. package/dist/schema/fields/validation.test.d.ts +1 -0
  107. package/dist/schema/global.d.ts +10 -7
  108. package/dist/schema/index.js +22 -6
  109. package/dist/server/admin-router.d.ts +2 -2
  110. package/dist/server/admin.d.ts +2 -1
  111. package/dist/server/collection-router.d.ts +1 -1
  112. package/dist/server/handlers.d.ts +10 -0
  113. package/dist/server/middlewares/admin.d.ts +2 -2
  114. package/dist/server/middlewares/auth.d.ts +1 -1
  115. package/dist/server/middlewares/context.d.ts +2 -0
  116. package/dist/server/middlewares/rate-limit.d.ts +1 -1
  117. package/dist/server/openapi.d.ts +2 -0
  118. package/dist/server/plugins-loader.d.ts +6 -0
  119. package/dist/server/router.d.ts +3 -3
  120. package/dist/server/routers/admin.d.ts +2 -2
  121. package/dist/server/routers/auth.d.ts +1 -1
  122. package/dist/server/routers/collections.d.ts +1 -1
  123. package/dist/server/routers/plugins.d.ts +18 -0
  124. package/dist/server/setup-middlewares.d.ts +2 -2
  125. package/dist/server/system-router.d.ts +1 -1
  126. package/dist/server.js +11 -7
  127. package/dist/storage/adapters/local.d.ts +1 -1
  128. package/dist/storage/adapters/s3.d.ts +1 -1
  129. package/dist/storage/index.js +34 -25
  130. package/dist/types.d.ts +224 -17
  131. package/dist/utils/logger.d.ts +13 -35
  132. package/dist/validation.d.ts +40 -0
  133. package/dist/validator.d.ts +1 -1
  134. package/package.json +41 -27
  135. package/dist/admin/ui/components/DataDetailSheet.d.ts +0 -13
  136. package/dist/admin/ui/components/ui/relationship-detail-sheet.d.ts +0 -9
  137. package/dist/chunk-62ev8gnc.js +0 -41
  138. package/dist/chunk-j4d50hrx.js +0 -20
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getSystemCollections,
3
3
  init_system_schema
4
- } from "./chunk-ybbbqj63.js";
4
+ } from "./chunk-v9z61v3g.js";
5
5
  import"./chunk-8sqjbsgt.js";
6
6
  init_system_schema();
7
7
 
@@ -0,0 +1,20 @@
1
+ // src/server/plugins-loader.ts
2
+ async function loadPluginSettings(config) {
3
+ if (!config.db)
4
+ return {};
5
+ try {
6
+ const result = await config.db.find("_opaca_plugin_settings");
7
+ if (!result || !result.docs)
8
+ return {};
9
+ return result.docs.reduce((acc, curr) => {
10
+ if (curr.pluginName && curr.config) {
11
+ acc[curr.pluginName] = curr.config;
12
+ }
13
+ return acc;
14
+ }, {});
15
+ } catch (e) {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ export { loadPluginSettings };
@@ -4,7 +4,7 @@ import {
4
4
  mapFieldToPostgresType,
5
5
  mapFieldToSQLiteType,
6
6
  toSnakeCase
7
- } from "./chunk-2kyhqvhc.js";
7
+ } from "./chunk-qxt9vge8.js";
8
8
  import"./chunk-8sqjbsgt.js";
9
9
  export {
10
10
  toSnakeCase,
@@ -3,31 +3,31 @@ import {
3
3
  flattenPayload,
4
4
  pushSchema,
5
5
  unflattenRow
6
- } from "./chunk-cpw2y3pn.js";
6
+ } from "./chunk-dykn5hr6.js";
7
7
  import {
8
8
  BaseDatabaseAdapter
9
9
  } from "./chunk-s8mqwnm1.js";
10
- import {
11
- logger
12
- } from "./chunk-62ev8gnc.js";
13
10
  import {
14
11
  flattenFields,
15
12
  getRelationalFields,
16
13
  toSnakeCase
17
- } from "./chunk-2kyhqvhc.js";
14
+ } from "./chunk-qxt9vge8.js";
15
+ import {
16
+ logger
17
+ } from "./chunk-t0zg026p.js";
18
+ import {
19
+ __require
20
+ } from "./chunk-8sqjbsgt.js";
18
21
 
19
22
  // src/db/sqlite.ts
20
23
  import fs from "node:fs/promises";
21
24
  import path from "node:path";
22
- import { createRequire } from "node:module";
23
25
  import { CompiledQuery, FileMigrationProvider, Kysely, Migrator, SqliteDialect } from "kysely";
24
- var require2 = createRequire(import.meta.url);
25
- var Database = require2("better-sqlite3");
26
-
27
26
  class SQLiteAdapter extends BaseDatabaseAdapter {
27
+ path;
28
28
  name = "sqlite";
29
29
  _rawDb;
30
- _db;
30
+ _db = null;
31
31
  _collections = [];
32
32
  _globals = [];
33
33
  push;
@@ -37,27 +37,37 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
37
37
  return this._rawDb;
38
38
  }
39
39
  get db() {
40
+ if (!this._db)
41
+ throw new Error("Database not connected. Call connect() first.");
40
42
  return this._db;
41
43
  }
42
44
  constructor(path2, options) {
43
45
  super();
44
- this._rawDb = new Database(path2);
46
+ this.path = path2;
47
+ this.push = options?.push ?? true;
48
+ this.pushDestructive = options?.pushDestructive ?? false;
49
+ this.migrationDir = options?.migrationDir ?? "./migrations";
50
+ }
51
+ async connect() {
52
+ if (this._db)
53
+ return;
54
+ const { createRequire } = await import("node:module");
55
+ const require2 = createRequire(import.meta.url);
56
+ const Database = require2("better-sqlite3");
57
+ this._rawDb = new Database(this.path);
45
58
  this._db = new Kysely({
46
59
  dialect: new SqliteDialect({
47
60
  database: this._rawDb
48
61
  })
49
62
  });
50
- this.push = options?.push ?? true;
51
- this.pushDestructive = options?.pushDestructive ?? false;
52
- this.migrationDir = options?.migrationDir ?? "./migrations";
53
63
  }
54
- async connect() {}
55
64
  async disconnect() {
56
- await this._db.destroy();
65
+ if (this._db)
66
+ await this.db.destroy();
57
67
  }
58
68
  async unsafe(query, params) {
59
69
  const compiled = CompiledQuery.raw(query, params || []);
60
- const result = await this._db.executeQuery(compiled);
70
+ const result = await this.db.executeQuery(compiled);
61
71
  return result.rows;
62
72
  }
63
73
  async coerceData(collection, data) {
@@ -100,7 +110,7 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
100
110
  }
101
111
  async count(collection, query) {
102
112
  const tableName = toSnakeCase(collection);
103
- let qb = this._db.selectFrom(tableName).select((eb) => eb.fn.count("id").as("count"));
113
+ let qb = this.db.selectFrom(tableName).select((eb) => eb.fn.count("id").as("count"));
104
114
  if (query && Object.keys(query).length > 0) {
105
115
  qb = qb.where((eb) => buildKyselyWhere(eb, query) || eb.val(true));
106
116
  }
@@ -109,7 +119,7 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
109
119
  }
110
120
  async create(collection, data) {
111
121
  const tableName = toSnakeCase(collection);
112
- return this._db.transaction().execute(async (tx) => {
122
+ return this.db.transaction().execute(async (tx) => {
113
123
  const colDef = this._collections.find((c) => c.slug === collection);
114
124
  const jsonFields = colDef?.fields.filter((f) => f.name && (["richtext", "json", "file"].includes(f.type) || f.localized)).map((f) => f.name) || [];
115
125
  const flatData = flattenPayload(data, "", jsonFields);
@@ -186,7 +196,7 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
186
196
  }
187
197
  async findOne(collection, query, tx) {
188
198
  const tableName = toSnakeCase(collection);
189
- const executor = tx || this._db;
199
+ const executor = tx || this.db;
190
200
  let qb = executor.selectFrom(tableName).selectAll();
191
201
  if (query && Object.keys(query).length > 0) {
192
202
  qb = qb.where((eb) => buildKyselyWhere(eb, query) || eb.val(true));
@@ -252,7 +262,7 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
252
262
  const offset = (page - 1) * limit;
253
263
  const total = await this.count(collection, query);
254
264
  const tableName = toSnakeCase(collection);
255
- let qb = this._db.selectFrom(tableName).selectAll().limit(limit).offset(offset);
265
+ let qb = this.db.selectFrom(tableName).selectAll().limit(limit).offset(offset);
256
266
  if (query && Object.keys(query).length > 0) {
257
267
  qb = qb.where((eb) => buildKyselyWhere(eb, query) || eb.val(true));
258
268
  }
@@ -265,7 +275,87 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
265
275
  qb = qb.orderBy("created_at", "desc");
266
276
  }
267
277
  const rows = await qb.execute();
268
- const docs = await Promise.all(rows.map((row) => this.findOne(collection, { id: row.id })));
278
+ if (rows.length === 0) {
279
+ const totalPages2 = Math.ceil(total / limit);
280
+ return {
281
+ docs: [],
282
+ totalDocs: total,
283
+ limit,
284
+ totalPages: totalPages2,
285
+ page,
286
+ pagingCounter: offset + 1,
287
+ hasNextPage: page * limit < total,
288
+ hasPrevPage: page > 1,
289
+ prevPage: page > 1 ? page - 1 : null,
290
+ nextPage: page < totalPages2 ? page + 1 : null
291
+ };
292
+ }
293
+ const rowIds = rows.map((r) => r.id);
294
+ const docs = rows.map((r) => unflattenRow(r));
295
+ const colDef = this._collections.find((c) => c.slug === collection);
296
+ if (colDef) {
297
+ const relationalFields = getRelationalFields(colDef.fields);
298
+ for (const field of relationalFields) {
299
+ if (!field.name)
300
+ continue;
301
+ const snakeName = toSnakeCase(field.name);
302
+ if (field.type === "relationship" && "hasMany" in field && field.hasMany) {
303
+ const joinTableName = `${toSnakeCase(collection)}_${snakeName}_relations`.toLowerCase();
304
+ try {
305
+ const allRelations = await this.db.selectFrom(joinTableName).selectAll().where("source_id", "in", rowIds).orderBy("order", "asc").execute();
306
+ const relationsBySource = allRelations.reduce((acc, r) => {
307
+ if (!acc[r.source_id])
308
+ acc[r.source_id] = [];
309
+ acc[r.source_id].push(r.target_id);
310
+ return acc;
311
+ }, {});
312
+ const parts = field.name.split("__");
313
+ for (let docIdx = 0;docIdx < docs.length; docIdx++) {
314
+ const rowId = rowIds[docIdx];
315
+ let current = docs[docIdx];
316
+ for (let i = 0;i < parts.length - 1; i++) {
317
+ if (!current[parts[i]])
318
+ current[parts[i]] = {};
319
+ current = current[parts[i]];
320
+ }
321
+ current[parts[parts.length - 1]] = relationsBySource[rowId] || [];
322
+ }
323
+ } catch (e) {}
324
+ } else if (field.type === "blocks" && field.blocks) {
325
+ const blocksBySource = {};
326
+ for (const b of field.blocks) {
327
+ const blockTableName = `${collection}_${snakeName}_${toSnakeCase(b.slug)}`.toLowerCase();
328
+ try {
329
+ const allBlocks = await this.db.selectFrom(blockTableName).selectAll().where("_parent_id", "in", rowIds).execute();
330
+ for (const blk of allBlocks) {
331
+ const uf = unflattenRow(blk);
332
+ uf.blockType = blk.block_type;
333
+ if (!blocksBySource[blk._parent_id])
334
+ blocksBySource[blk._parent_id] = [];
335
+ blocksBySource[blk._parent_id].push(uf);
336
+ }
337
+ } catch (e) {}
338
+ }
339
+ const parts = field.name.split("__");
340
+ for (let docIdx = 0;docIdx < docs.length; docIdx++) {
341
+ const rowId = rowIds[docIdx];
342
+ const blockData = blocksBySource[rowId] || [];
343
+ blockData.sort((a, b) => a._order - b._order);
344
+ blockData.forEach((b) => {
345
+ delete b._order;
346
+ delete b._parentId;
347
+ });
348
+ let current = docs[docIdx];
349
+ for (let i = 0;i < parts.length - 1; i++) {
350
+ if (!current[parts[i]])
351
+ current[parts[i]] = {};
352
+ current = current[parts[i]];
353
+ }
354
+ current[parts[parts.length - 1]] = blockData;
355
+ }
356
+ }
357
+ }
358
+ }
269
359
  const totalPages = Math.ceil(total / limit);
270
360
  return {
271
361
  docs: docs.filter(Boolean),
@@ -281,7 +371,7 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
281
371
  };
282
372
  }
283
373
  async update(collection, query, data) {
284
- return this._db.transaction().execute(async (tx) => {
374
+ return this.db.transaction().execute(async (tx) => {
285
375
  let normalizedQuery = query;
286
376
  if (typeof query !== "object" || query === null) {
287
377
  normalizedQuery = { id: query };
@@ -374,7 +464,7 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
374
464
  if (!current)
375
465
  return false;
376
466
  const tableName = toSnakeCase(collection);
377
- await this._db.transaction().execute(async (tx) => {
467
+ await this.db.transaction().execute(async (tx) => {
378
468
  const colDef = this._collections.find((c) => c.slug === collection);
379
469
  if (colDef) {
380
470
  const relationalFields = getRelationalFields(colDef.fields);
@@ -403,7 +493,7 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
403
493
  }
404
494
  async updateMany(collection, query, data) {
405
495
  const tableName = toSnakeCase(collection);
406
- return this._db.transaction().execute(async (tx) => {
496
+ return this.db.transaction().execute(async (tx) => {
407
497
  let qb = tx.updateTable(tableName);
408
498
  if (query && Object.keys(query).length > 0) {
409
499
  qb = qb.where((eb) => buildKyselyWhere(eb, query) || eb.val(true));
@@ -432,7 +522,7 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
432
522
  }
433
523
  async deleteMany(collection, query) {
434
524
  const tableName = toSnakeCase(collection);
435
- return this._db.transaction().execute(async (tx) => {
525
+ return this.db.transaction().execute(async (tx) => {
436
526
  let selectQb = tx.selectFrom(tableName).select("id");
437
527
  if (query && Object.keys(query).length > 0) {
438
528
  selectQb = selectQb.where((eb) => buildKyselyWhere(eb, query) || eb.val(true));
@@ -469,7 +559,7 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
469
559
  }
470
560
  async findGlobal(slug) {
471
561
  const tableName = toSnakeCase(slug);
472
- const row = await this._db.selectFrom(tableName).selectAll().limit(1).executeTakeFirst();
562
+ const row = await this.db.selectFrom(tableName).selectAll().limit(1).executeTakeFirst();
473
563
  return row ? unflattenRow(row) : null;
474
564
  }
475
565
  async updateGlobal(slug, data) {
@@ -482,15 +572,15 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
482
572
  if (!flatData.id)
483
573
  flatData.id = "global";
484
574
  flatData[toSnakeCase("createdAt")] = now;
485
- await this._db.insertInto(tableName).values(flatData).execute();
575
+ await this.db.insertInto(tableName).values(flatData).execute();
486
576
  } else {
487
- await this._db.updateTable(tableName).set(flatData).where("id", "=", existing.id).execute();
577
+ await this.db.updateTable(tableName).set(flatData).where("id", "=", existing.id).execute();
488
578
  }
489
579
  return this.findGlobal(slug);
490
580
  }
491
581
  async runMigrations() {
492
582
  const migrator = new Migrator({
493
- db: this._db,
583
+ db: this.db,
494
584
  provider: new FileMigrationProvider({
495
585
  fs,
496
586
  path,
@@ -513,7 +603,7 @@ class SQLiteAdapter extends BaseDatabaseAdapter {
513
603
  this._collections = collections;
514
604
  this._globals = globals;
515
605
  if (this.push) {
516
- await pushSchema(this._db, "sqlite", collections, globals, {
606
+ await pushSchema(this.db, "sqlite", collections, globals, {
517
607
  pushDestructive: this.pushDestructive
518
608
  });
519
609
  }
@@ -3,7 +3,7 @@ import {
3
3
  mapFieldToPostgresType,
4
4
  mapFieldToSQLiteType,
5
5
  toSnakeCase
6
- } from "./chunk-2kyhqvhc.js";
6
+ } from "./chunk-qxt9vge8.js";
7
7
  import"./chunk-8sqjbsgt.js";
8
8
 
9
9
  // src/db/kysely/migration-generator.ts
@@ -60,6 +60,26 @@ function generateMigrationCode(collections, globals = [], dialect) {
60
60
  `;
61
61
  downCode = ` await db.schema.dropTable('${slug}').execute();
62
62
  ` + downCode;
63
+ if ("versions" in collection && collection.versions) {
64
+ const versionsTable = `${toSnakeCase(slug)}_versions`;
65
+ upCode += ` await db.schema.createTable('${versionsTable}')
66
+ `;
67
+ upCode += ` .addColumn('id', 'text', (col) => col.primaryKey())
68
+ `;
69
+ upCode += ` .addColumn('_parent_id', 'text', (col) => col.notNull())
70
+ `;
71
+ upCode += ` .addColumn('_version_data', '${dialect === "postgres" ? "jsonb" : "text"}', (col) => col.notNull())
72
+ `;
73
+ upCode += ` .addColumn('_status', 'text')
74
+ `;
75
+ upCode += ` .addColumn('created_at', '${tsType}', (col) => col.defaultTo(sql\`CURRENT_TIMESTAMP\`))
76
+ `;
77
+ upCode += ` .execute();
78
+
79
+ `;
80
+ downCode = ` await db.schema.dropTable('${versionsTable}').execute();
81
+ ` + downCode;
82
+ }
63
83
  for (const field of flattenedFields) {
64
84
  if (field.type === "relationship" && "hasMany" in field && field.hasMany) {
65
85
  const colName = toSnakeCase(field.name);
@@ -188,6 +208,24 @@ function generateSQLCode(collections, globals = []) {
188
208
  );
189
209
 
190
210
  `;
211
+ if ("versions" in collection && collection.versions) {
212
+ const versionsTable = `${toSnakeCase(slug)}_versions`;
213
+ sql += `CREATE TABLE IF NOT EXISTS "${versionsTable}" (
214
+ `;
215
+ sql += ` "id" TEXT PRIMARY KEY,
216
+ `;
217
+ sql += ` "_parent_id" TEXT NOT NULL,
218
+ `;
219
+ sql += ` "_version_data" TEXT NOT NULL,
220
+ `;
221
+ sql += ` "_status" TEXT,
222
+ `;
223
+ sql += ` "created_at" TEXT DEFAULT CURRENT_TIMESTAMP
224
+ `;
225
+ sql += `);
226
+
227
+ `;
228
+ }
191
229
  for (const field of flattenedFields) {
192
230
  if (field.type === "relationship" && "hasMany" in field && field.hasMany) {
193
231
  const colName = toSnakeCase(field.name);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-62ev8gnc.js";
3
+ } from "./chunk-t0zg026p.js";
4
4
  import {
5
5
  __require
6
6
  } from "./chunk-8sqjbsgt.js";
@@ -17,8 +17,8 @@ async function migrateCreateCommand(opaca, migrationName = "migration", outDir)
17
17
  fs.mkdirSync(fullMigrationDir, { recursive: true });
18
18
  }
19
19
  const timestamp = new Date().toISOString().replace(/[-:T]/g, "").split(".")[0];
20
- const { generateMigrationCode, generateSQLCode } = await import("./chunk-wmvjvn7b.js");
21
- const { getSystemCollections } = await import("./chunk-v521d72w.js");
20
+ const { generateMigrationCode, generateSQLCode } = await import("./chunk-6qq3ne6b.js");
21
+ const { getSystemCollections } = await import("./chunk-3rdhbedb.js");
22
22
  let dialect = "sqlite";
23
23
  if (db.name === "postgres")
24
24
  dialect = "postgres";
@@ -104,9 +104,9 @@ async function migrateD1Command(migrationDir, isRemote, binding = "DB") {
104
104
  isRemote ? "--remote" : "--local",
105
105
  "--yes"
106
106
  ];
107
- const result = spawnSync("bunx", wranglerArgs, {
107
+ const result = spawnSync("bun", ["x", ...wranglerArgs], {
108
108
  stdio: "inherit",
109
- shell: true,
109
+ shell: false,
110
110
  cwd: process.cwd()
111
111
  });
112
112
  if (result.status !== 0) {
@@ -1,3 +1,7 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-t0zg026p.js";
4
+
1
5
  // src/validation.ts
2
6
  import { z } from "zod";
3
7
  var FieldTypeSchema = z.enum([
@@ -20,7 +24,8 @@ var FieldTypeSchema = z.enum([
20
24
  "tabs",
21
25
  "join",
22
26
  "array",
23
- "virtual"
27
+ "virtual",
28
+ "ui"
24
29
  ]);
25
30
  var BaseFieldSchema = z.object({
26
31
  name: z.string(),
@@ -41,7 +46,8 @@ var BaseFieldSchema = z.object({
41
46
  components: z.object({
42
47
  Field: z.string().optional(),
43
48
  Cell: z.string().optional()
44
- }).optional()
49
+ }).optional(),
50
+ customProps: z.record(z.string(), z.any()).optional()
45
51
  }).optional()
46
52
  });
47
53
  var FieldSchema = z.lazy(() => z.discriminatedUnion("type", [
@@ -154,6 +160,9 @@ var FieldSchema = z.lazy(() => z.discriminatedUnion("type", [
154
160
  type: z.literal("virtual"),
155
161
  resolve: z.function(),
156
162
  returnType: z.string().optional()
163
+ }),
164
+ BaseFieldSchema.extend({
165
+ type: z.literal("ui")
157
166
  })
158
167
  ]));
159
168
  var AccessConfigSchema = z.object({
@@ -266,6 +275,11 @@ var OpacaAuthConfigSchema = z.object({
266
275
  enabled: z.boolean(),
267
276
  issuer: z.string()
268
277
  }).optional()
278
+ }).optional(),
279
+ logger: z.object({
280
+ disabled: z.boolean().optional(),
281
+ disableColors: z.boolean().optional(),
282
+ level: z.enum(["debug", "info", "warn", "error"]).optional()
269
283
  }).optional()
270
284
  });
271
285
  var ApiConfigSchema = z.object({
@@ -277,9 +291,19 @@ var ApiConfigSchema = z.object({
277
291
  store: z.any().optional(),
278
292
  provider: z.function().optional(),
279
293
  keyGenerator: z.function().optional()
294
+ }).optional(),
295
+ openAPI: z.object({
296
+ enabled: z.boolean().optional().default(false),
297
+ path: z.string().optional().default("/reference"),
298
+ theme: z.string().optional(),
299
+ layout: z.string().optional(),
300
+ hideModels: z.boolean().optional(),
301
+ hideDownloadButton: z.boolean().optional(),
302
+ customCss: z.string().optional()
280
303
  }).optional()
281
304
  });
282
305
  var OpacaConfigSchema = z.object({
306
+ plugins: z.array(z.any()).optional(),
283
307
  collections: z.array(CollectionSchema),
284
308
  globals: z.array(GlobalSchema).optional(),
285
309
  db: z.any(),
@@ -321,7 +345,14 @@ function recursivelyBuild(obj) {
321
345
  return built;
322
346
  }
323
347
  function defineConfig(config) {
324
- const builtConfig = recursivelyBuild(config);
348
+ let builtConfig = recursivelyBuild(config);
349
+ if (builtConfig.plugins && Array.isArray(builtConfig.plugins)) {
350
+ for (const plugin of builtConfig.plugins) {
351
+ if (plugin.onInit) {
352
+ builtConfig = plugin.onInit({ config: builtConfig, logger }) || builtConfig;
353
+ }
354
+ }
355
+ }
325
356
  const result = OpacaConfigSchema.safeParse(builtConfig);
326
357
  if (!result.success) {
327
358
  throw new Error(`Invalid OpacaCMS Configuration: ${result.error.message}`);
@@ -0,0 +1,44 @@
1
+ // src/admin/stores/ui.ts
2
+ import { persistentAtom } from "@nanostores/persistent";
3
+ import { atom } from "nanostores";
4
+ var $toasts = atom([]);
5
+ var $isSidebarCollapsed = persistentAtom("opaca-sidebar-collapsed", false, {
6
+ encode: JSON.stringify,
7
+ decode: JSON.parse
8
+ });
9
+ function toggleSidebar() {
10
+ $isSidebarCollapsed.set(!$isSidebarCollapsed.get());
11
+ }
12
+ function notify(message, type = "success") {
13
+ const id = Math.random().toString(36).substring(2, 9);
14
+ $toasts.set([...$toasts.get(), { id, message, type }]);
15
+ }
16
+ function clearToast(id) {
17
+ $toasts.set($toasts.get().filter((t) => t.id !== id));
18
+ }
19
+ var $customSidebarItems = atom([]);
20
+ function registerSidebarItem(item) {
21
+ const current = $customSidebarItems.get();
22
+ if (!current.find((i) => i.path === item.path)) {
23
+ if (item.render && !item.component) {
24
+ item.component = `opaca-plugin-render-${Math.random().toString(36).substring(2, 9)}`;
25
+ if (typeof customElements !== "undefined" && !customElements.get(item.component)) {
26
+ const componentName = item.component;
27
+ const renderFn = item.render;
28
+
29
+ class OpacaGenericPlugin extends HTMLElement {
30
+ connectedCallback() {
31
+ const serverUrl = this.getAttribute("server-url") || "";
32
+ const config = JSON.parse(this.getAttribute("config") || "{}");
33
+ const user = JSON.parse(this.getAttribute("user") || "{}");
34
+ this.innerHTML = renderFn(serverUrl, config, user);
35
+ }
36
+ }
37
+ customElements.define(componentName, OpacaGenericPlugin);
38
+ }
39
+ }
40
+ $customSidebarItems.set([...current, item]);
41
+ }
42
+ }
43
+
44
+ export { $toasts, $isSidebarCollapsed, toggleSidebar, notify, clearToast, $customSidebarItems, registerSidebarItem };
@@ -1,12 +1,15 @@
1
1
  import {
2
2
  getSystemCollections,
3
3
  init_system_schema
4
- } from "./chunk-ybbbqj63.js";
4
+ } from "./chunk-v9z61v3g.js";
5
+ import {
6
+ OpacaLogger
7
+ } from "./chunk-t0zg026p.js";
5
8
 
6
9
  // src/auth/index.ts
7
10
  import { apiKey } from "@better-auth/api-key";
8
11
  import { betterAuth } from "better-auth";
9
- import { admin } from "better-auth/plugins";
12
+ import { admin, openAPI } from "better-auth/plugins";
10
13
  import { CamelCasePlugin } from "kysely";
11
14
 
12
15
  // src/auth/premissions.ts
@@ -81,6 +84,10 @@ async function createAuth(config) {
81
84
  ac,
82
85
  roles
83
86
  }),
87
+ openAPI({
88
+ disableDefaultReference: true,
89
+ path: "/open-api"
90
+ }),
84
91
  ...userAuth.features?.apiKeys?.enabled ? [
85
92
  apiKey({
86
93
  defaultPrefix: "opaca_",
@@ -158,19 +165,50 @@ async function createAuth(config) {
158
165
  }
159
166
  }
160
167
  },
161
- plugins
168
+ plugins,
169
+ logger: {
170
+ disabled: config.logger?.disabled,
171
+ disableColors: config.logger?.disableColors,
172
+ level: config.logger?.level,
173
+ log: (level, message, ...args) => {
174
+ const rebrand = (msg) => {
175
+ if (typeof msg !== "string")
176
+ return String(msg);
177
+ return msg.replace(/\[better-auth\]\s*/g, "").replace(/BETTER_AUTH_SECRET/g, "OPACA_SECRET").replace(/`npx auth secret`/g, "`openssl rand -base64 32`").replace(/npx auth secret/g, "openssl rand -base64 32").replace(/^Warning:\s*/i, "");
178
+ };
179
+ const branded = rebrand(message);
180
+ const brandedArgs = args.map((a) => typeof a === "string" ? rebrand(a) : a);
181
+ const authLogger = new OpacaLogger(config.logger);
182
+ switch (level) {
183
+ case "debug":
184
+ authLogger.debug(branded, ...brandedArgs);
185
+ break;
186
+ case "info":
187
+ authLogger.info(branded, ...brandedArgs);
188
+ break;
189
+ case "warn":
190
+ authLogger.warn(branded, ...brandedArgs);
191
+ break;
192
+ case "error":
193
+ authLogger.error(branded, ...brandedArgs);
194
+ break;
195
+ default:
196
+ authLogger.info(branded, ...brandedArgs);
197
+ }
198
+ }
199
+ }
162
200
  });
163
201
  }
164
202
 
165
203
  // src/config-utils.ts
166
204
  init_system_schema();
167
- function sanitizeConfig(config) {
205
+ function sanitizeConfig(config, settings = {}) {
168
206
  const collections = [...config.collections];
169
207
  const supportsAuth = ["sqlite", "postgres", "d1", "bun-sqlite", "better-sqlite3"].includes(config.db.name);
170
208
  const systemCollections = getSystemCollections();
171
209
  for (const systemCol of systemCollections) {
172
210
  const isAsset = systemCol.slug === "_opaca_assets";
173
- const isAuth = ["_users", "_sessions", "_accounts", "_verifications", "_api_keys"].includes(systemCol.slug);
211
+ const isAuth = ["_users", "_sessions", "_accounts", "_verifications", "_api_keys", "_opaca_plugin_settings"].includes(systemCol.slug);
174
212
  if (isAsset && config.storages || isAuth && supportsAuth) {
175
213
  if (!collections.find((col) => col.slug === systemCol.slug)) {
176
214
  collections.push({
@@ -235,7 +273,20 @@ function sanitizeConfig(config) {
235
273
  acc[key] = {};
236
274
  return acc;
237
275
  }, {}) : {},
238
- i18n: config.i18n
276
+ i18n: config.i18n,
277
+ plugins: config.plugins?.map((p) => ({
278
+ name: p.name,
279
+ label: p.label,
280
+ description: p.description,
281
+ version: p.version,
282
+ author: p.author,
283
+ homepage: p.homepage,
284
+ icon: p.icon,
285
+ adminAssets: p.adminAssets ? p.adminAssets() : undefined,
286
+ adminUI: p.adminUI,
287
+ settings: settings[p.name] || {},
288
+ configSchema: p.configSchema ? p.configSchema.map(sanitizeField) : undefined
289
+ }))
239
290
  };
240
291
  }
241
292
 
@@ -0,0 +1,36 @@
1
+ import {
2
+ defineCustomField
3
+ } from "./chunk-rqyjjqgy.js";
4
+
5
+ // src/admin/react.tsx
6
+ import React from "react";
7
+ import { createRoot } from "react-dom/client";
8
+ function defineReactField(tagName, Component) {
9
+ defineCustomField(tagName, {
10
+ mount: (container, props) => {
11
+ const el = container;
12
+ const root = createRoot(el);
13
+ el._opacaReactRoot = root;
14
+ root.render(React.createElement(Component, props));
15
+ },
16
+ update: (container, props) => {
17
+ const el = container;
18
+ const root = el._opacaReactRoot;
19
+ if (root) {
20
+ root.render(React.createElement(Component, props));
21
+ }
22
+ },
23
+ unmount: (container) => {
24
+ const el = container;
25
+ const root = el._opacaReactRoot;
26
+ if (root) {
27
+ setTimeout(() => {
28
+ root.unmount();
29
+ delete el._opacaReactRoot;
30
+ }, 0);
31
+ }
32
+ }
33
+ });
34
+ }
35
+
36
+ export { defineReactField };