domma-cms 0.44.0 → 0.45.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.
- package/CLAUDE.md +53 -53
- package/README.md +27 -27
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +2 -2
- package/admin/js/app.js +1 -1
- package/admin/js/lib/card-builder.js +2 -2
- package/admin/js/lib/crud-tutorial.js +1 -1
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/image-editor.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +2 -2
- package/admin/js/lib/project-context.js +1 -1
- package/admin/js/lib/project-quick-create.js +1 -1
- package/admin/js/lib/scribe-composer.js +1 -1
- package/admin/js/lib/server-restart.js +1 -1
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/simple-editor.js +1 -1
- package/admin/js/lib/themes.js +1 -1
- package/admin/js/lib/timeline-builder.js +1 -1
- package/admin/js/templates/action-editor.html +8 -8
- package/admin/js/templates/api-endpoint-editor.html +7 -7
- package/admin/js/templates/api-endpoints.html +1 -1
- package/admin/js/templates/api-reference.html +1 -1
- package/admin/js/templates/block-editor.html +6 -6
- package/admin/js/templates/collection-editor.html +1 -1
- package/admin/js/templates/component-editor.html +2 -2
- package/admin/js/templates/dashboard/cache.html +3 -3
- package/admin/js/templates/dashboard/journeys.html +3 -3
- package/admin/js/templates/dashboard/kpi-strip.html +3 -3
- package/admin/js/templates/dashboard/top-pages.html +1 -1
- package/admin/js/templates/dashboard/traffic-chart.html +1 -1
- package/admin/js/templates/docs/api-actions.html +1 -1
- package/admin/js/templates/docs/api-collections.html +4 -4
- package/admin/js/templates/docs/api-pages.html +2 -2
- package/admin/js/templates/docs/api-plugins.html +1 -1
- package/admin/js/templates/docs/api-settings.html +2 -2
- package/admin/js/templates/docs/api-users.html +1 -1
- package/admin/js/templates/docs/components-howto.html +4 -4
- package/admin/js/templates/docs/components-reference.html +21 -21
- package/admin/js/templates/docs/components-rules.html +14 -14
- package/admin/js/templates/docs/components-walkthrough.html +13 -13
- package/admin/js/templates/docs/tutorial-crud.html +49 -49
- package/admin/js/templates/docs/tutorial-forms.html +4 -4
- package/admin/js/templates/docs/tutorial-plugin.html +10 -10
- package/admin/js/templates/docs/usage-actions.html +6 -6
- package/admin/js/templates/docs/usage-cta-shortcode.html +9 -9
- package/admin/js/templates/docs/usage-dconfig.html +4 -4
- package/admin/js/templates/docs/usage-navigation.html +1 -1
- package/admin/js/templates/docs/usage-pages.html +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +24 -24
- package/admin/js/templates/docs/usage-users-roles.html +3 -3
- package/admin/js/templates/docs/usage-views.html +4 -4
- package/admin/js/templates/documentation.html +1 -1
- package/admin/js/templates/effects.html +752 -752
- package/admin/js/templates/form-editor.html +5 -5
- package/admin/js/templates/forms.html +17 -17
- package/admin/js/templates/login.html +5 -5
- package/admin/js/templates/menu-editor.html +10 -10
- package/admin/js/templates/my-profile.html +17 -17
- package/admin/js/templates/navigation.html +11 -11
- package/admin/js/templates/notifications.html +1 -1
- package/admin/js/templates/page-editor.html +4 -4
- package/admin/js/templates/pro-docs.html +14 -14
- package/admin/js/templates/role-editor.html +70 -70
- package/admin/js/templates/roles.html +10 -10
- package/admin/js/templates/settings.html +10 -10
- package/admin/js/templates/tutorials.html +3 -3
- package/admin/js/templates/view-editor.html +15 -15
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/api-endpoints.js +2 -2
- package/admin/js/views/api-tokens.js +3 -3
- package/admin/js/views/blocks.js +2 -2
- package/admin/js/views/collection-editor.js +1 -1
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/component-editor.js +1 -1
- package/admin/js/views/components.js +4 -4
- package/admin/js/views/dashboard/widgets/cache.js +1 -1
- package/admin/js/views/dashboard/widgets/journeys.js +1 -1
- package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +1 -1
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/layouts.js +1 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/menu-editor.js +3 -3
- package/admin/js/views/menu-locations.js +1 -1
- package/admin/js/views/menus.js +3 -3
- package/admin/js/views/navigation.js +8 -8
- package/admin/js/views/page-editor.js +40 -40
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/plugin-code.js +2 -2
- package/admin/js/views/plugins.js +1 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/projects.js +1 -1
- package/admin/js/views/roles.js +1 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +3 -3
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/view-preview.js +1 -1
- package/admin/js/views/views-list.js +1 -1
- package/bin/cli.js +8 -8
- package/bin/lib/config-merge.js +44 -44
- package/bin/update.js +16 -16
- package/config/plugins.json +1 -1
- package/package.json +2 -2
- package/plugins/_template/admin/views/index.js +1 -1
- package/plugins/_template/config.js +1 -1
- package/plugins/_template/plugin.js +1 -1
- package/plugins/analytics/admin/templates/analytics.html +3 -3
- package/plugins/analytics/admin/views/analytics.js +1 -1
- package/plugins/analytics/config.js +1 -1
- package/plugins/analytics/plugin.js +4 -4
- package/plugins/analytics/plugin.json +1 -1
- package/plugins/analytics/public/inject-body.html +2 -2
- package/plugins/analytics/public/inject-head.html +1 -1
- package/plugins/blog/admin/templates/blog.html +4 -4
- package/plugins/blog/admin/views/blog.js +3 -3
- package/plugins/blog/admin/views/categories.js +2 -2
- package/plugins/blog/admin/views/comments.js +4 -4
- package/plugins/blog/admin/views/post-editor.js +1 -1
- package/plugins/blog/plugin.js +2 -2
- package/plugins/blog/plugin.json +4 -4
- package/plugins/contacts/admin/views/contacts.js +8 -8
- package/plugins/contacts/plugin.js +4 -4
- package/plugins/contacts/plugin.json +1 -1
- package/plugins/invoice/admin/templates/editor.html +6 -6
- package/plugins/invoice/admin/templates/index.html +4 -4
- package/plugins/invoice/admin/views/editor.js +3 -3
- package/plugins/invoice/admin/views/index.js +4 -4
- package/plugins/invoice/admin/views/issuers.js +1 -1
- package/plugins/invoice/admin/views/party-view.js +3 -3
- package/plugins/invoice/admin/views/receivers.js +1 -1
- package/plugins/invoice/config.js +1 -1
- package/plugins/invoice/plugin.js +1 -1
- package/plugins/invoice/plugin.json +4 -4
- package/plugins/notes/admin/views/notes.js +4 -4
- package/plugins/notes/plugin.js +1 -1
- package/plugins/notes/plugin.json +1 -1
- package/plugins/site-search/admin/templates/site-search.html +3 -3
- package/plugins/site-search/admin/views/site-search.js +1 -1
- package/plugins/site-search/config.js +1 -1
- package/plugins/site-search/plugin.js +4 -4
- package/plugins/site-search/plugin.json +1 -1
- package/plugins/surveys/admin/templates/results.html +1 -1
- package/plugins/surveys/admin/templates/survey-editor.html +4 -4
- package/plugins/surveys/admin/views/audience.js +6 -6
- package/plugins/surveys/admin/views/results.js +3 -3
- package/plugins/surveys/admin/views/survey-editor.js +1 -1
- package/plugins/surveys/admin/views/surveys.js +2 -2
- package/plugins/surveys/plugin.js +1 -1
- package/plugins/surveys/plugin.json +4 -4
- package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
- package/plugins/theme-switcher/plugin.json +3 -3
- package/plugins/theme-switcher/public/inject-body.html +28 -28
- package/plugins/theme-switcher/public/inject-head.html +1 -1
- package/plugins/todo/plugin.js +1 -1
- package/public/css/forms.css +1 -1
- package/public/js/collection-browser.js +1 -1
- package/public/js/collection-context.js +1 -1
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -1
- package/public/js/site.js +1 -1
- package/scripts/build.js +14 -14
- package/scripts/create-plugin.js +2 -2
- package/scripts/fresh.js +2 -2
- package/scripts/gen-instance-secret.js +1 -1
- package/scripts/pro.js +12 -12
- package/scripts/reset.js +2 -2
- package/scripts/setup.js +15 -15
- package/scripts/users.js +5 -5
- package/scripts/verify-assets.mjs +6 -6
- package/server/config.js +1 -1
- package/server/middleware/auth.js +253 -253
- package/server/middleware/managerAuth.js +3 -3
- package/server/routes/api/actions.js +4 -4
- package/server/routes/api/api-endpoints.js +7 -7
- package/server/routes/api/api-tokens.js +5 -5
- package/server/routes/api/auth.js +309 -309
- package/server/routes/api/blocks.js +3 -3
- package/server/routes/api/collections.js +18 -18
- package/server/routes/api/dashboard.js +5 -5
- package/server/routes/api/effects.js +2 -2
- package/server/routes/api/endpoints-public.js +5 -5
- package/server/routes/api/forms.js +47 -47
- package/server/routes/api/menu-locations.js +3 -3
- package/server/routes/api/menus.js +7 -7
- package/server/routes/api/navigation.js +42 -42
- package/server/routes/api/notifications.js +2 -2
- package/server/routes/api/pages.js +14 -2
- package/server/routes/api/plugin-marketplace.js +3 -3
- package/server/routes/api/plugins.js +6 -6
- package/server/routes/api/projects.js +9 -9
- package/server/routes/api/scaffold.js +7 -7
- package/server/routes/api/settings.js +6 -6
- package/server/routes/api/sidebar.js +1 -1
- package/server/routes/api/users.js +5 -5
- package/server/routes/api/versions.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/docs-public.js +3 -3
- package/server/routes/public.js +14 -14
- package/server/server.js +34 -16
- package/server/services/actions.js +19 -19
- package/server/services/adapters/FileAdapter.js +2 -2
- package/server/services/adapters/MongoAdapter.js +5 -5
- package/server/services/apiEndpoints.js +15 -15
- package/server/services/apiTokens.js +7 -7
- package/server/services/blocks.js +13 -13
- package/server/services/cache/drivers/MemoryDriver.js +3 -3
- package/server/services/cache/drivers/NoneDriver.js +1 -1
- package/server/services/cache/index.js +5 -5
- package/server/services/collections.js +14 -14
- package/server/services/components.js +9 -9
- package/server/services/connectionManager.js +4 -4
- package/server/services/content.js +25 -2
- package/server/services/docs.js +14 -14
- package/server/services/email.js +167 -167
- package/server/services/filterEngine.js +6 -6
- package/server/services/forms.js +18 -10
- package/server/services/health.js +1 -1
- package/server/services/hooks.js +7 -7
- package/server/services/images.js +4 -4
- package/server/services/managerClient.js +1 -1
- package/server/services/markdown.js +79 -79
- package/server/services/menuRender.js +5 -5
- package/server/services/menus-migration.js +3 -3
- package/server/services/menus.js +31 -31
- package/server/services/permissionRegistry.js +1 -1
- package/server/services/pluginFiles.js +5 -5
- package/server/services/pluginInstaller.js +17 -17
- package/server/services/pluginScaffold.js +1 -1
- package/server/services/plugins.js +17 -17
- package/server/services/presetCollections.js +2 -2
- package/server/services/projects.js +35 -26
- package/server/services/recipes/contact-list.json +1 -1
- package/server/services/recipes/onboarding.json +8 -8
- package/server/services/references.js +8 -8
- package/server/services/renderer.js +9 -9
- package/server/services/roles.js +9 -9
- package/server/services/rowAccess.js +10 -10
- package/server/services/scaffolder.js +30 -30
- package/server/services/sidebar-migration.js +10 -10
- package/server/services/sitemap.js +1 -1
- package/server/services/userProfiles.js +199 -199
- package/server/services/userRoles.js +7 -7
- package/server/services/users.js +302 -302
- package/server/services/versions.js +3 -3
- package/server/services/viewPipeline.js +6 -6
- package/server/services/views.js +11 -11
- package/server/templates/page.html +5 -5
|
@@ -61,7 +61,7 @@ export async function initCache(config = {}) {
|
|
|
61
61
|
*
|
|
62
62
|
* When turning on, re-initialise with the last config (or sensible defaults)
|
|
63
63
|
* so a previously disabled cache gets a real driver. When turning off, keep
|
|
64
|
-
* the driver in place but short-circuit reads/writes
|
|
64
|
+
* the driver in place but short-circuit reads/writes - preserves cached data
|
|
65
65
|
* if the user toggles back on. The persistence of this choice across restarts
|
|
66
66
|
* is the caller's responsibility (the route handler writes to config/cache.json).
|
|
67
67
|
*/
|
|
@@ -69,12 +69,12 @@ export async function setEnabled(enabled) {
|
|
|
69
69
|
if (Boolean(enabled) === _enabled) return;
|
|
70
70
|
if (enabled) {
|
|
71
71
|
if (_driver instanceof NoneDriver) {
|
|
72
|
-
// No usable driver yet
|
|
72
|
+
// No usable driver yet - bootstrap one from last-known config.
|
|
73
73
|
const cfg = {...(_lastConfig || {}), enabled: true};
|
|
74
74
|
if (!cfg.driver || cfg.driver === 'none') cfg.driver = 'memory';
|
|
75
75
|
await initCache(cfg);
|
|
76
76
|
} else {
|
|
77
|
-
// Memory/Redis driver still live
|
|
77
|
+
// Memory/Redis driver still live - flip the flag and preserve data.
|
|
78
78
|
_enabled = true;
|
|
79
79
|
}
|
|
80
80
|
} else {
|
|
@@ -110,7 +110,7 @@ export async function clear() {
|
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
112
|
* List the cache's keys with their tags and expiry. Values are deliberately
|
|
113
|
-
* excluded
|
|
113
|
+
* excluded - cached entries can be megabytes of HTML.
|
|
114
114
|
* @returns {{key: string, tags: string[], expiresAt: number|null}[]}
|
|
115
115
|
*/
|
|
116
116
|
export function listEntries() {
|
|
@@ -148,7 +148,7 @@ export function getStats() {
|
|
|
148
148
|
* Get-or-set with in-flight coalescing.
|
|
149
149
|
*
|
|
150
150
|
* Producer may return either a raw value, or an object with a `_cacheTags`
|
|
151
|
-
* array
|
|
151
|
+
* array - those tags are merged into opts.tags before storing, and the
|
|
152
152
|
* `_cacheTags` field is stripped from both the cached and returned value.
|
|
153
153
|
* This lets producers report tags discovered during rendering (e.g. the
|
|
154
154
|
* markdown pipeline noting embedded collection slugs) without the caller
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collections Service
|
|
3
|
-
* Schema-first data store
|
|
3
|
+
* Schema-first data store - one directory per collection under content/collections/{slug}/.
|
|
4
4
|
* Each collection has a schema.json (field definitions + API access config).
|
|
5
5
|
*
|
|
6
6
|
* Entry storage is delegated to the active adapter (FileAdapter or MongoAdapter).
|
|
@@ -65,11 +65,11 @@ function defaultApiAccess() {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// ---------------------------------------------------------------------------
|
|
68
|
-
// Schema (collection) operations
|
|
68
|
+
// Schema (collection) operations - always file-based
|
|
69
69
|
// ---------------------------------------------------------------------------
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* List all collections
|
|
72
|
+
* List all collections - reads all schema.json files and appends entryCount.
|
|
73
73
|
*
|
|
74
74
|
* @returns {Promise<object[]>}
|
|
75
75
|
*/
|
|
@@ -93,7 +93,7 @@ export async function listCollections() {
|
|
|
93
93
|
return results
|
|
94
94
|
.filter(r => r.status === 'fulfilled')
|
|
95
95
|
.map(r => r.value)
|
|
96
|
-
// Hand-rolled schemas can lack `title`
|
|
96
|
+
// Hand-rolled schemas can lack `title` - fall back to slug rather
|
|
97
97
|
// than crashing the whole listing (seen in the wild: a schema with
|
|
98
98
|
// `name` instead of `title` 500'd /api/collections permanently).
|
|
99
99
|
.sort((a, b) => String(a.title || a.slug || '').localeCompare(String(b.title || b.slug || '')));
|
|
@@ -215,7 +215,7 @@ export async function deleteCollection(slug) {
|
|
|
215
215
|
if (!schema) throw new Error(`Collection "${slug}" not found`);
|
|
216
216
|
|
|
217
217
|
// Remove adapter-side data first (handles MongoDB collections on
|
|
218
|
-
// non-file adapters). Prefer drop()
|
|
218
|
+
// non-file adapters). Prefer drop() - clear() only deletes documents
|
|
219
219
|
// and leaves the backing Mongo collection + its unique index behind
|
|
220
220
|
// forever.
|
|
221
221
|
try {
|
|
@@ -228,7 +228,7 @@ export async function deleteCollection(slug) {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
} catch {
|
|
231
|
-
// Ignore errors
|
|
231
|
+
// Ignore errors - directory removal handles file-backed data.
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
invalidate(slug);
|
|
@@ -244,7 +244,7 @@ export async function deleteCollection(slug) {
|
|
|
244
244
|
* Validate entry data against a collection schema.
|
|
245
245
|
*
|
|
246
246
|
* Reference-field validation (target id existence) requires async I/O and is
|
|
247
|
-
* therefore done in `validateReferences()`
|
|
247
|
+
* therefore done in `validateReferences()` - called from create/update paths
|
|
248
248
|
* after this synchronous shape check passes.
|
|
249
249
|
*
|
|
250
250
|
* @param {object} schema
|
|
@@ -268,7 +268,7 @@ export function validateEntryData(schema, data) {
|
|
|
268
268
|
* entry that actually exists in the target collection. Single-valued and
|
|
269
269
|
* array-valued references are both supported.
|
|
270
270
|
*
|
|
271
|
-
* Empty/null references are skipped here
|
|
271
|
+
* Empty/null references are skipped here - the required check in
|
|
272
272
|
* `validateEntryData()` handles those.
|
|
273
273
|
*
|
|
274
274
|
* @param {object} schema
|
|
@@ -280,7 +280,7 @@ export async function validateReferences(schema, data) {
|
|
|
280
280
|
for (const field of (schema.fields || [])) {
|
|
281
281
|
if (field.type !== 'reference') continue;
|
|
282
282
|
const targetSlug = field.reference?.collection;
|
|
283
|
-
if (!targetSlug) continue; // misconfigured schema
|
|
283
|
+
if (!targetSlug) continue; // misconfigured schema - skip silently
|
|
284
284
|
|
|
285
285
|
const raw = data[field.name];
|
|
286
286
|
if (raw === undefined || raw === null || raw === '') continue;
|
|
@@ -301,7 +301,7 @@ export async function validateReferences(schema, data) {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
// ---------------------------------------------------------------------------
|
|
304
|
-
// Entry operations
|
|
304
|
+
// Entry operations - delegated to storage adapter
|
|
305
305
|
// ---------------------------------------------------------------------------
|
|
306
306
|
|
|
307
307
|
/**
|
|
@@ -309,22 +309,22 @@ export async function validateReferences(schema, data) {
|
|
|
309
309
|
* structured filtering, and reference resolution.
|
|
310
310
|
*
|
|
311
311
|
* Delegates to the configured adapter (File or Mongo). Both adapters honour
|
|
312
|
-
* identical option semantics
|
|
312
|
+
* identical option semantics - see `filterEngine.js` for the filter DSL.
|
|
313
313
|
*
|
|
314
314
|
* @example
|
|
315
315
|
* // Paginated, sorted, simple search
|
|
316
316
|
* await listEntries('jobs', { page: 1, limit: 20, sort: 'postedAt', search: 'engineer' });
|
|
317
317
|
*
|
|
318
318
|
* @example
|
|
319
|
-
* // Structured filter
|
|
319
|
+
* // Structured filter - full-time London roles paying £50k+
|
|
320
320
|
* await listEntries('jobs', { filter: { location: 'London', type: 'full-time', salary_gte: 50000 } });
|
|
321
321
|
*
|
|
322
322
|
* @example
|
|
323
|
-
* // Row-level scoping
|
|
323
|
+
* // Row-level scoping - applications belonging to one user
|
|
324
324
|
* await listEntries('applications', { filter: { createdBy: userId } });
|
|
325
325
|
*
|
|
326
326
|
* @example
|
|
327
|
-
* // Resolve reference fields
|
|
327
|
+
* // Resolve reference fields - each entry gets `_refs.<field>` with display labels
|
|
328
328
|
* await listEntries('applications', { resolveRefs: true });
|
|
329
329
|
* // → entries[0]._refs.jobId === { id, display: 'Senior Engineer', missing: false, link: null }
|
|
330
330
|
*
|
|
@@ -159,7 +159,7 @@ export function compileComponent(source, name) {
|
|
|
159
159
|
catch (e) { return {js: null, errors: [{type: e.code, message: e.message}]}; }
|
|
160
160
|
|
|
161
161
|
const js = [
|
|
162
|
-
`// Compiled from ${name}.dmc
|
|
162
|
+
`// Compiled from ${name}.dmc - do not edit by hand.`,
|
|
163
163
|
`const template = ${toBacktickLiteral(parts.template)};`,
|
|
164
164
|
`const style = ${toBacktickLiteral(parts.style)};`,
|
|
165
165
|
`const props = ${JSON.stringify(propsObj)};`,
|
|
@@ -219,7 +219,7 @@ export async function getCompiledJs(name) {
|
|
|
219
219
|
// ─── CRUD operations ──────────────────────────────────────────────────────
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
|
-
* List all components. Lightweight metadata
|
|
222
|
+
* List all components. Lightweight metadata - does NOT compile.
|
|
223
223
|
* The `props` field is parsed from each file's <props> block so the admin
|
|
224
224
|
* list view can show props without a follow-up fetch.
|
|
225
225
|
*
|
|
@@ -245,7 +245,7 @@ export async function listComponents() {
|
|
|
245
245
|
const src = await fs.readFile(path.join(COMPONENTS_DIR, file), 'utf8');
|
|
246
246
|
const {props: propsBlock} = parseDmcSource(src);
|
|
247
247
|
props = validatePropsJson(propsBlock);
|
|
248
|
-
} catch { /* malformed
|
|
248
|
+
} catch { /* malformed - list still usable */ }
|
|
249
249
|
entries.push({
|
|
250
250
|
name,
|
|
251
251
|
size: stat?.size ?? 0,
|
|
@@ -263,7 +263,7 @@ export async function listComponents() {
|
|
|
263
263
|
try {
|
|
264
264
|
const parts = parseDmcSource(pc.source);
|
|
265
265
|
props = validatePropsJson(parts.props);
|
|
266
|
-
} catch { /* malformed plugin source
|
|
266
|
+
} catch { /* malformed plugin source - list still usable */ }
|
|
267
267
|
entries.push({
|
|
268
268
|
name: pc.name,
|
|
269
269
|
size: pc.source.length,
|
|
@@ -313,7 +313,7 @@ export async function getComponent(name) {
|
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
/**
|
|
316
|
-
* Create or update a component. Compiles BEFORE writing
|
|
316
|
+
* Create or update a component. Compiles BEFORE writing - a malformed source
|
|
317
317
|
* never reaches disk. Returns the compile errors (if any) rather than throwing.
|
|
318
318
|
*
|
|
319
319
|
* @param {string} name
|
|
@@ -352,12 +352,12 @@ export async function saveComponent(name, source, {bundled = false, meta} = {})
|
|
|
352
352
|
return {success: true, name, errors: []};
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
/** Lazy, circular-safe import
|
|
355
|
+
/** Lazy, circular-safe import - refreshes the markdown sanitiser's dm-* tag allowlist. */
|
|
356
356
|
async function _refreshSanitiserAllowlist() {
|
|
357
357
|
try {
|
|
358
358
|
const {refreshComponentTagAllowlist} = await import('./markdown.js');
|
|
359
359
|
await refreshComponentTagAllowlist();
|
|
360
|
-
} catch { /* no-op
|
|
360
|
+
} catch { /* no-op - markdown module not ready at first save */ }
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
/**
|
|
@@ -465,7 +465,7 @@ const SEED_COMPONENTS = {
|
|
|
465
465
|
}
|
|
466
466
|
</props>
|
|
467
467
|
<script>
|
|
468
|
-
// NB: Domma's factory calls data() with no \`this\` binding
|
|
468
|
+
// NB: Domma's factory calls data() with no \`this\` binding - props must be
|
|
469
469
|
// read from within onMount() / methods, which ARE bound to the component context.
|
|
470
470
|
export default {
|
|
471
471
|
data() { return { count: 0 }; },
|
|
@@ -633,7 +633,7 @@ export default {
|
|
|
633
633
|
|
|
634
634
|
/**
|
|
635
635
|
* Seed starter components into content/components/.
|
|
636
|
-
* Never overwrites existing files
|
|
636
|
+
* Never overwrites existing files - user customisations are preserved.
|
|
637
637
|
* All seeded components are marked `bundled: true` via their .meta.json.
|
|
638
638
|
*/
|
|
639
639
|
export async function seedDefaultComponents() {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Connection Manager (Pro feature)
|
|
3
3
|
*
|
|
4
4
|
* Manages MongoDB client lifecycle for named database connections.
|
|
5
|
-
* Loaded dynamically via import()
|
|
5
|
+
* Loaded dynamically via import() - never required in the free version.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* await initialise(connectionsConfig) // on server startup
|
|
@@ -15,13 +15,13 @@ const clients = new Map();
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Initialise all named MongoDB connections from config.
|
|
18
|
-
* Connects lazily
|
|
18
|
+
* Connects lazily - each client is created but connect() is deferred to first use.
|
|
19
19
|
*
|
|
20
20
|
* @param {Record<string, { type: string, uri: string, database: string, options?: object }>} connectionsConfig
|
|
21
21
|
* @returns {Promise<void>}
|
|
22
22
|
*/
|
|
23
23
|
export async function initialise(connectionsConfig) {
|
|
24
|
-
// Dynamic import
|
|
24
|
+
// Dynamic import - mongodb package is optional and not loaded in the free version.
|
|
25
25
|
const { MongoClient } = await import('mongodb');
|
|
26
26
|
|
|
27
27
|
for (const [name, cfg] of Object.entries(connectionsConfig)) {
|
|
@@ -43,7 +43,7 @@ export async function initialise(connectionsConfig) {
|
|
|
43
43
|
console.log(`[connectionManager] Connected: ${name} → ${cfg.database}`);
|
|
44
44
|
} catch (err) {
|
|
45
45
|
console.error(`[connectionManager] Failed to connect "${name}": ${err.message}`);
|
|
46
|
-
// Don't throw
|
|
46
|
+
// Don't throw - allow server to start with degraded MongoDB support.
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -79,6 +79,17 @@ export async function getPage(urlPath, opts = {}) {
|
|
|
79
79
|
/**
|
|
80
80
|
* Create a new page. Auto-creates parent directories.
|
|
81
81
|
*
|
|
82
|
+
* Every frontmatter key the caller sends is written through - the same
|
|
83
|
+
* contract as updatePage. An earlier version built `meta` from a fixed
|
|
84
|
+
* whitelist, which silently discarded `project`, `visibility`, `tags`,
|
|
85
|
+
* `category` and `theme` on the FIRST save of a page: a page created inside
|
|
86
|
+
* a project landed in core, vanished from that project's list, and yet the
|
|
87
|
+
* URL was taken, so re-creating it answered "already exists".
|
|
88
|
+
*
|
|
89
|
+
* `project: ''` means "inherit by rootUrl" - the key is omitted rather than
|
|
90
|
+
* written as null (null is the deprecated core opt-out, and it pins the page
|
|
91
|
+
* to core forever).
|
|
92
|
+
*
|
|
82
93
|
* @param {string} urlPath
|
|
83
94
|
* @param {object} frontmatter
|
|
84
95
|
* @param {string} body
|
|
@@ -91,6 +102,7 @@ export async function createPage(urlPath, frontmatter, body) {
|
|
|
91
102
|
const defaults = config.content.pageDefaults;
|
|
92
103
|
const now = new Date().toISOString();
|
|
93
104
|
const meta = {
|
|
105
|
+
...frontmatter,
|
|
94
106
|
title: frontmatter.title || 'Untitled',
|
|
95
107
|
slug: slugFromPath(urlPath),
|
|
96
108
|
description: frontmatter.description || '',
|
|
@@ -104,6 +116,7 @@ export async function createPage(urlPath, frontmatter, body) {
|
|
|
104
116
|
createdAt: now,
|
|
105
117
|
updatedAt: now
|
|
106
118
|
};
|
|
119
|
+
if (meta.project === '' || meta.project === undefined) delete meta.project;
|
|
107
120
|
|
|
108
121
|
await fs.writeFile(filePath, serialiseMarkdown(meta, body || ''), 'utf8');
|
|
109
122
|
const page = await readPageFile(filePath);
|
|
@@ -134,6 +147,11 @@ export async function updatePage(urlPath, frontmatter, body, {author} = {}) {
|
|
|
134
147
|
|
|
135
148
|
if (existingMeta.createdAt) meta.createdAt = existingMeta.createdAt;
|
|
136
149
|
|
|
150
|
+
// '' is the editor's "inherit by rootUrl" choice - drop the key so the
|
|
151
|
+
// resolver re-derives it. (null stays meaningful: the deprecated explicit
|
|
152
|
+
// core opt-out.)
|
|
153
|
+
if (meta.project === '') delete meta.project;
|
|
154
|
+
|
|
137
155
|
try {
|
|
138
156
|
await createVersion(urlPath, {author, type: 'auto', label: null});
|
|
139
157
|
} catch {
|
|
@@ -321,7 +339,7 @@ async function collectMdFiles(dir) {
|
|
|
321
339
|
* @param {object} [opts]
|
|
322
340
|
* @param {object|null} [opts.user] - Forwarded to parseMarkdown so the
|
|
323
341
|
* `[menu]` shortcode can filter items by per-user visibility. Defaults to
|
|
324
|
-
* anonymous (null)
|
|
342
|
+
* anonymous (null) - most callers (listings, admin previews) leave this unset.
|
|
325
343
|
* @returns {Promise<object>}
|
|
326
344
|
*/
|
|
327
345
|
async function readPageFile(filePath, opts = {}) {
|
|
@@ -331,7 +349,12 @@ async function readPageFile(filePath, opts = {}) {
|
|
|
331
349
|
// against menus bound to this page or its project, exactly as the navbar does.
|
|
332
350
|
const { data, content, html, usedComponents, tags } =
|
|
333
351
|
await parseMarkdown(raw, {user: opts.user || null, urlPath});
|
|
334
|
-
|
|
352
|
+
// parseMarkdown's `tags` are CACHE tags (`block:x`, `collection:y`), not
|
|
353
|
+
// the page's editorial tags. Spreading them last used to shadow the
|
|
354
|
+
// frontmatter's `tags`, so the editor loaded `block:feedback-card` into
|
|
355
|
+
// the tag pillbox and wrote it back on save, and the pages list showed
|
|
356
|
+
// cache internals in its Tags column. Keep them apart.
|
|
357
|
+
return { ...data, urlPath, content, html, usedComponents, cacheTags: tags };
|
|
335
358
|
}
|
|
336
359
|
|
|
337
360
|
/**
|
package/server/services/docs.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Domma Docs
|
|
2
|
+
* Domma Docs - request-time renderer
|
|
3
3
|
*
|
|
4
4
|
* Serves the Domma CMS handbook at /domma-docs/* directly from the admin doc
|
|
5
5
|
* HTML templates (admin/js/templates/docs/*.html) at request time. There is no
|
|
6
6
|
* baked content: a domma-cms version bump ships new templates and the docs
|
|
7
|
-
* update everywhere automatically
|
|
7
|
+
* update everywhere automatically - no porter, no per-site provisioning, no
|
|
8
8
|
* staleness. (Replaces the one-shot scripts/migrate-docs.js bake.)
|
|
9
9
|
*
|
|
10
10
|
* The publish switch is the `domma-docs` project's enabled flag, enforced by
|
|
@@ -33,7 +33,7 @@ const TPL_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../..', 'admin
|
|
|
33
33
|
// Functional nav chrome only (breadcrumb + prev/next + section nav). Everything
|
|
34
34
|
// else (hero, cards, tabs, accordion, buttons) is a native Domma component, so
|
|
35
35
|
// it follows the theme. Injected as an inline <style> per docs page at render
|
|
36
|
-
// time
|
|
36
|
+
// time - no dependency on content/custom.css.
|
|
37
37
|
const DOCS_BODY_CSS = `.doc-breadcrumb { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; max-width: 80ch; margin: 0 auto 1.25rem; font-size: .85rem; color: var(--dm-text-muted); }
|
|
38
38
|
.doc-breadcrumb a { color: var(--dm-primary); text-decoration: none; }
|
|
39
39
|
.doc-breadcrumb a:hover { text-decoration: underline; }
|
|
@@ -76,7 +76,7 @@ const MAP = [
|
|
|
76
76
|
];
|
|
77
77
|
|
|
78
78
|
// Admin-SPA hash routes and stale /resources/… paths don't resolve on the
|
|
79
|
-
// public site
|
|
79
|
+
// public site - rewrite doc-to-doc links to /domma-docs/…, admin-feature links
|
|
80
80
|
// to the admin SPA, and unwrap the rest.
|
|
81
81
|
const LINK_MAP = {
|
|
82
82
|
'#/docs/components-walkthrough': '/domma-docs/components/walkthrough',
|
|
@@ -97,7 +97,7 @@ const LINK_MAP = {
|
|
|
97
97
|
'/resources/components': '/domma-docs/components'
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
-
// The four top-level doc sections
|
|
100
|
+
// The four top-level doc sections - rendered as a persistent nav on every page.
|
|
101
101
|
const SECTIONS = [
|
|
102
102
|
{label: 'Usage', href: '/domma-docs/usage'},
|
|
103
103
|
{label: 'Tutorials', href: '/domma-docs/tutorials'},
|
|
@@ -134,13 +134,13 @@ function rewriteLinks(html) {
|
|
|
134
134
|
for (const [from, to] of Object.entries(LINK_MAP)) {
|
|
135
135
|
out = out.split(`href="${from}"`).join(`href="${to}"`);
|
|
136
136
|
}
|
|
137
|
-
// Any remaining /resources/… link has no doc equivalent
|
|
137
|
+
// Any remaining /resources/… link has no doc equivalent - unwrap to text.
|
|
138
138
|
out = out.replace(/<a\b[^>]*\shref="\/resources\/[^"]*"[^>]*>([\s\S]*?)<\/a>/gi, '$1');
|
|
139
139
|
return out;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
// Pull every inline <style> block out of the body, returning the cleaned HTML
|
|
143
|
-
// plus the extracted (deduped) CSS
|
|
143
|
+
// plus the extracted (deduped) CSS - collected PER CALL (no shared module state,
|
|
144
144
|
// which would leak across requests in a long-lived server).
|
|
145
145
|
function stripStyleBlocks(html) {
|
|
146
146
|
const styles = new Set();
|
|
@@ -182,7 +182,7 @@ function unwrapDocsBody(html) {
|
|
|
182
182
|
// Plain-text heading title, safe as a shortcode attribute value.
|
|
183
183
|
function headingText(inner) {
|
|
184
184
|
return inner.replace(/<[^>]+>/g, '')
|
|
185
|
-
.replace(/&/g, '&').replace(
|
|
185
|
+
.replace(/&/g, '&').replace(/-/g, '-').replace(/-/g, '-')
|
|
186
186
|
.replace(/"/g, "'").replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>')
|
|
187
187
|
.replace(/\s+/g, ' ').trim().replace(/"/g, "'");
|
|
188
188
|
}
|
|
@@ -200,7 +200,7 @@ function splitSections(inner, level) {
|
|
|
200
200
|
return {preamble, sections};
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
// ≥7 sections → accordion; 2
|
|
203
|
+
// ≥7 sections → accordion; 2-6 → tabs; <2 → a single card.
|
|
204
204
|
function structureBody(body) {
|
|
205
205
|
const inner = unwrapDocsBody(body);
|
|
206
206
|
const level = /<h2\b/i.test(inner) ? 2 : 3;
|
|
@@ -265,9 +265,9 @@ function buildSectionLanding(label) {
|
|
|
265
265
|
function buildHomeLanding() {
|
|
266
266
|
const hero = `[hero title="Domma Docs" tagline="The Domma CMS handbook" variant="gradient" align="center" fullwidth="true"][/hero]`;
|
|
267
267
|
const cards = [
|
|
268
|
-
{label: 'Usage', href: '/domma-docs/usage', icon: 'book-open', blurb: 'Day-to-day guides for running the CMS
|
|
268
|
+
{label: 'Usage', href: '/domma-docs/usage', icon: 'book-open', blurb: 'Day-to-day guides for running the CMS - pages, media, navigation, shortcodes and more.'},
|
|
269
269
|
{label: 'Tutorials', href: '/domma-docs/tutorials', icon: 'academic-cap', blurb: 'Step-by-step walkthroughs that build something real from start to finish.'},
|
|
270
|
-
{label: 'Components', href: '/domma-docs/components', icon: 'puzzle', blurb: 'The full component reference
|
|
270
|
+
{label: 'Components', href: '/domma-docs/components', icon: 'puzzle', blurb: 'The full component reference - shortcodes, how-tos, walkthroughs and rules.'},
|
|
271
271
|
{label: 'API Reference', href: '/domma-docs/api', icon: 'code', blurb: 'Every REST endpoint for headless and external integrations.'}
|
|
272
272
|
].map(s =>
|
|
273
273
|
`[card icon="${s.icon}" title="${heroAttr(s.label)}"]\n${escText(s.blurb)}\n\n[button label="Browse ${heroAttr(s.label)} →" href="${s.href}" variant="primary" /]\n[/card]`
|
|
@@ -278,7 +278,7 @@ function buildHomeLanding() {
|
|
|
278
278
|
// ── render ───────────────────────────────────────────────────────────────────
|
|
279
279
|
|
|
280
280
|
// Build the shortcode body for a doc path, or null if the path is unknown.
|
|
281
|
-
// Returns {title, body, styles[]}
|
|
281
|
+
// Returns {title, body, styles[]} - `styles` is the inline CSS extracted from a
|
|
282
282
|
// leaf template (empty for synthesized landings).
|
|
283
283
|
async function buildDocBody(docPath) {
|
|
284
284
|
if (docPath in LANDINGS) {
|
|
@@ -292,7 +292,7 @@ async function buildDocBody(docPath) {
|
|
|
292
292
|
try {
|
|
293
293
|
src = await readFile(join(TPL_DIR, `${tpl}.html`), 'utf8');
|
|
294
294
|
} catch {
|
|
295
|
-
return null; // template missing
|
|
295
|
+
return null; // template missing - treat as a 404
|
|
296
296
|
}
|
|
297
297
|
const {html: stripped, styles} = stripStyleBlocks(stripHeader(src));
|
|
298
298
|
const body = escapeCodeBrackets(dedentOutsidePre(rewriteLinks(stripped))).trim();
|
|
@@ -308,7 +308,7 @@ async function buildDocBody(docPath) {
|
|
|
308
308
|
/**
|
|
309
309
|
* Render a Domma Docs page to full HTML, or return null for an unknown path
|
|
310
310
|
* (the route turns that into a 404). Cached per path+origin under the static
|
|
311
|
-
* 'docs' tag
|
|
311
|
+
* 'docs' tag - invalidated on boot so a version bump never serves stale HTML.
|
|
312
312
|
*
|
|
313
313
|
* @param {string} docPath url tail after /domma-docs (e.g. 'usage/pages', '' for home)
|
|
314
314
|
* @param {{user?: object|null, baseUrl?: string}} [opts]
|