@sonicjs-cms/core 3.0.0-beta.22 → 3.0.0-beta.24

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 (108) hide show
  1. package/dist/adapters.cjs +512 -0
  2. package/dist/adapters.cjs.map +1 -0
  3. package/dist/adapters.d.cts +342 -0
  4. package/dist/adapters.d.ts +342 -0
  5. package/dist/adapters.js +501 -0
  6. package/dist/adapters.js.map +1 -0
  7. package/dist/admin-documents-form.template-55V2ZGWT.js +6 -0
  8. package/dist/{admin-documents-form.template-DDSH6ROU.js.map → admin-documents-form.template-55V2ZGWT.js.map} +1 -1
  9. package/dist/admin-documents-form.template-YNAVMJCV.cjs +19 -0
  10. package/dist/{admin-layout-catalyst.template-YQ4EMF2J.js.map → admin-documents-form.template-YNAVMJCV.cjs.map} +1 -1
  11. package/dist/admin-layout-catalyst.template-3XZXDGF7.cjs +21 -0
  12. package/dist/{admin-layout-catalyst.template-KDHKVLXR.cjs.map → admin-layout-catalyst.template-3XZXDGF7.cjs.map} +1 -1
  13. package/dist/admin-layout-catalyst.template-6UQGLCKI.js +7 -0
  14. package/dist/{admin-documents-form.template-LSZKGA5J.cjs.map → admin-layout-catalyst.template-6UQGLCKI.js.map} +1 -1
  15. package/dist/{chunk-FTXGZ3BF.cjs → chunk-37V3RET3.cjs} +21 -37
  16. package/dist/chunk-37V3RET3.cjs.map +1 -0
  17. package/dist/{chunk-3CT3SUR4.cjs → chunk-A2SN3RVL.cjs} +3 -3
  18. package/dist/chunk-A2SN3RVL.cjs.map +1 -0
  19. package/dist/{chunk-CRGUD4KC.cjs → chunk-BBEKHLTW.cjs} +9 -9
  20. package/dist/{chunk-CRGUD4KC.cjs.map → chunk-BBEKHLTW.cjs.map} +1 -1
  21. package/dist/{chunk-RMRJGMDE.js → chunk-BHND5PEV.js} +3 -3
  22. package/dist/{chunk-RMRJGMDE.js.map → chunk-BHND5PEV.js.map} +1 -1
  23. package/dist/{chunk-OBA2RYZN.js → chunk-BJROXF4N.js} +3 -3
  24. package/dist/{chunk-OBA2RYZN.js.map → chunk-BJROXF4N.js.map} +1 -1
  25. package/dist/{chunk-EUJAEB6W.cjs → chunk-BP52BFIV.cjs} +53 -4
  26. package/dist/chunk-BP52BFIV.cjs.map +1 -0
  27. package/dist/{chunk-PXNTCCPE.cjs → chunk-E66UVONV.cjs} +8 -8
  28. package/dist/{chunk-PXNTCCPE.cjs.map → chunk-E66UVONV.cjs.map} +1 -1
  29. package/dist/{chunk-CHWT5KBK.js → chunk-FHPEM77Y.js} +9 -25
  30. package/dist/chunk-FHPEM77Y.js.map +1 -0
  31. package/dist/{chunk-JFLZHOQP.cjs → chunk-I767LVN7.cjs} +531 -419
  32. package/dist/chunk-I767LVN7.cjs.map +1 -0
  33. package/dist/{chunk-N32OWET6.cjs → chunk-JMJM7DMM.cjs} +5 -5
  34. package/dist/{chunk-N32OWET6.cjs.map → chunk-JMJM7DMM.cjs.map} +1 -1
  35. package/dist/{chunk-QWSR4THH.cjs → chunk-KKNEYIZJ.cjs} +12 -5
  36. package/dist/chunk-KKNEYIZJ.cjs.map +1 -0
  37. package/dist/{chunk-73O4GPJ6.js → chunk-LNI3FYSI.js} +12 -5
  38. package/dist/chunk-LNI3FYSI.js.map +1 -0
  39. package/dist/{chunk-PYZYU7EK.cjs → chunk-QCJHCSOK.cjs} +36 -2
  40. package/dist/chunk-QCJHCSOK.cjs.map +1 -0
  41. package/dist/{chunk-MGHHPNJN.cjs → chunk-QJUJNTIU.cjs} +11 -59
  42. package/dist/chunk-QJUJNTIU.cjs.map +1 -0
  43. package/dist/{chunk-DOMXI5PM.js → chunk-RJ2O5IIE.js} +36 -2
  44. package/dist/chunk-RJ2O5IIE.js.map +1 -0
  45. package/dist/{chunk-5V62WT6M.js → chunk-THFZSDIU.js} +4 -2
  46. package/dist/chunk-THFZSDIU.js.map +1 -0
  47. package/dist/{chunk-2FCKKOA5.js → chunk-UIHIXTIS.js} +3 -3
  48. package/dist/chunk-UIHIXTIS.js.map +1 -0
  49. package/dist/{chunk-JQISFW6U.js → chunk-URIBTX27.js} +3 -3
  50. package/dist/{chunk-JQISFW6U.js.map → chunk-URIBTX27.js.map} +1 -1
  51. package/dist/{chunk-K623Q6WD.cjs → chunk-UYJQDDEE.cjs} +4 -2
  52. package/dist/chunk-UYJQDDEE.cjs.map +1 -0
  53. package/dist/{chunk-6KNTZ67K.js → chunk-VC5XVPLM.js} +234 -126
  54. package/dist/chunk-VC5XVPLM.js.map +1 -0
  55. package/dist/{chunk-I2DM27HP.js → chunk-VGEZNFWH.js} +49 -4
  56. package/dist/chunk-VGEZNFWH.js.map +1 -0
  57. package/dist/{chunk-GB2Q2DNK.js → chunk-VYQVDL2M.js} +5 -49
  58. package/dist/chunk-VYQVDL2M.js.map +1 -0
  59. package/dist/{define-plugin-DsKiGu1q.d.cts → define-plugin-DMcvEy_C.d.cts} +33 -0
  60. package/dist/{define-plugin-BYIey6sp.d.ts → define-plugin-D_6D9vVZ.d.ts} +33 -0
  61. package/dist/index.cjs +2587 -326
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/index.d.cts +30 -3
  64. package/dist/index.d.ts +30 -3
  65. package/dist/index.js +2281 -22
  66. package/dist/index.js.map +1 -1
  67. package/dist/middleware.cjs +34 -34
  68. package/dist/middleware.js +5 -5
  69. package/dist/migrations-FGXZPKDC.cjs +13 -0
  70. package/dist/{migrations-UIMA6ZL6.cjs.map → migrations-FGXZPKDC.cjs.map} +1 -1
  71. package/dist/migrations-Z6D27NAW.js +4 -0
  72. package/dist/{migrations-HLK2QF4T.js.map → migrations-Z6D27NAW.js.map} +1 -1
  73. package/dist/plugins.cjs +40 -40
  74. package/dist/plugins.d.cts +1 -1
  75. package/dist/plugins.d.ts +1 -1
  76. package/dist/plugins.js +3 -3
  77. package/dist/routes.cjs +29 -29
  78. package/dist/routes.js +9 -9
  79. package/dist/services.cjs +18 -18
  80. package/dist/services.js +2 -2
  81. package/dist/templates.cjs +17 -17
  82. package/dist/templates.js +3 -3
  83. package/dist/utils.cjs +3 -3
  84. package/dist/utils.js +1 -1
  85. package/migrations/0001_core.sql +7 -1
  86. package/package.json +10 -3
  87. package/dist/admin-documents-form.template-DDSH6ROU.js +0 -6
  88. package/dist/admin-documents-form.template-LSZKGA5J.cjs +0 -19
  89. package/dist/admin-layout-catalyst.template-KDHKVLXR.cjs +0 -21
  90. package/dist/admin-layout-catalyst.template-YQ4EMF2J.js +0 -7
  91. package/dist/chunk-2FCKKOA5.js.map +0 -1
  92. package/dist/chunk-3CT3SUR4.cjs.map +0 -1
  93. package/dist/chunk-5V62WT6M.js.map +0 -1
  94. package/dist/chunk-6KNTZ67K.js.map +0 -1
  95. package/dist/chunk-73O4GPJ6.js.map +0 -1
  96. package/dist/chunk-CHWT5KBK.js.map +0 -1
  97. package/dist/chunk-DOMXI5PM.js.map +0 -1
  98. package/dist/chunk-EUJAEB6W.cjs.map +0 -1
  99. package/dist/chunk-FTXGZ3BF.cjs.map +0 -1
  100. package/dist/chunk-GB2Q2DNK.js.map +0 -1
  101. package/dist/chunk-I2DM27HP.js.map +0 -1
  102. package/dist/chunk-JFLZHOQP.cjs.map +0 -1
  103. package/dist/chunk-K623Q6WD.cjs.map +0 -1
  104. package/dist/chunk-MGHHPNJN.cjs.map +0 -1
  105. package/dist/chunk-PYZYU7EK.cjs.map +0 -1
  106. package/dist/chunk-QWSR4THH.cjs.map +0 -1
  107. package/dist/migrations-HLK2QF4T.js +0 -4
  108. package/dist/migrations-UIMA6ZL6.cjs +0 -13
@@ -1,18 +1,18 @@
1
1
  import { isFirstUserRegistration, isRegistrationEnabled, authValidationService } from './chunk-VVYVFCML.js';
2
- import { renderDocumentFormPage, renderAlert } from './chunk-RMRJGMDE.js';
2
+ import { renderDocumentFormPage, renderAlert } from './chunk-BHND5PEV.js';
3
3
  import { getCacheService, CACHE_CONFIGS, SettingsService, getLogger, getAppInstance, buildRouteList, CATEGORY_INFO } from './chunk-2XAQTPAE.js';
4
- import { DocumentTypeRegistry, requireAuth, requireRole, optionalAuth, isPluginActive, rateLimit, AuthManager, getJwtExpirySecondsFromDb, getJwtRefreshGraceSecondsFromDb, logActivity, generateCsrfToken } from './chunk-CHWT5KBK.js';
4
+ import { DocumentTypeRegistry, requireAuth, requireRole, optionalAuth, isPluginActive, rateLimit, AuthManager, getJwtExpirySecondsFromDb, getJwtRefreshGraceSecondsFromDb, logActivity, generateCsrfToken } from './chunk-FHPEM77Y.js';
5
5
  import { DocumentsService, documentSecondsToMs, RbacService } from './chunk-3TMTYJAJ.js';
6
6
  import { nanoid } from './chunk-UWUP2VMT.js';
7
- import { getCollectionRegistry, collectionRecordToRow, loadCollectionConfigs, getVisibleCollections, PluginService, PLUGIN_REGISTRY, findPluginByCodeName, isCodeCollectionInternal, isDbDocTypeInternal, invalidateTenantCache } from './chunk-DOMXI5PM.js';
8
- import { MigrationService } from './chunk-2FCKKOA5.js';
9
- import { renderCheckboxPage, renderAlert as renderAlert$1, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-OBA2RYZN.js';
10
- import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-5V62WT6M.js';
11
- import { createServiceSingleton, definePlugin, getPluginDefinition, applySchemaDefaults, renderSchemaFields, parseFormDataToSettings } from './chunk-I2DM27HP.js';
7
+ import { getCollectionRegistry, collectionRecordToRow, loadCollectionConfigs, getVisibleCollections, PluginService, PLUGIN_REGISTRY, findPluginByCodeName, isCodeCollectionInternal, isDbDocTypeInternal, invalidateTenantCache } from './chunk-RJ2O5IIE.js';
8
+ import { MigrationService } from './chunk-UIHIXTIS.js';
9
+ import { renderCheckboxPage, renderAlert as renderAlert$1, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-BJROXF4N.js';
10
+ import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-THFZSDIU.js';
11
+ import { createServiceSingleton, definePlugin, getPluginDefinition, applySchemaDefaults, renderSchemaFields, parseFormDataToSettings, getPluginMenu } from './chunk-VGEZNFWH.js';
12
12
  import { hasHookSystem, getTypedHooks } from './chunk-RNZFGN4R.js';
13
13
  import { QueryFilterBuilder, getBlocksFieldConfig, parseBlocksValue } from './chunk-QZGABF2M.js';
14
14
  import { metricsTracker } from './chunk-FICTAGD4.js';
15
- import { getCoreVersion } from './chunk-73O4GPJ6.js';
15
+ import { getCoreVersion } from './chunk-LNI3FYSI.js';
16
16
  import { escapeHtml, sanitizeRichText, sanitizeInput } from './chunk-TQABQWOP.js';
17
17
  import { Hono } from 'hono';
18
18
  import { z } from 'zod';
@@ -149,6 +149,18 @@ var DocumentRepository = class {
149
149
  const row = await this.db.prepare("SELECT * FROM documents WHERE root_id = ? AND tenant_id = ? AND is_published = 1").bind(rootId, this.tenantId).first();
150
150
  return row ? rowToDocument(row) : null;
151
151
  }
152
+ // Slug lookup for a single doc. Prefer the published row; fall back to the current
153
+ // draft so a never-published doc is still reachable by slug (matches getById, which
154
+ // resolves drafts too). ORDER BY makes the preference deterministic when both exist.
155
+ async getBySlug(typeId, slug) {
156
+ const row = await this.db.prepare(
157
+ `SELECT * FROM documents
158
+ WHERE tenant_id = ? AND type_id = ? AND slug = ? AND deleted_at IS NULL
159
+ AND (is_published = 1 OR is_current_draft = 1)
160
+ ORDER BY is_published DESC LIMIT 1`
161
+ ).bind(this.tenantId, typeId, slug).first();
162
+ return row ? rowToDocument(row) : null;
163
+ }
152
164
  // Unified, tenant-scoped list with optional generated-column / facet filters and sort. This is the
153
165
  // single place document list SQL is built — route handlers must call this, never inline SQL (R4).
154
166
  async list(opts = {}) {
@@ -3399,7 +3411,7 @@ adminApiRoutes.patch("/collections/:id", collectionsReadOnly);
3399
3411
  adminApiRoutes.delete("/collections/:id", collectionsReadOnly);
3400
3412
  adminApiRoutes.get("/migrations/status", async (c) => {
3401
3413
  try {
3402
- const { MigrationService: MigrationService2 } = await import('./migrations-HLK2QF4T.js');
3414
+ const { MigrationService: MigrationService2 } = await import('./migrations-Z6D27NAW.js');
3403
3415
  const db = c.env.DB;
3404
3416
  const migrationService = new MigrationService2(db);
3405
3417
  const status = await migrationService.getMigrationStatus();
@@ -3442,7 +3454,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
3442
3454
  });
3443
3455
  adminApiRoutes.get("/migrations/validate", async (c) => {
3444
3456
  try {
3445
- const { MigrationService: MigrationService2 } = await import('./migrations-HLK2QF4T.js');
3457
+ const { MigrationService: MigrationService2 } = await import('./migrations-Z6D27NAW.js');
3446
3458
  const db = c.env.DB;
3447
3459
  const migrationService = new MigrationService2(db);
3448
3460
  const validation = await migrationService.validateSchema();
@@ -9008,20 +9020,20 @@ function renderContentFormPage(data, opts) {
9008
9020
  <dl class="space-y-3 text-sm">
9009
9021
  <div>
9010
9022
  <dt class="text-zinc-500 dark:text-zinc-400">Created</dt>
9011
- <dd class="mt-1 text-zinc-950 dark:text-white">${data.created_at ? new Date(data.created_at).toLocaleDateString() : "Unknown"}</dd>
9023
+ <dd class="mt-1 text-zinc-950 dark:text-white">${data.created_at ? new Date(data.created_at * 1e3).toLocaleDateString() : "Unknown"}</dd>
9012
9024
  </div>
9013
9025
  <div>
9014
9026
  <dt class="text-zinc-500 dark:text-zinc-400">Last Modified</dt>
9015
- <dd class="mt-1 text-zinc-950 dark:text-white">${data.updated_at ? new Date(data.updated_at).toLocaleDateString() : "Unknown"}</dd>
9027
+ <dd class="mt-1 text-zinc-950 dark:text-white">${data.updated_at ? new Date(data.updated_at * 1e3).toLocaleDateString() : "Unknown"}</dd>
9016
9028
  </div>
9017
9029
  <div>
9018
9030
  <dt class="text-zinc-500 dark:text-zinc-400">Author</dt>
9019
- <dd class="mt-1 text-zinc-950 dark:text-white">${data.data?.author || "Unknown"}</dd>
9031
+ <dd class="mt-1 text-zinc-950 dark:text-white">${escapeHtml(String(data.data?.author || data.author_name || "Unknown"))}</dd>
9020
9032
  </div>
9021
9033
  ${data.published_at ? `
9022
9034
  <div>
9023
9035
  <dt class="text-zinc-500 dark:text-zinc-400">Published</dt>
9024
- <dd class="mt-1 text-zinc-950 dark:text-white">${new Date(data.published_at).toLocaleDateString()}</dd>
9036
+ <dd class="mt-1 text-zinc-950 dark:text-white">${new Date(data.published_at * 1e3).toLocaleDateString()}</dd>
9025
9037
  </div>
9026
9038
  ` : ""}
9027
9039
  </dl>
@@ -11623,6 +11635,14 @@ adminContentRoutes.get("/", async (c) => {
11623
11635
  docSql += " ORDER BY updated_at DESC LIMIT ? OFFSET ?";
11624
11636
  docParams.push(limit, offset);
11625
11637
  const { results: docRows } = await db.prepare(docSql).bind(...docParams).all();
11638
+ const docUserIds = [...new Set((docRows ?? []).map((r) => r.created_by).filter(Boolean))];
11639
+ const docAuthorMap = {};
11640
+ for (let i = 0; i < docUserIds.length; i += 99) {
11641
+ const chunk = docUserIds.slice(i, i + 99);
11642
+ const ph = chunk.map(() => "?").join(",");
11643
+ const { results: uRows } = await db.prepare(`SELECT id, email FROM auth_user WHERE id IN (${ph})`).bind(...chunk).all();
11644
+ for (const u of uRows ?? []) docAuthorMap[u.id] = u.email;
11645
+ }
11626
11646
  const statusBadgeCss = {
11627
11647
  published: "bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400 ring-1 ring-inset ring-green-600/20 dark:ring-green-500/20",
11628
11648
  draft: "bg-zinc-50 dark:bg-zinc-500/10 text-zinc-700 dark:text-zinc-400 ring-1 ring-inset ring-zinc-600/20 dark:ring-zinc-500/20"
@@ -11639,7 +11659,7 @@ adminContentRoutes.get("/", async (c) => {
11639
11659
  slug: row.slug || "",
11640
11660
  modelName: docType?.displayName ?? typeId,
11641
11661
  statusBadge: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${css}">${label}</span>`,
11642
- authorName: row.created_by || "System",
11662
+ authorName: (row.created_by ? docAuthorMap[row.created_by] ?? row.created_by : null) || "System",
11643
11663
  // Document timestamps are stored in SECONDS (documents.ts), unlike legacy content rows which
11644
11664
  // store MILLISECONDS — hence the *1000 here (D23).
11645
11665
  formattedDate: new Date((row.updated_at ?? 0) * 1e3).toLocaleDateString(),
@@ -11687,8 +11707,8 @@ adminContentRoutes.get("/", async (c) => {
11687
11707
  SELECT d.root_id AS id, d.title AS title, d.slug AS slug,
11688
11708
  CASE WHEN d.is_published = 1 THEN 'published' ELSE 'draft' END AS status,
11689
11709
  d.updated_at * 1000 AS updated_at,
11690
- dt.display_name AS cdisplay, COALESCE(d.created_by, 'System') AS author_label
11691
- FROM documents d JOIN document_types dt ON dt.id = d.type_id
11710
+ dt.display_name AS cdisplay, COALESCE(u.email, d.created_by, 'System') AS author_label
11711
+ FROM documents d JOIN document_types dt ON dt.id = d.type_id LEFT JOIN auth_user u ON u.id = d.created_by
11692
11712
  WHERE ${docConds.join(" AND ")}
11693
11713
  ORDER BY updated_at DESC LIMIT ? OFFSET ?`;
11694
11714
  const { results: unionRows } = await db.prepare(unionSql).bind(...docParams, limit, offset).all();
@@ -12029,6 +12049,13 @@ adminContentRoutes.get("/:id/edit", async (c) => {
12029
12049
  if (docType && dcoll) {
12030
12050
  const fields2 = await getCollectionFields(db, dcoll.id);
12031
12051
  const flags = await loadContentEditorFlags(db);
12052
+ let authorName;
12053
+ if (docRow.created_by) {
12054
+ const authorRow = await db.prepare("SELECT name, first_name, last_name, email FROM auth_user WHERE id = ?").bind(docRow.created_by).first();
12055
+ if (authorRow) {
12056
+ authorName = authorRow.name || [authorRow.first_name, authorRow.last_name].filter(Boolean).join(" ") || authorRow.email;
12057
+ }
12058
+ }
12032
12059
  const formData2 = {
12033
12060
  id: docRow.root_id,
12034
12061
  title: docRow.title,
@@ -12043,6 +12070,7 @@ adminContentRoutes.get("/:id/edit", async (c) => {
12043
12070
  isEdit: true,
12044
12071
  referrerParams,
12045
12072
  versioningEnabled: docType?.settings?.versioning === true,
12073
+ author_name: authorName,
12046
12074
  user: user ? { name: user.email, email: user.email, role: user.role } : void 0,
12047
12075
  version: c.get("appVersion"),
12048
12076
  ...flags
@@ -12202,6 +12230,7 @@ adminContentRoutes.post("/", async (c) => {
12202
12230
  const fields = await getCollectionFields(db, collectionId);
12203
12231
  const { data, errors } = extractFieldData(fields, formData);
12204
12232
  if (Object.keys(errors).length > 0) {
12233
+ const flags = await loadContentEditorFlags(db);
12205
12234
  const formDataWithErrors = {
12206
12235
  collection,
12207
12236
  fields,
@@ -12212,7 +12241,8 @@ adminContentRoutes.post("/", async (c) => {
12212
12241
  name: user.email,
12213
12242
  email: user.email,
12214
12243
  role: user.role
12215
- } : void 0
12244
+ } : void 0,
12245
+ ...flags
12216
12246
  };
12217
12247
  if (c.req.header("HX-Request") === "true") {
12218
12248
  c.header("HX-Retarget", "#content-form-page");
@@ -12393,6 +12423,7 @@ adminContentRoutes.put("/:id", async (c) => {
12393
12423
  const fields = await getCollectionFields(db, existingContent.collection_id);
12394
12424
  const { data, errors } = extractFieldData(fields, formData);
12395
12425
  if (Object.keys(errors).length > 0) {
12426
+ const flags = await loadContentEditorFlags(db);
12396
12427
  const formDataWithErrors = {
12397
12428
  id,
12398
12429
  collection,
@@ -12405,7 +12436,8 @@ adminContentRoutes.put("/:id", async (c) => {
12405
12436
  name: user.email,
12406
12437
  email: user.email,
12407
12438
  role: user.role
12408
- } : void 0
12439
+ } : void 0,
12440
+ ...flags
12409
12441
  };
12410
12442
  if (c.req.header("HX-Request") === "true") {
12411
12443
  c.header("HX-Retarget", "#content-form-page");
@@ -13071,12 +13103,12 @@ adminContentRoutes.post("/documents/:typeId/:documentId/delete", async (c) => {
13071
13103
  adminContentRoutes.get("/documents/:typeId/:rootId/versions", async (c) => {
13072
13104
  const { typeId, rootId } = c.req.param();
13073
13105
  const db = c.env.DB;
13074
- const { renderVersionHistoryFragment } = await import('./admin-documents-form.template-DDSH6ROU.js');
13106
+ const { renderVersionHistoryFragment } = await import('./admin-documents-form.template-55V2ZGWT.js');
13075
13107
  const registry = new DocumentTypeRegistry(db);
13076
13108
  const docType = await registry.findById(typeId);
13077
13109
  if (!docType) return c.html("<div>Unknown type.</div>", 404);
13078
13110
  const result = await db.prepare(
13079
- "SELECT id, version_number, is_current_draft, is_published, status, updated_at, created_by FROM documents WHERE root_id = ? AND tenant_id = ? ORDER BY version_number DESC LIMIT 50"
13111
+ "SELECT d.id, d.version_number, d.is_current_draft, d.is_published, d.status, d.updated_at, COALESCE(u.email, d.created_by) AS created_by FROM documents d LEFT JOIN auth_user u ON u.id = d.created_by WHERE d.root_id = ? AND d.tenant_id = ? ORDER BY d.version_number DESC LIMIT 50"
13080
13112
  ).bind(rootId, reqTenant(c)).all();
13081
13113
  const versions = (result.results ?? []).map((r) => ({
13082
13114
  id: r.id,
@@ -19344,8 +19376,9 @@ function escapeHtmlAttr(value) {
19344
19376
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
19345
19377
  }
19346
19378
  function renderPluginSettingsPage(data) {
19347
- const { plugin, activity = [], user } = data;
19348
- const hasUserSettings = plugin.id in pluginSettingsComponents || Object.keys(plugin.settings || {}).some((k) => !k.startsWith("_"));
19379
+ const { plugin, activity = [], user, settingsTabData } = data;
19380
+ const pluginDef = getPluginDefinition(plugin.id || plugin.name);
19381
+ const hasUserSettings = pluginDef?.settingsTabContent != null || Object.keys(plugin.settings || {}).some((k) => !k.startsWith("_"));
19349
19382
  const defaultTab = hasUserSettings ? "settings" : "info";
19350
19383
  const pageContent = `
19351
19384
  <div class="w-full px-4 sm:px-6 lg:px-8 py-6">
@@ -19425,7 +19458,7 @@ function renderPluginSettingsPage(data) {
19425
19458
  <div id="tab-content">
19426
19459
  <!-- Settings Tab -->
19427
19460
  <div id="settings-content" class="tab-content hidden">
19428
- ${hasUserSettings ? renderSettingsTab(plugin) : ""}
19461
+ ${hasUserSettings ? renderSettingsTab(plugin, settingsTabData) : ""}
19429
19462
  </div>
19430
19463
 
19431
19464
  <!-- Activity Tab -->
@@ -19646,10 +19679,14 @@ function renderToggleButton(plugin) {
19646
19679
  }
19647
19680
  return plugin.status === "active" ? `<button onclick="togglePlugin('${plugin.id}', 'deactivate')" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">Deactivate</button>` : `<button onclick="togglePlugin('${plugin.id}', 'activate')" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">Activate</button>`;
19648
19681
  }
19649
- function renderSettingsTab(plugin) {
19682
+ function renderSettingsTab(plugin, settingsTabData) {
19650
19683
  const settings = plugin.settings || {};
19651
19684
  const pluginId = plugin.id || plugin.name;
19652
19685
  const hasUserKeys = Object.keys(settings).some((k) => !k.startsWith("_"));
19686
+ const pluginDef = getPluginDefinition(pluginId);
19687
+ if (pluginDef?.settingsTabContent) {
19688
+ return pluginDef.settingsTabContent.render({ plugin, settings, data: settingsTabData });
19689
+ }
19653
19690
  const customRenderer = pluginSettingsComponents[pluginId];
19654
19691
  if (customRenderer) {
19655
19692
  return `
@@ -19996,8 +20033,7 @@ function formatTimestamp(timestamp) {
19996
20033
  }
19997
20034
  var pluginSettingsComponents = {
19998
20035
  "otp-login": renderOTPLoginSettingsContent,
19999
- "email": renderEmailSettingsContent,
20000
- "oauth-providers": renderOAuthProvidersSettingsContent
20036
+ "email": renderEmailSettingsContent
20001
20037
  };
20002
20038
  function renderOTPLoginSettingsContent(plugin, settings) {
20003
20039
  const siteName = settings.siteName || "SonicJS";
@@ -20720,99 +20756,157 @@ defineUserProfile({
20720
20756
  </div>
20721
20757
  `;
20722
20758
  }
20723
- function renderOAuthProvidersSettingsContent(_plugin, settings) {
20724
- const providers = settings.providers || {};
20725
- const github = providers.github || {};
20726
- const google = providers.google || {};
20727
- const inputClass = "w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 focus:border-blue-500 focus:outline-none text-white";
20728
- function renderProviderCard(name, label, icon, data) {
20729
- const clientId = escapeHtmlAttr(data.clientId || "");
20730
- const clientSecret = escapeHtmlAttr(data.clientSecret || "");
20731
- const enabled = data.enabled === true;
20732
- return `
20733
- <div class="backdrop-blur-md bg-white/5 rounded-xl border border-white/10 p-6">
20734
- <div class="flex items-center justify-between mb-4">
20735
- <h3 class="text-lg font-semibold text-white flex items-center gap-2">
20736
- <span>${icon}</span> ${label}
20737
- </h3>
20738
- <label class="relative inline-flex items-center cursor-pointer">
20739
- <input type="checkbox" id="oauth_${name}_enabled" ${enabled ? "checked" : ""} class="sr-only peer">
20740
- <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
20741
- <span class="ml-2 text-sm text-gray-300">Enabled</span>
20742
- </label>
20743
- </div>
20744
- <div class="space-y-4">
20745
- <div>
20746
- <label for="oauth_${name}_clientId" class="block text-sm font-medium text-gray-300 mb-2">Client ID</label>
20747
- <input type="text" id="oauth_${name}_clientId" value="${clientId}" placeholder="${label} OAuth Client ID" class="${inputClass}">
20748
- </div>
20749
- <div>
20750
- <label for="oauth_${name}_clientSecret" class="block text-sm font-medium text-gray-300 mb-2">Client Secret</label>
20751
- <input type="password" id="oauth_${name}_clientSecret" value="${clientSecret}" placeholder="${label} OAuth Client Secret" class="${inputClass}">
20752
- </div>
20753
- </div>
20754
- </div>
20755
- `;
20756
- }
20757
- return `
20758
- <div class="space-y-6">
20759
- <div class="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
20760
- <p class="text-blue-200 text-sm">
20761
- Configure OAuth provider credentials below. Credentials are stored securely and used server-side only.
20762
- Make sure to add the callback URL <code class="text-blue-300">/auth/oauth/[provider]/callback</code> to your OAuth app's allowed redirect URIs.
20763
- </p>
20764
- </div>
20765
-
20766
- ${renderProviderCard("github", "GitHub", "\u{1F419}", github)}
20767
- ${renderProviderCard("google", "Google", "\u{1F535}", google)}
20768
- </div>
20769
20759
 
20770
- <script>
20771
- (function () {
20772
- const origSave = window.saveSettings;
20773
- window.saveSettings = async function () {
20774
- const settings = {
20775
- providers: {
20776
- github: {
20777
- clientId: document.getElementById('oauth_github_clientId').value,
20778
- clientSecret: document.getElementById('oauth_github_clientSecret').value,
20779
- enabled: document.getElementById('oauth_github_enabled').checked,
20780
- },
20781
- google: {
20782
- clientId: document.getElementById('oauth_google_clientId').value,
20783
- clientSecret: document.getElementById('oauth_google_clientSecret').value,
20784
- enabled: document.getElementById('oauth_google_enabled').checked,
20785
- },
20786
- }
20787
- };
20788
-
20789
- const saveButton = document.getElementById('save-button');
20790
- saveButton.disabled = true;
20791
- saveButton.textContent = 'Saving...';
20792
-
20793
- try {
20794
- const response = await fetch('/admin/plugins/oauth-providers/settings', {
20795
- method: 'POST',
20796
- headers: { 'Content-Type': 'application/json' },
20797
- body: JSON.stringify(settings),
20798
- });
20799
- const result = await response.json();
20800
- if (result.success) {
20801
- showNotification('Settings saved successfully', 'success');
20802
- setTimeout(() => location.reload(), 1000);
20803
- } else {
20804
- throw new Error(result.error || 'Failed to save settings');
20805
- }
20806
- } catch (error) {
20807
- showNotification(error.message, 'error');
20808
- } finally {
20809
- saveButton.disabled = false;
20810
- saveButton.textContent = 'Save Settings';
20811
- }
20812
- };
20813
- })();
20814
- </script>
20815
- `;
20760
+ // src/plugins/core-plugins/menu-plugin/services/menu-reconcile.ts
20761
+ async function upsertPluginRow(db, slug, entry, visible) {
20762
+ const now = Math.floor(Date.now() / 1e3);
20763
+ const existing = await db.prepare(
20764
+ `SELECT id FROM documents
20765
+ WHERE slug = ? AND type_id = 'menu_item' AND tenant_id = 'default'
20766
+ AND is_current_draft = 1 AND deleted_at IS NULL`
20767
+ ).bind(slug).first();
20768
+ if (existing) {
20769
+ await db.prepare(
20770
+ `UPDATE documents
20771
+ SET data = json_set(data, '$.url', ?, '$.pluginId', ?, '$.visible', ?),
20772
+ visible = ?,
20773
+ updated_at = ?
20774
+ WHERE id = ? AND tenant_id = 'default'`
20775
+ ).bind(entry.url, entry.pluginId, visible ? 1 : 0, visible ? 1 : 0, now, existing.id).run();
20776
+ return;
20777
+ }
20778
+ const id = nanoid();
20779
+ const data = JSON.stringify({
20780
+ label: entry.label,
20781
+ url: entry.url,
20782
+ icon: entry.icon,
20783
+ target: "_self",
20784
+ isExternal: false,
20785
+ visible,
20786
+ parent: null,
20787
+ source: "plugin",
20788
+ pluginId: entry.pluginId,
20789
+ permissions: [],
20790
+ lockedFields: ["url"],
20791
+ sortOrder: entry.sortOrder
20792
+ });
20793
+ await db.prepare(
20794
+ `INSERT INTO documents (id, root_id, type_id, type_version, version_of_id, version_number,
20795
+ is_current_draft, is_published, status, parent_root_id, slug, path, title, zone,
20796
+ sort_order, visible, published_at, scheduled_at, expires_at, deleted_at,
20797
+ tenant_id, locale, translation_group_id, data, metadata,
20798
+ owner_id, created_by, updated_by, created_at, updated_at)
20799
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
20800
+ ).bind(
20801
+ id,
20802
+ id,
20803
+ "menu_item",
20804
+ 1,
20805
+ null,
20806
+ 1,
20807
+ 1,
20808
+ 0,
20809
+ "draft",
20810
+ "",
20811
+ slug,
20812
+ null,
20813
+ entry.label,
20814
+ null,
20815
+ entry.sortOrder,
20816
+ visible ? 1 : 0,
20817
+ null,
20818
+ null,
20819
+ null,
20820
+ null,
20821
+ "default",
20822
+ "default",
20823
+ "",
20824
+ data,
20825
+ "{}",
20826
+ null,
20827
+ null,
20828
+ null,
20829
+ now,
20830
+ now
20831
+ ).run();
20832
+ }
20833
+ async function deactivateStalePluginRows(db, activePluginIds) {
20834
+ const now = Math.floor(Date.now() / 1e3);
20835
+ const rows = await db.prepare(
20836
+ `SELECT id, data FROM documents
20837
+ WHERE type_id = 'menu_item' AND tenant_id = 'default'
20838
+ AND is_current_draft = 1 AND deleted_at IS NULL
20839
+ AND json_extract(data, '$.source') = 'plugin'`
20840
+ ).all();
20841
+ const stale = (rows.results ?? []).filter((row) => {
20842
+ let pluginId;
20843
+ try {
20844
+ pluginId = JSON.parse(row.data)?.pluginId;
20845
+ } catch {
20846
+ return false;
20847
+ }
20848
+ return pluginId !== void 0 && !activePluginIds.has(pluginId);
20849
+ });
20850
+ if (stale.length === 0) return;
20851
+ await db.batch(
20852
+ stale.map(
20853
+ (row) => db.prepare(
20854
+ `UPDATE documents
20855
+ SET data = json_set(data, '$.visible', 0), visible = 0, updated_at = ?
20856
+ WHERE id = ? AND tenant_id = 'default'`
20857
+ ).bind(now, row.id)
20858
+ )
20859
+ );
20860
+ }
20861
+ async function fetchActivePluginIds(db) {
20862
+ try {
20863
+ const rows = await db.prepare(
20864
+ `SELECT slug FROM documents
20865
+ WHERE type_id = 'plugin' AND tenant_id = 'default'
20866
+ AND is_current_draft = 1 AND deleted_at IS NULL
20867
+ AND json_extract(data, '$.status') = 'active'`
20868
+ ).all();
20869
+ return new Set((rows.results ?? []).map((r) => r.slug));
20870
+ } catch {
20871
+ return /* @__PURE__ */ new Set();
20872
+ }
20873
+ }
20874
+ async function reconcileMenuFromPlugins(db) {
20875
+ try {
20876
+ const singletonEntries = getPluginMenu();
20877
+ const manifestEntries = Object.values(PLUGIN_REGISTRY).filter(
20878
+ (p) => p.adminMenu !== null
20879
+ );
20880
+ const byPluginId = /* @__PURE__ */ new Map();
20881
+ manifestEntries.forEach((p, idx) => {
20882
+ const menu = p.adminMenu;
20883
+ byPluginId.set(p.id, {
20884
+ pluginId: p.id,
20885
+ label: menu.label,
20886
+ url: menu.path,
20887
+ icon: menu.icon,
20888
+ sortOrder: 100 + idx * 10
20889
+ });
20890
+ });
20891
+ singletonEntries.forEach((entry, idx) => {
20892
+ const pathParts = entry.path.replace(/\/$/, "").split("/");
20893
+ const pluginId = pathParts[pathParts.length - 1] || entry.label.toLowerCase().replace(/\s+/g, "-");
20894
+ byPluginId.set(pluginId, {
20895
+ pluginId,
20896
+ label: entry.label,
20897
+ url: entry.path,
20898
+ icon: entry.icon ?? "puzzle-piece",
20899
+ sortOrder: 100 + idx * 10
20900
+ });
20901
+ });
20902
+ const activeIds = await fetchActivePluginIds(db);
20903
+ for (const [pluginId, entry] of byPluginId) {
20904
+ const slug = `menu:plugin:${pluginId}`;
20905
+ await upsertPluginRow(db, slug, entry, activeIds.has(pluginId));
20906
+ }
20907
+ await deactivateStalePluginRows(db, activeIds);
20908
+ } catch {
20909
+ }
20816
20910
  }
20817
20911
 
20818
20912
  // src/routes/admin-plugins.ts
@@ -20847,9 +20941,11 @@ adminPluginRoutes.get("/", async (c) => {
20847
20941
  } catch (error) {
20848
20942
  console.error("Error loading plugins:", error);
20849
20943
  }
20850
- const installedPluginIds = new Set(installedPlugins.map((p) => p.id));
20944
+ const registryIds = new Set(AVAILABLE_PLUGINS.map((p) => p.id));
20945
+ const validInstalledPlugins = installedPlugins.filter((p) => registryIds.has(p.id));
20946
+ const installedPluginIds = new Set(validInstalledPlugins.map((p) => p.id));
20851
20947
  const uninstalledPlugins = AVAILABLE_PLUGINS.filter((p) => !installedPluginIds.has(p.id));
20852
- const templatePlugins = installedPlugins.map((p) => ({
20948
+ const templatePlugins = validInstalledPlugins.map((p) => ({
20853
20949
  id: p.id,
20854
20950
  name: p.name,
20855
20951
  displayName: p.display_name,
@@ -20885,7 +20981,7 @@ adminPluginRoutes.get("/", async (c) => {
20885
20981
  }));
20886
20982
  const allPlugins = [...templatePlugins, ...uninstalledTemplatePlugins];
20887
20983
  stats.uninstalled = uninstalledPlugins.length;
20888
- stats.total = installedPlugins.length + uninstalledPlugins.length;
20984
+ stats.total = validInstalledPlugins.length + uninstalledPlugins.length;
20889
20985
  const pageData = {
20890
20986
  plugins: allPlugins,
20891
20987
  stats,
@@ -20984,6 +21080,15 @@ adminPluginRoutes.get("/:id", async (c) => {
20984
21080
  timestamp: item.timestamp,
20985
21081
  user: item.userId || null
20986
21082
  }));
21083
+ const pluginDef = getPluginDefinition(pluginId);
21084
+ let settingsTabData = void 0;
21085
+ if (pluginDef?.settingsTabContent?.loadData) {
21086
+ try {
21087
+ settingsTabData = await pluginDef.settingsTabContent.loadData(db);
21088
+ } catch (e) {
21089
+ console.error(`settingsTabContent.loadData failed for plugin "${pluginId}":`, e);
21090
+ }
21091
+ }
20987
21092
  const pageData = {
20988
21093
  plugin: templatePlugin,
20989
21094
  activity: templateActivity,
@@ -20991,7 +21096,8 @@ adminPluginRoutes.get("/:id", async (c) => {
20991
21096
  name: user?.email || "User",
20992
21097
  email: user?.email || "",
20993
21098
  role: user?.role || "user"
20994
- }
21099
+ },
21100
+ settingsTabData
20995
21101
  };
20996
21102
  return c.html(renderPluginSettingsPage(pageData));
20997
21103
  } catch (error) {
@@ -21069,6 +21175,7 @@ adminPluginRoutes.post("/:id/activate", async (c) => {
21069
21175
  }
21070
21176
  const pluginService = new PluginService(db);
21071
21177
  await pluginService.activatePlugin(pluginId);
21178
+ await reconcileMenuFromPlugins(db);
21072
21179
  return c.json({ success: true });
21073
21180
  } catch (error) {
21074
21181
  console.error("Error activating plugin:", error);
@@ -21086,6 +21193,7 @@ adminPluginRoutes.post("/:id/deactivate", async (c) => {
21086
21193
  }
21087
21194
  const pluginService = new PluginService(db);
21088
21195
  await pluginService.deactivatePlugin(pluginId);
21196
+ await reconcileMenuFromPlugins(db);
21089
21197
  return c.json({ success: true });
21090
21198
  } catch (error) {
21091
21199
  console.error("Error deactivating plugin:", error);
@@ -27008,6 +27116,6 @@ var ROUTES_INFO = {
27008
27116
  reference: "https://github.com/sonicjs/sonicjs"
27009
27117
  };
27010
27118
 
27011
- export { DocumentRepository, ROUTES_INFO, TenantService, VALID_MEMBER_ROLES, adminCheckboxRoutes, adminCollectionsRoutes, adminDocumentsRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_content_default, apiDocumentsRoutes, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, createDocumentSchema, createUserProfilesPlugin, defineUserProfile, denyIfNotAllowed, dispatchHookEvent, getConfirmationDialogScript2 as getConfirmationDialogScript, getCustomData, getEmailService, getUserProfileConfig, getVariablesCached, hasEmailService, invalidateVariablesCache, isValidMemberRole, lexicalEditorPlugin, renderConfirmationDialog2 as renderConfirmationDialog, renderTable2 as renderTable, resolveDocScope, resolveVariablesInObject, router, router2, setEmailService, setVariablesCache, test_cleanup_default, userProfilesPlugin, userRoutes };
27012
- //# sourceMappingURL=chunk-6KNTZ67K.js.map
27013
- //# sourceMappingURL=chunk-6KNTZ67K.js.map
27119
+ export { DocumentRepository, ROUTES_INFO, TenantService, VALID_MEMBER_ROLES, adminCheckboxRoutes, adminCollectionsRoutes, adminDocumentsRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_content_default, apiDocumentsRoutes, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, createDocumentSchema, createUserProfilesPlugin, defineUserProfile, denyIfNotAllowed, dispatchHookEvent, effectiveTenantForType, getConfirmationDialogScript2 as getConfirmationDialogScript, getCustomData, getDocumentRequestContext, getEmailService, getUserProfileConfig, getVariablesCached, hasEmailService, invalidateVariablesCache, isValidMemberRole, lexicalEditorPlugin, reconcileMenuFromPlugins, renderConfirmationDialog2 as renderConfirmationDialog, renderTable2 as renderTable, resolveDocScope, resolveVariablesInObject, router, router2, setEmailService, setVariablesCache, test_cleanup_default, updateDocumentSchema, userProfilesPlugin, userRoutes };
27120
+ //# sourceMappingURL=chunk-VC5XVPLM.js.map
27121
+ //# sourceMappingURL=chunk-VC5XVPLM.js.map