domma-cms 0.9.0 → 0.9.5
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/admin/js/templates/block-editor.html +163 -163
- package/admin/js/templates/form-editor.html +245 -245
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/block-editor.js +8 -8
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/navigation.js +14 -14
- package/admin/js/views/page-editor.js +35 -35
- package/admin/js/views/pages.js +5 -5
- package/admin/js/views/plugins.js +19 -10
- package/admin/js/views/view-editor.js +1 -1
- package/config/plugins.json +35 -0
- package/package.json +1 -1
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +4 -4
- package/plugins/docs/data/folders.json +3 -3
- package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +5 -0
- package/plugins/garage/admin/templates/garage.html +30 -0
- package/plugins/garage/admin/views/garage.js +62 -1
- package/plugins/garage/plugin.json +1 -1
- package/plugins/notes/admin/templates/notes.html +2 -11
- package/plugins/notes/admin/views/notes.js +107 -129
- package/plugins/notes/collections/user-notes/schema.json +2 -1
- package/plugins/notes/plugin.json +1 -1
- package/plugins/site-search/admin/templates/site-search.html +174 -46
- package/plugins/site-search/admin/views/site-search.js +72 -1
- package/plugins/site-search/config.js +6 -1
- package/plugins/site-search/plugin.json +1 -1
- package/plugins/site-search/public/inject-head.html +1 -1
- package/plugins/site-search/public/search.css +1 -1
- package/plugins/site-search/public/search.js +1 -1
- package/plugins/todo/admin/templates/todo.html +2 -8
- package/plugins/todo/admin/views/todo.js +122 -106
- package/plugins/todo/collections/todos/schema.json +2 -1
- package/plugins/todo/plugin.json +1 -1
- package/server/routes/api/media.js +127 -118
- package/server/routes/api/plugins.js +15 -4
- package/server/server.js +288 -285
- package/server/services/blocks.js +6 -3
- package/server/services/collections.js +17 -10
- package/server/services/plugins.js +77 -67
- package/server/services/renderer.js +3 -3
- package/plugins/docs/data/documents/452f49b7-9c93-4a67-874d-27f882891ad2.json +0 -11
|
@@ -37,7 +37,6 @@ function slugify(str) {
|
|
|
37
37
|
async function readSchema(slug) {
|
|
38
38
|
const raw = await fs.readFile(schemaPath(slug), 'utf8');
|
|
39
39
|
const schema = JSON.parse(raw);
|
|
40
|
-
// Normalise legacy 'preset' flag — both mean "bundled with fresh installs"
|
|
41
40
|
if (schema.preset && !schema.bundled) {
|
|
42
41
|
schema.bundled = true;
|
|
43
42
|
delete schema.preset;
|
|
@@ -139,11 +138,11 @@ export async function createCollection({title, slug, description = '', fields =
|
|
|
139
138
|
slug: finalSlug,
|
|
140
139
|
title: title.trim(),
|
|
141
140
|
description: description.trim(),
|
|
142
|
-
...(bundled ? {bundled: true} : {}),
|
|
143
|
-
...(plugin ? {plugin} : {}),
|
|
144
141
|
fields,
|
|
145
142
|
api: { ...defaultApiAccess(), ...api },
|
|
146
143
|
storage: storage || {adapter: 'file'},
|
|
144
|
+
...(bundled ? {bundled: true} : {}),
|
|
145
|
+
...(plugin ? {plugin} : {}),
|
|
147
146
|
createdAt: now,
|
|
148
147
|
updatedAt: now
|
|
149
148
|
};
|
|
@@ -165,20 +164,28 @@ export async function updateCollection(slug, updates) {
|
|
|
165
164
|
const schema = await getCollection(slug);
|
|
166
165
|
if (!schema) throw new Error(`Collection "${slug}" not found`);
|
|
167
166
|
|
|
168
|
-
const {
|
|
167
|
+
const {slug: _ignore, createdAt, plugin: _stripPlugin, ...rest} = updates;
|
|
169
168
|
const updated = {
|
|
170
169
|
...schema,
|
|
171
170
|
...rest,
|
|
172
|
-
// bundled is user-editable — set from update, omit if falsy
|
|
173
|
-
...(updates.bundled ? {bundled: true} : {}),
|
|
174
|
-
// plugin is ownership metadata — never overwrite from updates
|
|
175
|
-
...(schema.plugin ? {plugin: schema.plugin} : {}),
|
|
176
171
|
slug,
|
|
177
172
|
createdAt: schema.createdAt,
|
|
178
173
|
updatedAt: new Date().toISOString()
|
|
179
174
|
};
|
|
180
|
-
|
|
181
|
-
|
|
175
|
+
|
|
176
|
+
// Preserve the original plugin field (never overwrite)
|
|
177
|
+
if (schema.plugin) {
|
|
178
|
+
updated.plugin = schema.plugin;
|
|
179
|
+
} else {
|
|
180
|
+
delete updated.plugin;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Allow bundled to be set or cleared
|
|
184
|
+
if (updates.bundled) {
|
|
185
|
+
updated.bundled = true;
|
|
186
|
+
} else {
|
|
187
|
+
delete updated.bundled;
|
|
188
|
+
}
|
|
182
189
|
|
|
183
190
|
await writeSchema(updated);
|
|
184
191
|
|
|
@@ -233,8 +233,7 @@ export async function getInjectionSnippets() {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/**
|
|
236
|
-
* Recursively
|
|
237
|
-
* Returns an empty array if the directory does not exist.
|
|
236
|
+
* Recursively walk a directory and return all .md file paths.
|
|
238
237
|
*
|
|
239
238
|
* @param {string} dir
|
|
240
239
|
* @returns {Promise<string[]>}
|
|
@@ -256,60 +255,65 @@ async function walkMdFiles(dir) {
|
|
|
256
255
|
}
|
|
257
256
|
|
|
258
257
|
/**
|
|
259
|
-
* Set up
|
|
260
|
-
*
|
|
261
|
-
* Called automatically before `onEnable`.
|
|
258
|
+
* Set up plugin-owned resources (collections, forms, pages) when a plugin is enabled.
|
|
259
|
+
* Only creates resources that do not already exist.
|
|
262
260
|
*
|
|
263
261
|
* @param {string} pluginName
|
|
264
|
-
* @param {object} services
|
|
265
|
-
* @returns {Promise<{collections: string[], forms: string[], pages: string[]}>}
|
|
262
|
+
* @param {object} services
|
|
263
|
+
* @returns {Promise<{ collections: string[], forms: string[], pages: string[] }>}
|
|
266
264
|
*/
|
|
267
265
|
export async function setupPlugin(pluginName, services) {
|
|
268
|
-
const pluginDir = path.join(PLUGINS_DIR, pluginName);
|
|
269
266
|
const result = {collections: [], forms: [], pages: []};
|
|
267
|
+
const pluginDir = path.join(PLUGINS_DIR, pluginName);
|
|
270
268
|
|
|
271
269
|
// Collections
|
|
272
270
|
if (services.collections) {
|
|
273
|
-
|
|
271
|
+
const collectionsDir = path.join(pluginDir, 'collections');
|
|
272
|
+
let collectionDirs;
|
|
274
273
|
try {
|
|
275
|
-
|
|
276
|
-
} catch {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
274
|
+
collectionDirs = await fs.readdir(collectionsDir, {withFileTypes: true});
|
|
275
|
+
} catch {
|
|
276
|
+
collectionDirs = [];
|
|
277
|
+
}
|
|
278
|
+
for (const entry of collectionDirs.filter(e => e.isDirectory())) {
|
|
279
|
+
const schemaFile = path.join(collectionsDir, entry.name, 'schema.json');
|
|
280
280
|
try {
|
|
281
|
-
|
|
281
|
+
const raw = await fs.readFile(schemaFile, 'utf8');
|
|
282
|
+
const schema = JSON.parse(raw);
|
|
283
|
+
schema.plugin = pluginName;
|
|
284
|
+
const existing = await services.collections.getCollection(schema.slug).catch(() => null);
|
|
285
|
+
if (!existing) {
|
|
286
|
+
await services.collections.createCollection(schema);
|
|
287
|
+
result.collections.push(schema.slug);
|
|
288
|
+
}
|
|
282
289
|
} catch {
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
schema.plugin = pluginName;
|
|
286
|
-
const existing = await services.collections.getCollection(schema.slug).catch(() => null);
|
|
287
|
-
if (!existing) {
|
|
288
|
-
await services.collections.createCollection(schema);
|
|
289
|
-
result.collections.push(schema.slug);
|
|
290
|
+
// Skip missing or invalid schema files
|
|
290
291
|
}
|
|
291
292
|
}
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
// Forms
|
|
295
296
|
if (services.forms) {
|
|
296
|
-
|
|
297
|
+
const formsDir = path.join(pluginDir, 'forms');
|
|
298
|
+
let formFiles;
|
|
297
299
|
try {
|
|
298
|
-
|
|
299
|
-
} catch {
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
formFiles = await fs.readdir(formsDir);
|
|
301
|
+
} catch {
|
|
302
|
+
formFiles = [];
|
|
303
|
+
}
|
|
304
|
+
for (const file of formFiles.filter(f => f.endsWith('.json'))) {
|
|
302
305
|
try {
|
|
303
|
-
|
|
306
|
+
const raw = await fs.readFile(path.join(formsDir, file), 'utf8');
|
|
307
|
+
const data = JSON.parse(raw);
|
|
308
|
+
data.plugin = pluginName;
|
|
309
|
+
try {
|
|
310
|
+
await services.forms.createForm(data);
|
|
311
|
+
result.forms.push(data.slug || file);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (err.code !== 'FORM_ALREADY_EXISTS') throw err;
|
|
314
|
+
}
|
|
304
315
|
} catch {
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
data.plugin = pluginName;
|
|
308
|
-
try {
|
|
309
|
-
await services.forms.createForm(data);
|
|
310
|
-
result.forms.push(data.slug || data.title);
|
|
311
|
-
} catch (err) {
|
|
312
|
-
if (err.code !== 'FORM_ALREADY_EXISTS') throw err;
|
|
316
|
+
// Skip missing or invalid form files
|
|
313
317
|
}
|
|
314
318
|
}
|
|
315
319
|
}
|
|
@@ -317,22 +321,22 @@ export async function setupPlugin(pluginName, services) {
|
|
|
317
321
|
// Pages
|
|
318
322
|
if (services.content) {
|
|
319
323
|
const pagesDir = path.join(pluginDir, 'pages');
|
|
320
|
-
const
|
|
321
|
-
for (const filePath of
|
|
322
|
-
let raw;
|
|
324
|
+
const mdFiles = await walkMdFiles(pagesDir);
|
|
325
|
+
for (const filePath of mdFiles) {
|
|
323
326
|
try {
|
|
324
|
-
raw = await fs.readFile(filePath, 'utf8');
|
|
327
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
328
|
+
const parsed = matter(raw);
|
|
329
|
+
const rel = path.relative(pagesDir, filePath);
|
|
330
|
+
let urlPath = '/' + rel.replace(/\\/g, '/').replace(/\.md$/, '').replace(/\/index$/, '');
|
|
331
|
+
if (urlPath !== '/' && urlPath.endsWith('/')) urlPath = urlPath.slice(0, -1);
|
|
332
|
+
parsed.data.plugin = pluginName;
|
|
333
|
+
const existing = await services.content.getPage(urlPath).catch(() => null);
|
|
334
|
+
if (!existing) {
|
|
335
|
+
await services.content.createPage(urlPath, parsed.data, parsed.content.trim());
|
|
336
|
+
result.pages.push(urlPath);
|
|
337
|
+
}
|
|
325
338
|
} catch {
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
const parsed = matter(raw);
|
|
329
|
-
const rel = path.relative(pagesDir, filePath).replace(/\\/g, '/');
|
|
330
|
-
const urlPath = '/' + rel.replace(/\.md$/, '').replace(/\/index$/, '').replace(/^index$/, '');
|
|
331
|
-
parsed.data.plugin = pluginName;
|
|
332
|
-
const existing = await services.content.getPage(urlPath);
|
|
333
|
-
if (!existing) {
|
|
334
|
-
await services.content.createPage(urlPath, parsed.data, parsed.content.trim());
|
|
335
|
-
result.pages.push(urlPath);
|
|
339
|
+
// Skip unreadable or invalid page files
|
|
336
340
|
}
|
|
337
341
|
}
|
|
338
342
|
}
|
|
@@ -341,43 +345,52 @@ export async function setupPlugin(pluginName, services) {
|
|
|
341
345
|
}
|
|
342
346
|
|
|
343
347
|
/**
|
|
344
|
-
*
|
|
345
|
-
* Collections are
|
|
346
|
-
* Called automatically before `onDisable`.
|
|
348
|
+
* Tear down plugin-owned pages and forms when a plugin is disabled.
|
|
349
|
+
* Collections are NOT deleted to preserve data.
|
|
347
350
|
*
|
|
348
351
|
* @param {string} pluginName
|
|
349
|
-
* @param {object} services
|
|
350
|
-
* @param {import('fastify').FastifyInstance}
|
|
351
|
-
* @returns {Promise<{
|
|
352
|
+
* @param {object} services
|
|
353
|
+
* @param {import('fastify').FastifyInstance} fastify
|
|
354
|
+
* @returns {Promise<{ pages: string[], forms: string[] }>}
|
|
352
355
|
*/
|
|
353
356
|
export async function teardownPlugin(pluginName, services, fastify) {
|
|
354
|
-
const
|
|
357
|
+
const result = {pages: [], forms: []};
|
|
355
358
|
|
|
359
|
+
// Pages
|
|
356
360
|
if (services.content) {
|
|
357
361
|
try {
|
|
358
362
|
const pages = await services.content.listPages();
|
|
359
363
|
for (const page of pages.filter(p => p.plugin === pluginName)) {
|
|
360
|
-
|
|
361
|
-
|
|
364
|
+
try {
|
|
365
|
+
await services.content.deletePage(page.urlPath);
|
|
366
|
+
result.pages.push(page.urlPath);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
fastify.log.warn(`[plugins] Could not remove page "${page.urlPath}" for "${pluginName}": ${err.message}`);
|
|
369
|
+
}
|
|
362
370
|
}
|
|
363
371
|
} catch (err) {
|
|
364
|
-
fastify
|
|
372
|
+
fastify.log.warn(`[plugins] Could not list pages during teardown of "${pluginName}": ${err.message}`);
|
|
365
373
|
}
|
|
366
374
|
}
|
|
367
375
|
|
|
376
|
+
// Forms
|
|
368
377
|
if (services.forms) {
|
|
369
378
|
try {
|
|
370
379
|
const forms = await services.forms.listForms();
|
|
371
380
|
for (const form of forms.filter(f => f.plugin === pluginName)) {
|
|
372
|
-
|
|
373
|
-
|
|
381
|
+
try {
|
|
382
|
+
await services.forms.deleteForm(form.slug);
|
|
383
|
+
result.forms.push(form.slug);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
fastify.log.warn(`[plugins] Could not remove form "${form.slug}" for "${pluginName}": ${err.message}`);
|
|
386
|
+
}
|
|
374
387
|
}
|
|
375
388
|
} catch (err) {
|
|
376
|
-
fastify
|
|
389
|
+
fastify.log.warn(`[plugins] Could not list forms during teardown of "${pluginName}": ${err.message}`);
|
|
377
390
|
}
|
|
378
391
|
}
|
|
379
392
|
|
|
380
|
-
return
|
|
393
|
+
return result;
|
|
381
394
|
}
|
|
382
395
|
|
|
383
396
|
/**
|
|
@@ -390,7 +403,6 @@ export async function teardownPlugin(pluginName, services, fastify) {
|
|
|
390
403
|
* @param {import('fastify').FastifyInstance} fastify
|
|
391
404
|
* @returns {Promise<void>}
|
|
392
405
|
*/
|
|
393
|
-
|
|
394
406
|
export async function runLifecycleHook(name, hook, fastify) {
|
|
395
407
|
// Validate hook name
|
|
396
408
|
if (!['onEnable', 'onDisable'].includes(hook)) return;
|
|
@@ -398,7 +410,6 @@ export async function runLifecycleHook(name, hook, fastify) {
|
|
|
398
410
|
const pluginJsPath = path.join(PLUGINS_DIR, name, 'plugin.js');
|
|
399
411
|
try {
|
|
400
412
|
const mod = await import(pluginJsPath);
|
|
401
|
-
if (typeof mod[hook] !== 'function') return;
|
|
402
413
|
|
|
403
414
|
// Use Promise.allSettled to prevent total failure if any service import rejects
|
|
404
415
|
// (e.g. MongoDB-dependent modules like actions.js or views.js)
|
|
@@ -441,7 +452,6 @@ export async function runLifecycleHook(name, hook, fastify) {
|
|
|
441
452
|
if (setup.forms.length) fastify.log.info(`[plugins] Created forms for "${name}": ${setup.forms.join(', ')}`);
|
|
442
453
|
if (setup.pages.length) fastify.log.info(`[plugins] Created pages for "${name}": ${setup.pages.join(', ')}`);
|
|
443
454
|
}
|
|
444
|
-
|
|
445
455
|
if (hook === 'onDisable') {
|
|
446
456
|
const torn = await teardownPlugin(name, services, fastify);
|
|
447
457
|
if (torn.pages.length) fastify.log.info(`[plugins] Removed pages for "${name}": ${torn.pages.join(', ')}`);
|
|
@@ -68,9 +68,9 @@ export async function renderPage(page) {
|
|
|
68
68
|
|
|
69
69
|
const preset = presets[page.layout] || presets['default'] || {};
|
|
70
70
|
|
|
71
|
-
const seoTitle = page.seo?.title || `${page.title}${site.seo?.titleSeparator || ' | '}${site.seo?.defaultTitle || site.title}
|
|
72
|
-
const seoDescription = page.seo?.description || site.seo?.defaultDescription || '';
|
|
73
|
-
const ogImage = page.seo?.image || site.seo?.defaultImage || '';
|
|
71
|
+
const seoTitle = escapeHtml(page.seo?.title || `${page.title}${site.seo?.titleSeparator || ' | '}${site.seo?.defaultTitle || site.title}`);
|
|
72
|
+
const seoDescription = escapeHtml(page.seo?.description || site.seo?.defaultDescription || '');
|
|
73
|
+
const ogImage = escapeHtml(page.seo?.image || site.seo?.defaultImage || '');
|
|
74
74
|
|
|
75
75
|
const dconfig = page.dconfig || null;
|
|
76
76
|
// Escape </script> to prevent injection via dconfig values in the inline script block
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "452f49b7-9c93-4a67-874d-27f882891ad2",
|
|
3
|
-
"title": "Untitled",
|
|
4
|
-
"content": "",
|
|
5
|
-
"folderId": null,
|
|
6
|
-
"tags": [],
|
|
7
|
-
"userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
|
|
8
|
-
"wordCount": 0,
|
|
9
|
-
"createdAt": "2026-03-24T16:48:26.074Z",
|
|
10
|
-
"updatedAt": "2026-03-24T16:49:05.067Z"
|
|
11
|
-
}
|