domma-cms 0.43.1 → 0.45.0
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 +25 -25
- package/admin/js/templates/form-editor.html +5 -5
- package/admin/js/templates/login.html +5 -5
- package/admin/js/templates/menu-editor.html +10 -10
- 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 +1 -1
- 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 +5 -5
- package/bin/update.js +16 -16
- package/config/site.json +86 -86
- 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 +100 -6
- 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 +11 -11
- 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 +10 -10
- 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/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 +1 -1
- 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 +4 -4
- package/server/services/userRoles.js +7 -7
- package/server/services/users.js +5 -5
- 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
- package/config/navigation.json.bak +0 -31
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scaffolder
|
|
2
|
+
* Scaffolder - one-shot creator for a working CRUD system from a recipe.
|
|
3
3
|
*
|
|
4
4
|
* A "recipe" is a JSON file under `server/services/recipes/` that describes:
|
|
5
5
|
* - one Collection (schema + initial api access rules)
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* and template variable.
|
|
15
15
|
*
|
|
16
16
|
* Atomicity: pre-flight check refuses to start if ANY target slug already
|
|
17
|
-
* exists
|
|
17
|
+
* exists - keeps the user out of a partial-create state. If pre-flight passes
|
|
18
18
|
* we apply collection → form → actions in order. Action creation requires
|
|
19
19
|
* MongoDB (Pro feature); when not configured we still complete the
|
|
20
20
|
* collection + form and report actions as skipped with a warning.
|
|
@@ -64,7 +64,7 @@ export async function listRecipes() {
|
|
|
64
64
|
options: r.options || []
|
|
65
65
|
});
|
|
66
66
|
} catch {
|
|
67
|
-
/* skip malformed recipe
|
|
67
|
+
/* skip malformed recipe - don't break the listing */
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
return recipes;
|
|
@@ -101,7 +101,7 @@ function substituteTokens(value, tokens) {
|
|
|
101
101
|
if (typeof value === 'string') {
|
|
102
102
|
return value.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (_m, key) => {
|
|
103
103
|
// Leave action template variables (entry.*, user.*, now, env.*) untouched
|
|
104
|
-
//
|
|
104
|
+
// - those are resolved at action execution time, not at scaffold time.
|
|
105
105
|
if (key.includes('.') || key === 'now') return _m;
|
|
106
106
|
if (key === 'entry' || key === 'user' || key === 'env') return _m;
|
|
107
107
|
return tokens[key] != null ? String(tokens[key]) : _m;
|
|
@@ -117,18 +117,18 @@ function substituteTokens(value, tokens) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* Resolve effective options for a recipe
|
|
120
|
+
* Resolve effective options for a recipe - recipe defaults merged with caller
|
|
121
121
|
* overrides, with non-empty user values winning. Returns a plain `{ name: value }`
|
|
122
122
|
* map suitable to pass to `substituteTokens`.
|
|
123
123
|
*
|
|
124
124
|
* **Default-chaining**: an option's `default` may template-reference earlier
|
|
125
|
-
* options via `{{otherOption}}`. This is the "namespacing" affordance
|
|
125
|
+
* options via `{{otherOption}}`. This is the "namespacing" affordance - set
|
|
126
126
|
* one `namespace` option and let `collectionSlug`, `formSlug`, `actionPrefix`
|
|
127
127
|
* default to `{{namespace}}`, `{{namespace}}-form`, `{{namespace}}` etc. The
|
|
128
128
|
* recipe author chooses which options participate; users can still override
|
|
129
129
|
* any individual option to break the chain.
|
|
130
130
|
*
|
|
131
|
-
* Resolution is left-to-right over the options array
|
|
131
|
+
* Resolution is left-to-right over the options array - so the option being
|
|
132
132
|
* referenced MUST appear earlier in the array than the option that references
|
|
133
133
|
* it. (If forward references become a recurring need we can topologically
|
|
134
134
|
* sort, but the explicit-order rule keeps recipe files readable.)
|
|
@@ -160,7 +160,7 @@ function resolveOptions(recipe, userOverrides = {}) {
|
|
|
160
160
|
* Check whether anything the recipe wants to create already exists. Returns a
|
|
161
161
|
* list of human-readable conflict descriptions; empty array means clear.
|
|
162
162
|
*
|
|
163
|
-
* Action conflict checks are skipped silently when MongoDB isn't configured
|
|
163
|
+
* Action conflict checks are skipped silently when MongoDB isn't configured -
|
|
164
164
|
* the apply phase will degrade those into warnings rather than blocking.
|
|
165
165
|
*
|
|
166
166
|
* @param {object} resolvedRecipe
|
|
@@ -188,12 +188,12 @@ async function preflight(resolvedRecipe) {
|
|
|
188
188
|
const existing = await getAction(action.slug);
|
|
189
189
|
if (existing) conflicts.push(`Action "${action.slug}" already exists`);
|
|
190
190
|
} catch {
|
|
191
|
-
// Mongo unavailable
|
|
191
|
+
// Mongo unavailable - treat as not-conflicting; the apply step
|
|
192
192
|
// will report actions as skipped instead.
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
// Roles + users are NOT treated as pre-flight conflicts
|
|
196
|
+
// Roles + users are NOT treated as pre-flight conflicts - they're a
|
|
197
197
|
// looser kind of resource that frequently already exists (re-running a
|
|
198
198
|
// recipe, admin-created role, etc.) and where overwriting would lose
|
|
199
199
|
// data the user cares about (existing role's permissions tweaks, user's
|
|
@@ -247,7 +247,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
// Project
|
|
250
|
+
// Project - auto-create if the recipe declares one and it doesn't exist.
|
|
251
251
|
// Uses the recipe's `namespace` token as the project slug, so a single user
|
|
252
252
|
// choice ties together every artefact's `meta.project` tag below.
|
|
253
253
|
let projectSlug = null;
|
|
@@ -273,7 +273,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
273
273
|
|
|
274
274
|
const conflicts = await preflight(resolved);
|
|
275
275
|
if (conflicts.length) {
|
|
276
|
-
const err = new Error(`Cannot apply recipe
|
|
276
|
+
const err = new Error(`Cannot apply recipe - conflicts:\n - ${conflicts.join('\n - ')}`);
|
|
277
277
|
err.statusCode = 409;
|
|
278
278
|
err.conflicts = conflicts;
|
|
279
279
|
throw err;
|
|
@@ -283,16 +283,16 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
283
283
|
const skipped = [];
|
|
284
284
|
const warnings = [];
|
|
285
285
|
|
|
286
|
-
// 0. Roles
|
|
286
|
+
// 0. Roles - create FIRST so subsequent actions can reference them by name.
|
|
287
287
|
// If a role with the same name already exists (re-run, admin-created),
|
|
288
|
-
// we skip silently rather than failing
|
|
288
|
+
// we skip silently rather than failing - the role's existence is what
|
|
289
289
|
// matters for the actions that reference it, not who created it.
|
|
290
290
|
const existingRoles = getRoleMap();
|
|
291
291
|
for (const role of (resolved.roles || [])) {
|
|
292
292
|
if (!role.name) continue;
|
|
293
293
|
if (existingRoles.has(role.name)) {
|
|
294
294
|
skipped.push(`role:${role.name}`);
|
|
295
|
-
warnings.push(`Role "${role.name}" already exists
|
|
295
|
+
warnings.push(`Role "${role.name}" already exists - using existing definition (recipe-defined permissions not applied)`);
|
|
296
296
|
continue;
|
|
297
297
|
}
|
|
298
298
|
try {
|
|
@@ -310,7 +310,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
// 0b. Menus
|
|
313
|
+
// 0b. Menus - write each declared menu and, if requested, map it to a slot.
|
|
314
314
|
for (const menu of (resolved.menus || [])) {
|
|
315
315
|
if (!menu.slug) continue;
|
|
316
316
|
try {
|
|
@@ -331,7 +331,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
331
331
|
let touched = false;
|
|
332
332
|
for (const slot of menu.locations) {
|
|
333
333
|
if (map[slot] && map[slot] !== menu.slug && !menu.force) {
|
|
334
|
-
warnings.push(`Slot "${slot}" already mapped to "${map[slot]}"
|
|
334
|
+
warnings.push(`Slot "${slot}" already mapped to "${map[slot]}" - left unchanged (set force:true to overwrite)`);
|
|
335
335
|
continue;
|
|
336
336
|
}
|
|
337
337
|
map[slot] = menu.slug;
|
|
@@ -358,11 +358,11 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
358
358
|
api: resolved.collection.api,
|
|
359
359
|
// rowAccess is a top-level schema field; createCollection accepts arbitrary keys via spread,
|
|
360
360
|
// but to be explicit we set it on the schema after create. Simpler: write it via the schema
|
|
361
|
-
// file directly
|
|
361
|
+
// file directly - createCollection already writes the schema, so we re-read + patch + write.
|
|
362
362
|
...(projectSlug && {meta: {project: projectSlug}})
|
|
363
363
|
});
|
|
364
364
|
|
|
365
|
-
// Patch in rowAccess (if any)
|
|
365
|
+
// Patch in rowAccess (if any) - collections.createCollection doesn't take it directly
|
|
366
366
|
if (resolved.collection.rowAccess) {
|
|
367
367
|
const {getCollection: get, updateCollection} = await import('./collections.js');
|
|
368
368
|
const cur = await get(tokens.collectionSlug);
|
|
@@ -386,7 +386,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
386
386
|
created.form = tokens.formSlug;
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
// 2b. Users
|
|
389
|
+
// 2b. Users - recipe-defined seed accounts. Password is mandatory (the
|
|
390
390
|
// scaffolder won't invent one); if omitted, the user is reported as
|
|
391
391
|
// skipped so the admin knows to create them manually. Auto-creating
|
|
392
392
|
// accounts with a default password would be a meaningful security trap.
|
|
@@ -398,14 +398,14 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
398
398
|
const existing = await getUserByEmail(u.email);
|
|
399
399
|
if (existing) {
|
|
400
400
|
skipped.push(`user:${u.email}`);
|
|
401
|
-
warnings.push(`User "${u.email}" already exists
|
|
401
|
+
warnings.push(`User "${u.email}" already exists - left unchanged`);
|
|
402
402
|
continue;
|
|
403
403
|
}
|
|
404
404
|
} catch { /* no-op */ }
|
|
405
405
|
|
|
406
406
|
if (!u.password) {
|
|
407
407
|
skipped.push(`user:${u.email}`);
|
|
408
|
-
warnings.push(`User "${u.email}" skipped
|
|
408
|
+
warnings.push(`User "${u.email}" skipped - no password set in recipe. Create the account manually under Users → New.`);
|
|
409
409
|
continue;
|
|
410
410
|
}
|
|
411
411
|
try {
|
|
@@ -423,7 +423,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
-
// 3. Actions
|
|
426
|
+
// 3. Actions - best-effort; skip with warning when Mongo isn't available
|
|
427
427
|
for (const action of (resolved.actions || [])) {
|
|
428
428
|
if (!action.slug) continue;
|
|
429
429
|
try {
|
|
@@ -441,7 +441,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
441
441
|
created.actions.push(a.slug);
|
|
442
442
|
} catch (err) {
|
|
443
443
|
const reason = err.message.includes('MongoDB') || err.message.includes('Mongo')
|
|
444
|
-
? `MongoDB not configured
|
|
444
|
+
? `MongoDB not configured - action "${action.slug}" skipped (Pro feature)`
|
|
445
445
|
: `Action "${action.slug}" failed: ${err.message}`;
|
|
446
446
|
skipped.push(action.slug);
|
|
447
447
|
warnings.push(reason);
|
|
@@ -449,17 +449,17 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
449
449
|
}
|
|
450
450
|
|
|
451
451
|
// Wire the form's actionSlug to the first created action if the recipe
|
|
452
|
-
// wanted it
|
|
452
|
+
// wanted it - convenient for "submit this form, fire that action" patterns.
|
|
453
453
|
if (created.form && created.actions.length && resolved.form?.settings?.actionSlug === '') {
|
|
454
454
|
try {
|
|
455
455
|
const formDef = await readForm(created.form);
|
|
456
456
|
formDef.settings = formDef.settings || {};
|
|
457
457
|
formDef.settings.actionSlug = created.actions[0];
|
|
458
458
|
await writeForm(created.form, formDef);
|
|
459
|
-
} catch { /* non-fatal
|
|
459
|
+
} catch { /* non-fatal - admin can wire it manually */ }
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
// API tokens
|
|
462
|
+
// API tokens - generated at apply time; the plaintext rides ONCE in
|
|
463
463
|
// `created.apiTokens[].token` (only a hash is stored). Idempotent:
|
|
464
464
|
// re-applying never re-issues an existing name+project token, so the
|
|
465
465
|
// credential the user already deployed survives recipe re-runs.
|
|
@@ -471,7 +471,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
471
471
|
if (!t.name) continue;
|
|
472
472
|
if (await findTokenByName(t.name, tokenProject)) {
|
|
473
473
|
skipped.push(`apiToken:${t.name}`);
|
|
474
|
-
warnings.push(`API token "${t.name}" already exists for project "${tokenProject}"
|
|
474
|
+
warnings.push(`API token "${t.name}" already exists for project "${tokenProject}" - left unchanged (token value NOT re-issued)`);
|
|
475
475
|
continue;
|
|
476
476
|
}
|
|
477
477
|
try {
|
|
@@ -489,7 +489,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
489
489
|
}
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
// Custom API endpoints
|
|
492
|
+
// Custom API endpoints - declared as definition objects (path, collection,
|
|
493
493
|
// filter, ...). Idempotent: an existing (project, path-shape) match is
|
|
494
494
|
// skipped so re-applying a recipe never clobbers a tuned definition.
|
|
495
495
|
const endpointDecls = resolved.apiEndpoints ? [].concat(resolved.apiEndpoints) : [];
|
|
@@ -500,7 +500,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
|
|
|
500
500
|
if (!ep.path) continue;
|
|
501
501
|
if (await findEndpointByPath(epProject, ep.path)) {
|
|
502
502
|
skipped.push(`apiEndpoint:${ep.path}`);
|
|
503
|
-
warnings.push(`API endpoint "${ep.path}" already exists for project "${epProject}"
|
|
503
|
+
warnings.push(`API endpoint "${ep.path}" already exists for project "${epProject}" - left unchanged`);
|
|
504
504
|
continue;
|
|
505
505
|
}
|
|
506
506
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Admin sidebar auto-migration.
|
|
3
3
|
*
|
|
4
|
-
* Runs once on server boot. Idempotent
|
|
4
|
+
* Runs once on server boot. Idempotent - guarded by the existence of
|
|
5
5
|
* config/menus/admin-sidebar.json. Seeds the admin sidebar menu with the
|
|
6
6
|
* standard tree (Overview, Projects, Content, Data, System, Documentation)
|
|
7
7
|
* and patches config/menu-locations.json to map the `admin-sidebar` slot.
|
|
@@ -148,7 +148,7 @@ async function readJson(p) {
|
|
|
148
148
|
|
|
149
149
|
async function writeJson(p, data) {
|
|
150
150
|
await fs.writeFile(p, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
151
|
-
// Menu files written outside the menus service
|
|
151
|
+
// Menu files written outside the menus service - drop its cached index so
|
|
152
152
|
// binding resolution sees the seeded sidebar menu.
|
|
153
153
|
invalidateMenuIndex();
|
|
154
154
|
}
|
|
@@ -180,7 +180,7 @@ export async function runMigration(opts = {}) {
|
|
|
180
180
|
|
|
181
181
|
let locations = {};
|
|
182
182
|
if (await exists(locPath)) {
|
|
183
|
-
try { locations = await readJson(locPath); } catch { /* malformed
|
|
183
|
+
try { locations = await readJson(locPath); } catch { /* malformed - start fresh */ }
|
|
184
184
|
}
|
|
185
185
|
locations['admin-sidebar'] = 'admin-sidebar';
|
|
186
186
|
await writeJson(locPath, locations);
|
|
@@ -193,8 +193,8 @@ export async function runMigration(opts = {}) {
|
|
|
193
193
|
* Append-if-absent: ensure a single item exists in the persisted admin
|
|
194
194
|
* sidebar menu. Lets updates surface new admin pages on EXISTING installs
|
|
195
195
|
* (the seed above only reaches fresh ones) without clobbering admin edits:
|
|
196
|
-
* if any node in the tree already has the item's url
|
|
197
|
-
* or hidden
|
|
196
|
+
* if any node in the tree already has the item's url - even moved, renamed,
|
|
197
|
+
* or hidden - this is a no-op.
|
|
198
198
|
*
|
|
199
199
|
* Returns `{ensured: boolean, reason?: string}`.
|
|
200
200
|
*
|
|
@@ -237,13 +237,13 @@ export async function ensureSidebarItem({groupText, item}, opts = {}) {
|
|
|
237
237
|
* per-topic submenus. Idempotent and non-clobbering:
|
|
238
238
|
*
|
|
239
239
|
* - If the persisted menu has no "Documentation" group, do nothing (the admin
|
|
240
|
-
* may have removed it deliberately
|
|
241
|
-
* - Convert each flat leaf
|
|
242
|
-
* (`#/tutorials`) and "API Reference" (`#/api-reference`)
|
|
240
|
+
* may have removed it deliberately - don't resurrect it).
|
|
241
|
+
* - Convert each flat leaf - "Usage" (`#/documentation`), "Tutorials"
|
|
242
|
+
* (`#/tutorials`) and "API Reference" (`#/api-reference`) - into its
|
|
243
243
|
* submenu, replacing the node in place so its position is preserved. A leaf
|
|
244
244
|
* that the admin already turned into a parent, or whose new pages already
|
|
245
245
|
* appear anywhere in the group, is left untouched.
|
|
246
|
-
* - Add the Components submenu only if `#/docs/components` isn't present yet
|
|
246
|
+
* - Add the Components submenu only if `#/docs/components` isn't present yet -
|
|
247
247
|
* inserted before "API Reference" when that node exists.
|
|
248
248
|
*
|
|
249
249
|
* Returns `{updated: boolean, reason?: string}`.
|
|
@@ -290,7 +290,7 @@ export async function ensureDocumentationSubmenus(opts = {}) {
|
|
|
290
290
|
changed = true;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
// Components submenu
|
|
293
|
+
// Components submenu - add if absent, before API Reference.
|
|
294
294
|
if (!hasUrl(docGroup.items, '#/docs/components')) {
|
|
295
295
|
const apiIdx = findByText('API Reference');
|
|
296
296
|
const node = DOC_COMPONENTS_SUBMENU();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sitemap Service
|
|
3
3
|
* Builds sitemap.xml from the page list. Filter logic lives in
|
|
4
|
-
* shouldIncludeInSitemap()
|
|
4
|
+
* shouldIncludeInSitemap() - see the TODO below.
|
|
5
5
|
*
|
|
6
6
|
* Cache: callers should wrap generate() with the 'sitemap' tag so it
|
|
7
7
|
* invalidates whenever a page is created, updated, renamed, or deleted.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User Profiles Service
|
|
3
|
-
* Preset Collection
|
|
3
|
+
* Preset Collection - stores extended profile data for each user,
|
|
4
4
|
* keyed by user UUID. Seeded on startup; entries auto-managed on user lifecycle.
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'fs/promises';
|
|
@@ -70,7 +70,7 @@ async function writeData(entries) {
|
|
|
70
70
|
export async function seed() {
|
|
71
71
|
await fs.mkdir(DIR, {recursive: true});
|
|
72
72
|
|
|
73
|
-
// Only write schema if absent
|
|
73
|
+
// Only write schema if absent - admin edits to fields must be preserved across restarts
|
|
74
74
|
try {
|
|
75
75
|
await fs.access(SCHEMA_PATH);
|
|
76
76
|
} catch {
|
|
@@ -164,7 +164,7 @@ export async function deleteProfile(userId) {
|
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
166
|
* Return a Map of userId → profile data object.
|
|
167
|
-
* Reads data.json once
|
|
167
|
+
* Reads data.json once - use this for list endpoints to avoid N+1 reads.
|
|
168
168
|
*
|
|
169
169
|
* @returns {Promise<Map<string, object>>}
|
|
170
170
|
*/
|
|
@@ -178,7 +178,7 @@ export async function listProfiles() {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
|
-
* Migration helper
|
|
181
|
+
* Migration helper - ensure every existing user has a profile entry.
|
|
182
182
|
* Called once on startup after seed().
|
|
183
183
|
*
|
|
184
184
|
* @returns {Promise<void>}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* User Roles
|
|
2
|
+
* User Roles - multi-role support helpers.
|
|
3
3
|
*
|
|
4
4
|
* A user has ONE primary role (`user.role`) and an optional array of
|
|
5
5
|
* additional roles (`user.additionalRoles`). Permission, visibility, and
|
|
6
6
|
* row-access checks consult ALL of them and grant access if any one role
|
|
7
|
-
* satisfies
|
|
7
|
+
* satisfies - "any path wins" semantics that matches how most apps actually
|
|
8
8
|
* model multi-membership (you can be both a candidate AND a recruiter).
|
|
9
9
|
*
|
|
10
10
|
* Hierarchical-level comparisons (`canManageUser`, `requireAdmin`, the
|
|
11
11
|
* level-clamp in `checkSingleVisibility`) use `getEffectiveLevel` which
|
|
12
|
-
* returns the LOWEST level number across all the user's roles
|
|
12
|
+
* returns the LOWEST level number across all the user's roles - i.e. the
|
|
13
13
|
* highest privilege the user has access to. This is what makes "additional
|
|
14
14
|
* admin" meaningfully grant admin-tier access without rewriting the level
|
|
15
15
|
* arithmetic everywhere.
|
|
16
16
|
*
|
|
17
17
|
* Backward compatibility: users with no `additionalRoles` field behave
|
|
18
|
-
* exactly as before
|
|
18
|
+
* exactly as before - `getEffectiveRoles` returns `[user.role]`, `getEffectiveLevel`
|
|
19
19
|
* returns `getRoleLevel(user.role)`. Existing callers that read `user.role`
|
|
20
20
|
* directly continue to work; the new helpers are opt-in for callers that
|
|
21
21
|
* want the union semantics.
|
|
@@ -40,7 +40,7 @@ export function getEffectiveRoles(user) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Return the effective role level for a user
|
|
43
|
+
* Return the effective role level for a user - the LOWEST level number
|
|
44
44
|
* across all roles. Lower number = higher privilege, so the user's most
|
|
45
45
|
* privileged role wins all hierarchical checks.
|
|
46
46
|
*
|
|
@@ -57,7 +57,7 @@ export function getEffectiveLevel(user) {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Check whether a user holds a specific role (primary or additional).
|
|
60
|
-
* Exact-name match
|
|
60
|
+
* Exact-name match - does NOT walk the hierarchy. For hierarchical "user
|
|
61
61
|
* is at least this level" checks use `getEffectiveLevel` against the target
|
|
62
62
|
* role's level instead.
|
|
63
63
|
*
|
|
@@ -72,7 +72,7 @@ export function userHasRole(user, role) {
|
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Check whether a user holds ANY of the given roles. Convenience helper
|
|
75
|
-
* for "anyone in this set can do X" patterns
|
|
75
|
+
* for "anyone in this set can do X" patterns - equivalent to OR-ing
|
|
76
76
|
* `userHasRole` over the list.
|
|
77
77
|
*
|
|
78
78
|
* @param {object|null} user
|
package/server/services/users.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User Storage Service
|
|
3
|
-
* File-based CRUD
|
|
3
|
+
* File-based CRUD - one JSON file per user in content/users/{id}.json.
|
|
4
4
|
* Mirrors the page-per-file pattern from the content service.
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'fs/promises';
|
|
@@ -96,7 +96,7 @@ export async function getUserById(id) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* Get a user by email address
|
|
99
|
+
* Get a user by email address - includes password hash (needed for login).
|
|
100
100
|
*
|
|
101
101
|
* @param {string} email
|
|
102
102
|
* @returns {Promise<object|null>}
|
|
@@ -135,7 +135,7 @@ export async function createUser({ name, email, password, role = 'user', additio
|
|
|
135
135
|
name: name.trim(),
|
|
136
136
|
password: hash,
|
|
137
137
|
role,
|
|
138
|
-
// Additional roles beyond the primary
|
|
138
|
+
// Additional roles beyond the primary - permission/visibility checks
|
|
139
139
|
// grant access if ANY role satisfies. The primary `role` is what
|
|
140
140
|
// appears in UI badges and is used for hierarchical level comparisons
|
|
141
141
|
// when a single value is needed.
|
|
@@ -180,7 +180,7 @@ export async function updateUser(id, updates) {
|
|
|
180
180
|
|
|
181
181
|
await writeUserFile(updated);
|
|
182
182
|
|
|
183
|
-
// Invalidate per-user cache when the project access-scope changes
|
|
183
|
+
// Invalidate per-user cache when the project access-scope changes - downstream
|
|
184
184
|
// request handlers cache scope-filtered responses keyed by `user:<id>`.
|
|
185
185
|
if (updates.projects !== undefined) {
|
|
186
186
|
const before = JSON.stringify(user.projects || []);
|
|
@@ -208,7 +208,7 @@ export async function deleteUser(id) {
|
|
|
208
208
|
try {
|
|
209
209
|
await deleteProfile(id);
|
|
210
210
|
} catch {
|
|
211
|
-
// Profile deletion is best-effort
|
|
211
|
+
// Profile deletion is best-effort - never block user deletion
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
@@ -33,7 +33,7 @@ function versionsDirForPath(urlPath) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Validate a version filename
|
|
36
|
+
* Validate a version filename - must not contain path separators and must end in .md.
|
|
37
37
|
*
|
|
38
38
|
* @param {string} filename
|
|
39
39
|
* @throws {Error}
|
|
@@ -120,7 +120,7 @@ export async function createVersion(urlPath, {author = 'system', type = 'auto',
|
|
|
120
120
|
try {
|
|
121
121
|
content = await fs.readFile(livePath, 'utf8');
|
|
122
122
|
} catch {
|
|
123
|
-
return null; // Page doesn't exist yet
|
|
123
|
+
return null; // Page doesn't exist yet - nothing to snapshot
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
const dir = versionsDirForPath(urlPath);
|
|
@@ -182,7 +182,7 @@ export async function pruneVersions(urlPath, {keep = 5} = {}) {
|
|
|
182
182
|
const dir = versionsDirForPath(urlPath);
|
|
183
183
|
const meta = await readMeta(dir);
|
|
184
184
|
|
|
185
|
-
// Newest first
|
|
185
|
+
// Newest first - index 0 is the most recent snapshot
|
|
186
186
|
const sorted = [...meta].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
187
187
|
|
|
188
188
|
// Manually-named snapshots are always preserved (deliberate restore points).
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Executes the Views feature's allowed aggregation-stage subset in plain
|
|
5
5
|
* JavaScript so views work on file-backed collections (no MongoDB needed).
|
|
6
|
-
* MongoDB-backed sources keep using native aggregation in views.js
|
|
6
|
+
* MongoDB-backed sources keep using native aggregation in views.js - this
|
|
7
7
|
* module exists so the SAME view definition produces the same results on
|
|
8
8
|
* either storage adapter. Documents are the raw entry shape ({id, data,
|
|
9
9
|
* meta}) and field paths are dot-paths ('data.price', 'meta.createdAt').
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* that visibly "should" match. (Same trade filterEngine makes.)
|
|
15
15
|
* - $sort places missing/null values LAST regardless of direction (Mongo
|
|
16
16
|
* treats missing as lowest).
|
|
17
|
-
* - $addFields supports literals and '$path' references only
|
|
17
|
+
* - $addFields supports literals and '$path' references only - operator
|
|
18
18
|
* expressions throw a clear error rather than mis-evaluating.
|
|
19
19
|
*
|
|
20
20
|
* Pure module: no imports from other services. $lookup resolves foreign
|
|
@@ -103,7 +103,7 @@ function deletePath(target, path) {
|
|
|
103
103
|
if (typeof node === 'object' && node !== null) delete node[parts[parts.length - 1]];
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/** Number('') is 0
|
|
106
|
+
/** Number('') is 0 - treat empty strings as non-numeric like filterEngine. */
|
|
107
107
|
function maybeNumber(value) {
|
|
108
108
|
if (typeof value === 'number') return value;
|
|
109
109
|
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
|
|
@@ -342,7 +342,7 @@ function evalProject(docs, config) {
|
|
|
342
342
|
|
|
343
343
|
return docs.map((doc) => {
|
|
344
344
|
const out = {};
|
|
345
|
-
// id carried unless explicitly excluded ({..., id: 0}
|
|
345
|
+
// id carried unless explicitly excluded ({..., id: 0} - mongo's _id exception)
|
|
346
346
|
const idExcluded = entries.some(([k, v]) => (k === 'id' || k === '_id') && (v === 0 || v === false));
|
|
347
347
|
if (!idExcluded && doc.id !== undefined) out.id = doc.id;
|
|
348
348
|
for (const [path, spec] of inclusions) {
|
|
@@ -522,7 +522,7 @@ async function evalLookup(docs, config, resolveCollection, lookupCache) {
|
|
|
522
522
|
throw new ViewPipelineError('$lookup requires a collection resolver', '$lookup');
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
-
// Mongo-authored views reference `cms_<slug>` collection names
|
|
525
|
+
// Mongo-authored views reference `cms_<slug>` collection names - accept both.
|
|
526
526
|
const slug = String(from).replace(/^cms_/, '');
|
|
527
527
|
if (!lookupCache.has(slug)) {
|
|
528
528
|
lookupCache.set(slug, await resolveCollection(slug));
|
|
@@ -548,7 +548,7 @@ async function evalLookup(docs, config, resolveCollection, lookupCache) {
|
|
|
548
548
|
|
|
549
549
|
/**
|
|
550
550
|
* Execute a pipeline (mongo-form stage objects) over an array of documents.
|
|
551
|
-
* Returns the FULL result set
|
|
551
|
+
* Returns the FULL result set - the caller paginates.
|
|
552
552
|
*
|
|
553
553
|
* @param {object[]} docs
|
|
554
554
|
* @param {object[]} stages - [{$match: {...}}, {$sort: {...}}, ...]
|
package/server/services/views.js
CHANGED
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
* - everything else → the built-in JS pipeline evaluator (viewPipeline.js)
|
|
11
11
|
* over the collection adapter's documents
|
|
12
12
|
*
|
|
13
|
-
* The same view definition therefore works on both storage adapters
|
|
13
|
+
* The same view definition therefore works on both storage adapters -
|
|
14
14
|
* field paths are raw-document dot-paths ('data.price', 'meta.createdAt')
|
|
15
15
|
* in both cases.
|
|
16
16
|
*
|
|
17
17
|
* Older installs that stored view configs in the MongoDB `cms__views`
|
|
18
|
-
* collection are migrated to files automatically (one-time, read-only
|
|
18
|
+
* collection are migrated to files automatically (one-time, read-only -
|
|
19
19
|
* the Mongo collection is never modified).
|
|
20
20
|
*
|
|
21
21
|
* Allowed aggregation stage types:
|
|
@@ -40,10 +40,10 @@ export const VIEWS_DIR = path.join(ROOT, 'content', 'views');
|
|
|
40
40
|
/** Legacy MongoDB collection where pre-0.34 view configs were stored. */
|
|
41
41
|
const LEGACY_VIEWS_COLLECTION = 'cms__views';
|
|
42
42
|
|
|
43
|
-
/** Prefix applied to CMS data collections
|
|
43
|
+
/** Prefix applied to CMS data collections - mirrors MongoAdapter.PREFIX. */
|
|
44
44
|
const COLLECTION_PREFIX = 'cms_';
|
|
45
45
|
|
|
46
|
-
/** Filename/lookup guard
|
|
46
|
+
/** Filename/lookup guard - view slugs are slugified, so keep this strict. */
|
|
47
47
|
const SLUG_RE = /^[a-z0-9-]+$/;
|
|
48
48
|
|
|
49
49
|
const ALLOWED_STAGES = new Set([
|
|
@@ -103,7 +103,7 @@ function buildPipelineStages(stages) {
|
|
|
103
103
|
// ---------------------------------------------------------------------------
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
* Ensure the views directory exists. Lazy
|
|
106
|
+
* Ensure the views directory exists. Lazy - engine updates never touch
|
|
107
107
|
* content/, so the dir is created on first write.
|
|
108
108
|
*
|
|
109
109
|
* @returns {Promise<void>}
|
|
@@ -113,7 +113,7 @@ export async function ensureViewsDir() {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* Read a single view config by slug (no migration hook
|
|
116
|
+
* Read a single view config by slug (no migration hook - internal/raw).
|
|
117
117
|
*
|
|
118
118
|
* @param {string} slug
|
|
119
119
|
* @returns {Promise<object>}
|
|
@@ -164,14 +164,14 @@ export async function migrateFromMongo({ getDbImpl } = {}) {
|
|
|
164
164
|
try {
|
|
165
165
|
const entries = await fs.readdir(VIEWS_DIR);
|
|
166
166
|
if (entries.some(e => e.endsWith('.json'))) return;
|
|
167
|
-
} catch { /* dir absent
|
|
167
|
+
} catch { /* dir absent - proceed */ }
|
|
168
168
|
|
|
169
169
|
let db;
|
|
170
170
|
try {
|
|
171
171
|
const getDb = getDbImpl ?? (await import('./connectionManager.js')).getDb;
|
|
172
172
|
db = getDb('default');
|
|
173
173
|
} catch {
|
|
174
|
-
return; // file-mode install
|
|
174
|
+
return; // file-mode install - nothing to migrate
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
let docs;
|
|
@@ -238,7 +238,7 @@ export async function listViews() {
|
|
|
238
238
|
export async function getView(slug) {
|
|
239
239
|
await ensureMigrated();
|
|
240
240
|
|
|
241
|
-
// Strict slug guard
|
|
241
|
+
// Strict slug guard - this is hit with raw URL params (public route) and
|
|
242
242
|
// the slug becomes a filename.
|
|
243
243
|
if (typeof slug !== 'string' || !SLUG_RE.test(slug)) return null;
|
|
244
244
|
|
|
@@ -414,7 +414,7 @@ async function executeMongoView(view, { page, limit, user }) {
|
|
|
414
414
|
|
|
415
415
|
/**
|
|
416
416
|
* Execute a view against any other source via the JS pipeline evaluator.
|
|
417
|
-
* The full collection and full pipeline output are materialised in memory
|
|
417
|
+
* The full collection and full pipeline output are materialised in memory -
|
|
418
418
|
* same trade the FileAdapter already makes for whole collections.
|
|
419
419
|
*/
|
|
420
420
|
async function executeFileView(view, { page, limit, user }) {
|
|
@@ -456,7 +456,7 @@ export async function executeView(slug, {page = 1, limit = 25, user = null} = {}
|
|
|
456
456
|
validateStages(view.pipeline?.stages || []);
|
|
457
457
|
|
|
458
458
|
// Dispatch on the SOURCE collection's storage adapter. A mongodb-declared
|
|
459
|
-
// schema whose connection is unavailable falls back to the JS evaluator
|
|
459
|
+
// schema whose connection is unavailable falls back to the JS evaluator -
|
|
460
460
|
// matching adapterRegistry's own file fallback.
|
|
461
461
|
let useMongo = false;
|
|
462
462
|
try {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
<!-- Site CSS -->
|
|
20
20
|
<link rel="stylesheet" href="/public/css/site.css">
|
|
21
|
-
<link rel="stylesheet" href="/public/css/forms.css?v=
|
|
21
|
+
<link rel="stylesheet" href="/public/css/forms.css?v=20260821-wizard-steps">
|
|
22
22
|
|
|
23
23
|
<!-- Font overrides -->
|
|
24
24
|
{{fontStyleTag}}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<!-- Plugin head injection -->
|
|
27
27
|
{{headInject}}
|
|
28
28
|
|
|
29
|
-
<!-- Late head injection
|
|
29
|
+
<!-- Late head injection - custom CSS always loads last so it can override everything -->
|
|
30
30
|
{{headInjectLate}}
|
|
31
31
|
</head>
|
|
32
32
|
<body class="dm-cloaked dm-theme-{{theme}} {{layoutBodyClass}}" data-layout="{{layout}}">
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
</script>
|
|
117
117
|
|
|
118
118
|
<!-- Site initialisation -->
|
|
119
|
-
<script src="/public/js/site.js?v=
|
|
119
|
+
<script src="/public/js/site.js?v=20260821-wizard-steps" type="module"></script>
|
|
120
120
|
<script src="/public/js/collection-context.js?v=20260819-export" type="module"></script>
|
|
121
121
|
|
|
122
122
|
<!-- Interactive Collection Browser (used by [collection] shortcodes with
|
|
@@ -124,8 +124,8 @@
|
|
|
124
124
|
<script src="/public/js/collection-browser.js?v=20260523-browser"></script>
|
|
125
125
|
|
|
126
126
|
<!-- Core Forms (logic engine must load before renderer) -->
|
|
127
|
-
<script src="/public/js/form-logic-engine.js?v=
|
|
128
|
-
<script src="/public/js/forms.js?v=
|
|
127
|
+
<script src="/public/js/form-logic-engine.js?v=20260821-wizard-steps"></script>
|
|
128
|
+
<script src="/public/js/forms.js?v=20260821-wizard-steps" type="module"></script>
|
|
129
129
|
|
|
130
130
|
<!-- Core effects runtime -->
|
|
131
131
|
<script src="/public/js/effects.js"></script>
|