domma-cms 0.8.10 → 0.9.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/admin/js/templates/action-editor.html +5 -0
- package/admin/js/templates/block-editor.html +5 -0
- package/admin/js/templates/collection-editor.html +7 -0
- package/admin/js/templates/form-editor.html +7 -0
- package/admin/js/templates/page-editor.html +5 -0
- package/admin/js/templates/view-editor.html +5 -0
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/form-editor.js +1 -1
- package/admin/js/views/navigation.js +13 -12
- package/admin/js/views/page-editor.js +11 -11
- package/admin/js/views/pages.js +2 -2
- package/admin/js/views/view-editor.js +1 -1
- package/package.json +1 -1
- package/plugins/contacts/collections/user-contact-groups/schema.json +35 -0
- package/plugins/contacts/collections/user-contacts/schema.json +71 -0
- package/plugins/contacts/plugin.js +1 -55
- package/plugins/garage/collections/garage-vehicles/schema.json +101 -0
- package/plugins/garage/plugin.js +0 -40
- package/plugins/notes/collections/user-notes/schema.json +53 -0
- package/plugins/notes/plugin.js +1 -47
- package/plugins/todo/collections/todos/schema.json +59 -0
- package/plugins/todo/plugin.js +1 -48
- package/server/routes/api/blocks.js +2 -2
- package/server/services/blocks.js +22 -2
- package/server/services/collections.js +17 -3
- package/server/services/forms.js +2 -1
- package/server/services/plugins.js +166 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "todos",
|
|
3
|
+
"title": "Todos",
|
|
4
|
+
"description": "Todo items managed by the Todo plugin.",
|
|
5
|
+
"plugin": "todo",
|
|
6
|
+
"fields": [
|
|
7
|
+
{
|
|
8
|
+
"name": "text",
|
|
9
|
+
"label": "Task",
|
|
10
|
+
"type": "text",
|
|
11
|
+
"required": true
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "status",
|
|
15
|
+
"label": "Status",
|
|
16
|
+
"type": "text",
|
|
17
|
+
"required": false
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "priority",
|
|
21
|
+
"label": "Priority",
|
|
22
|
+
"type": "text",
|
|
23
|
+
"required": false
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "dueAt",
|
|
27
|
+
"label": "Due Date",
|
|
28
|
+
"type": "text",
|
|
29
|
+
"required": false
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "userId",
|
|
33
|
+
"label": "User ID",
|
|
34
|
+
"type": "text",
|
|
35
|
+
"required": false
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"api": {
|
|
39
|
+
"create": {
|
|
40
|
+
"enabled": true,
|
|
41
|
+
"access": "admin"
|
|
42
|
+
},
|
|
43
|
+
"read": {
|
|
44
|
+
"enabled": true,
|
|
45
|
+
"access": "admin"
|
|
46
|
+
},
|
|
47
|
+
"update": {
|
|
48
|
+
"enabled": false,
|
|
49
|
+
"access": "admin"
|
|
50
|
+
},
|
|
51
|
+
"delete": {
|
|
52
|
+
"enabled": false,
|
|
53
|
+
"access": "admin"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"storage": {
|
|
57
|
+
"adapter": "file"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/plugins/todo/plugin.js
CHANGED
|
@@ -4,44 +4,10 @@ import {
|
|
|
4
4
|
createEntry,
|
|
5
5
|
updateEntry,
|
|
6
6
|
deleteEntry,
|
|
7
|
-
getEntry
|
|
8
|
-
getCollection,
|
|
9
|
-
createCollection
|
|
7
|
+
getEntry
|
|
10
8
|
} from '../../server/services/collections.js';
|
|
11
9
|
|
|
12
10
|
const SLUG = 'todos';
|
|
13
|
-
const STORAGE = defaultConfig.storage ?? {adapter: 'file'};
|
|
14
|
-
|
|
15
|
-
const FIELDS = [
|
|
16
|
-
{name: 'text', label: 'Task', type: 'text', required: true},
|
|
17
|
-
{name: 'status', label: 'Status', type: 'text', required: false},
|
|
18
|
-
{name: 'priority', label: 'Priority', type: 'text', required: false},
|
|
19
|
-
{name: 'dueAt', label: 'Due Date', type: 'text', required: false},
|
|
20
|
-
{name: 'userId', label: 'User ID', type: 'text', required: false}
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Lifecycle: create the todos collection (MongoDB-backed) on plugin enable.
|
|
25
|
-
*/
|
|
26
|
-
export async function onEnable({services: {collections}}) {
|
|
27
|
-
const existing = await collections.getCollection(SLUG).catch(() => null);
|
|
28
|
-
if (existing) return;
|
|
29
|
-
await collections.createCollection({
|
|
30
|
-
title: 'Todos',
|
|
31
|
-
slug: SLUG,
|
|
32
|
-
description: 'Todo items managed by the Todo plugin.',
|
|
33
|
-
fields: FIELDS,
|
|
34
|
-
storage: STORAGE
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Lifecycle: remove the todos collection on plugin disable.
|
|
40
|
-
*/
|
|
41
|
-
export async function onDisable({services: {collections}}) {
|
|
42
|
-
await collections.deleteCollection(SLUG).catch(() => {
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
11
|
|
|
46
12
|
/** Flatten a collection entry into the shape the admin view expects. */
|
|
47
13
|
function toTodo(entry) {
|
|
@@ -57,19 +23,6 @@ export default async function todoPlugin(fastify, options) {
|
|
|
57
23
|
const { authenticate } = options.auth;
|
|
58
24
|
const config = {...defaultConfig, ...(options.settings || {})};
|
|
59
25
|
const scope = config.scope ?? 'user';
|
|
60
|
-
const storage = config.storage ?? STORAGE;
|
|
61
|
-
|
|
62
|
-
// Auto-create the collection if it doesn't exist yet.
|
|
63
|
-
const existing = await getCollection(SLUG).catch(() => null);
|
|
64
|
-
if (!existing) {
|
|
65
|
-
await createCollection({
|
|
66
|
-
title: 'Todos',
|
|
67
|
-
slug: SLUG,
|
|
68
|
-
description: 'Todo items managed by the Todo plugin.',
|
|
69
|
-
fields: FIELDS,
|
|
70
|
-
storage
|
|
71
|
-
}).catch(err => fastify.log.warn(`[todo] Collection setup: ${err.message}`));
|
|
72
|
-
}
|
|
73
26
|
|
|
74
27
|
function userId(request) {
|
|
75
28
|
return scope === 'user' ? (request.user?.id ?? request.user?.sub ?? null) : null;
|
|
@@ -34,11 +34,11 @@ export async function blocksRoutes(fastify) {
|
|
|
34
34
|
// Create or update block
|
|
35
35
|
fastify.put('/blocks/:name', canUpdate, async (request, reply) => {
|
|
36
36
|
const {name} = request.params;
|
|
37
|
-
const {content} = request.body || {};
|
|
37
|
+
const {content, bundled} = request.body || {};
|
|
38
38
|
if (typeof content !== 'string') return reply.status(400).send({error: 'content (string) is required'});
|
|
39
39
|
|
|
40
40
|
try {
|
|
41
|
-
return await saveBlock(name, content);
|
|
41
|
+
return await saveBlock(name, content, {bundled: !!bundled});
|
|
42
42
|
} catch (err) {
|
|
43
43
|
if (err.code === 'INVALID_NAME') return reply.status(400).send({error: err.message});
|
|
44
44
|
throw err;
|
|
@@ -180,10 +180,17 @@ export async function listBlocks() {
|
|
|
180
180
|
for (const file of files.filter(f => f.endsWith('.html'))) {
|
|
181
181
|
const name = file.slice(0, -5);
|
|
182
182
|
const fileStat = await fs.stat(path.join(BLOCKS_DIR, file)).catch(() => null);
|
|
183
|
+
let bundled = false;
|
|
184
|
+
try {
|
|
185
|
+
const meta = JSON.parse(await fs.readFile(path.join(BLOCKS_DIR, `${name}.meta.json`), 'utf8'));
|
|
186
|
+
bundled = !!meta.bundled;
|
|
187
|
+
} catch { /* no meta file */
|
|
188
|
+
}
|
|
183
189
|
blocks.push({
|
|
184
190
|
name,
|
|
185
191
|
size: fileStat?.size ?? 0,
|
|
186
192
|
updatedAt: fileStat?.mtime?.toISOString() ?? null,
|
|
193
|
+
bundled,
|
|
187
194
|
});
|
|
188
195
|
}
|
|
189
196
|
return blocks.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -200,7 +207,13 @@ export async function getBlock(name) {
|
|
|
200
207
|
assertValidName(name);
|
|
201
208
|
try {
|
|
202
209
|
const content = await fs.readFile(blockFilePath(name), 'utf8');
|
|
203
|
-
|
|
210
|
+
let bundled = false;
|
|
211
|
+
try {
|
|
212
|
+
const meta = JSON.parse(await fs.readFile(path.join(BLOCKS_DIR, `${name}.meta.json`), 'utf8'));
|
|
213
|
+
bundled = !!meta.bundled;
|
|
214
|
+
} catch { /* no meta file */
|
|
215
|
+
}
|
|
216
|
+
return {name, content, bundled};
|
|
204
217
|
} catch (err) {
|
|
205
218
|
if (err.code === 'ENOENT') {
|
|
206
219
|
const notFound = new Error('Block not found');
|
|
@@ -219,10 +232,17 @@ export async function getBlock(name) {
|
|
|
219
232
|
* @returns {Promise<{success: boolean, name: string}>}
|
|
220
233
|
* @throws {Error} With code INVALID_NAME on bad name
|
|
221
234
|
*/
|
|
222
|
-
export async function saveBlock(name, content) {
|
|
235
|
+
export async function saveBlock(name, content, {bundled} = {}) {
|
|
223
236
|
assertValidName(name);
|
|
224
237
|
await fs.mkdir(BLOCKS_DIR, {recursive: true});
|
|
225
238
|
await fs.writeFile(blockFilePath(name), content, 'utf8');
|
|
239
|
+
const metaPath = path.join(BLOCKS_DIR, `${name}.meta.json`);
|
|
240
|
+
if (bundled) {
|
|
241
|
+
await fs.writeFile(metaPath, JSON.stringify({bundled: true}, null, 2) + '\n', 'utf8');
|
|
242
|
+
} else {
|
|
243
|
+
await fs.unlink(metaPath).catch(() => {
|
|
244
|
+
});
|
|
245
|
+
}
|
|
226
246
|
return {success: true, name};
|
|
227
247
|
}
|
|
228
248
|
|
|
@@ -36,7 +36,13 @@ function slugify(str) {
|
|
|
36
36
|
|
|
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
|
+
if (schema.preset && !schema.bundled) {
|
|
42
|
+
schema.bundled = true;
|
|
43
|
+
delete schema.preset;
|
|
44
|
+
}
|
|
45
|
+
return schema;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
async function writeSchema(schema) {
|
|
@@ -115,7 +121,7 @@ export async function getCollection(slug) {
|
|
|
115
121
|
* @returns {Promise<object>} Created schema
|
|
116
122
|
* @throws {Error} If a collection with that slug already exists
|
|
117
123
|
*/
|
|
118
|
-
export async function createCollection({title, slug, description = '', fields = [], api = {}, storage}) {
|
|
124
|
+
export async function createCollection({title, slug, description = '', fields = [], api = {}, storage, bundled, plugin}) {
|
|
119
125
|
await ensureDir();
|
|
120
126
|
const finalSlug = slug ? slugify(slug) : slugify(title);
|
|
121
127
|
if (!finalSlug) throw new Error('Could not derive a slug from the title');
|
|
@@ -133,6 +139,8 @@ export async function createCollection({title, slug, description = '', fields =
|
|
|
133
139
|
slug: finalSlug,
|
|
134
140
|
title: title.trim(),
|
|
135
141
|
description: description.trim(),
|
|
142
|
+
...(bundled ? {bundled: true} : {}),
|
|
143
|
+
...(plugin ? {plugin} : {}),
|
|
136
144
|
fields,
|
|
137
145
|
api: { ...defaultApiAccess(), ...api },
|
|
138
146
|
storage: storage || {adapter: 'file'},
|
|
@@ -157,14 +165,20 @@ export async function updateCollection(slug, updates) {
|
|
|
157
165
|
const schema = await getCollection(slug);
|
|
158
166
|
if (!schema) throw new Error(`Collection "${slug}" not found`);
|
|
159
167
|
|
|
160
|
-
const { slug: _ignore, createdAt, ...rest } = updates;
|
|
168
|
+
const { slug: _ignore, createdAt, plugin: _plugin, bundled: _bundled, ...rest } = updates;
|
|
161
169
|
const updated = {
|
|
162
170
|
...schema,
|
|
163
171
|
...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} : {}),
|
|
164
176
|
slug,
|
|
165
177
|
createdAt: schema.createdAt,
|
|
166
178
|
updatedAt: new Date().toISOString()
|
|
167
179
|
};
|
|
180
|
+
// Clear bundled from schema if it was unchecked (schema spread may have preserved old value)
|
|
181
|
+
if (!updates.bundled) delete updated.bundled;
|
|
168
182
|
|
|
169
183
|
await writeSchema(updated);
|
|
170
184
|
|
package/server/services/forms.js
CHANGED
|
@@ -115,7 +115,7 @@ export async function getForm(slug) {
|
|
|
115
115
|
* @throws {Error} If title is missing, slug cannot be derived, or a form with the slug already exists.
|
|
116
116
|
*/
|
|
117
117
|
export async function createForm(data) {
|
|
118
|
-
const { title, slug: rawSlug, description = '', fields = [], settings = {}, actions = {} } = data || {};
|
|
118
|
+
const { title, slug: rawSlug, description = '', fields = [], settings = {}, actions = {}, plugin } = data || {};
|
|
119
119
|
|
|
120
120
|
if (!title?.trim() && !rawSlug?.trim()) {
|
|
121
121
|
throw new Error('A title or slug is required to create a form.');
|
|
@@ -144,6 +144,7 @@ export async function createForm(data) {
|
|
|
144
144
|
slug,
|
|
145
145
|
title: trimmedTitle,
|
|
146
146
|
description,
|
|
147
|
+
...(plugin ? {plugin} : {}),
|
|
147
148
|
fields: Array.isArray(fields) ? fields : [],
|
|
148
149
|
settings: {
|
|
149
150
|
submitText: 'Submit',
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import fs from 'fs/promises';
|
|
14
14
|
import path from 'path';
|
|
15
|
+
import matter from 'gray-matter';
|
|
15
16
|
import {getConfig, saveConfig} from '../config.js';
|
|
16
17
|
import {authenticate, requireAdmin, requireRole} from '../middleware/auth.js';
|
|
17
18
|
import {hooks, registerSanitizeRules, registerShortcode, registerTransform} from './hooks.js';
|
|
@@ -231,6 +232,154 @@ export async function getInjectionSnippets() {
|
|
|
231
232
|
return {head, headLate, bodyEnd};
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Recursively collect all .md files under a directory.
|
|
237
|
+
* Returns an empty array if the directory does not exist.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} dir
|
|
240
|
+
* @returns {Promise<string[]>}
|
|
241
|
+
*/
|
|
242
|
+
async function walkMdFiles(dir) {
|
|
243
|
+
const results = [];
|
|
244
|
+
let entries;
|
|
245
|
+
try {
|
|
246
|
+
entries = await fs.readdir(dir, {withFileTypes: true});
|
|
247
|
+
} catch {
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
250
|
+
for (const entry of entries) {
|
|
251
|
+
const full = path.join(dir, entry.name);
|
|
252
|
+
if (entry.isDirectory()) results.push(...await walkMdFiles(full));
|
|
253
|
+
else if (entry.name.endsWith('.md')) results.push(full);
|
|
254
|
+
}
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Set up all resources owned by a plugin: collections, forms, and pages.
|
|
260
|
+
* Each resource type is created only if it does not already exist (idempotent).
|
|
261
|
+
* Called automatically before `onEnable`.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} pluginName
|
|
264
|
+
* @param {object} services - Service map from runLifecycleHook
|
|
265
|
+
* @returns {Promise<{collections: string[], forms: string[], pages: string[]}>}
|
|
266
|
+
*/
|
|
267
|
+
export async function setupPlugin(pluginName, services) {
|
|
268
|
+
const pluginDir = path.join(PLUGINS_DIR, pluginName);
|
|
269
|
+
const result = {collections: [], forms: [], pages: []};
|
|
270
|
+
|
|
271
|
+
// Collections
|
|
272
|
+
if (services.collections) {
|
|
273
|
+
let entries = [];
|
|
274
|
+
try {
|
|
275
|
+
entries = await fs.readdir(path.join(pluginDir, 'collections'), {withFileTypes: true});
|
|
276
|
+
} catch { /* no collections dir */ }
|
|
277
|
+
for (const entry of entries.filter(e => e.isDirectory())) {
|
|
278
|
+
const schemaPath = path.join(pluginDir, 'collections', entry.name, 'schema.json');
|
|
279
|
+
let schema;
|
|
280
|
+
try {
|
|
281
|
+
schema = JSON.parse(await fs.readFile(schemaPath, 'utf8'));
|
|
282
|
+
} catch {
|
|
283
|
+
continue;
|
|
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
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Forms
|
|
295
|
+
if (services.forms) {
|
|
296
|
+
let entries = [];
|
|
297
|
+
try {
|
|
298
|
+
entries = await fs.readdir(path.join(pluginDir, 'forms'));
|
|
299
|
+
} catch { /* no forms dir */ }
|
|
300
|
+
for (const file of entries.filter(e => e.endsWith('.json'))) {
|
|
301
|
+
let data;
|
|
302
|
+
try {
|
|
303
|
+
data = JSON.parse(await fs.readFile(path.join(pluginDir, 'forms', file), 'utf8'));
|
|
304
|
+
} catch {
|
|
305
|
+
continue;
|
|
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;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Pages
|
|
318
|
+
if (services.content) {
|
|
319
|
+
const pagesDir = path.join(pluginDir, 'pages');
|
|
320
|
+
const pageFiles = await walkMdFiles(pagesDir);
|
|
321
|
+
for (const filePath of pageFiles) {
|
|
322
|
+
let raw;
|
|
323
|
+
try {
|
|
324
|
+
raw = await fs.readFile(filePath, 'utf8');
|
|
325
|
+
} catch {
|
|
326
|
+
continue;
|
|
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);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Remove all pages and forms owned by a plugin.
|
|
345
|
+
* Collections are intentionally preserved to protect stored data.
|
|
346
|
+
* Called automatically before `onDisable`.
|
|
347
|
+
*
|
|
348
|
+
* @param {string} pluginName
|
|
349
|
+
* @param {object} services - Service map from runLifecycleHook
|
|
350
|
+
* @param {import('fastify').FastifyInstance} [fastify]
|
|
351
|
+
* @returns {Promise<{forms: string[], pages: string[]}>}
|
|
352
|
+
*/
|
|
353
|
+
export async function teardownPlugin(pluginName, services, fastify) {
|
|
354
|
+
const removed = {forms: [], pages: []};
|
|
355
|
+
|
|
356
|
+
if (services.content) {
|
|
357
|
+
try {
|
|
358
|
+
const pages = await services.content.listPages();
|
|
359
|
+
for (const page of pages.filter(p => p.plugin === pluginName)) {
|
|
360
|
+
await services.content.deletePage(page.urlPath);
|
|
361
|
+
removed.pages.push(page.urlPath);
|
|
362
|
+
}
|
|
363
|
+
} catch (err) {
|
|
364
|
+
fastify?.log.warn(`[plugins] Could not remove pages for "${pluginName}": ${err.message}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (services.forms) {
|
|
369
|
+
try {
|
|
370
|
+
const forms = await services.forms.listForms();
|
|
371
|
+
for (const form of forms.filter(f => f.plugin === pluginName)) {
|
|
372
|
+
await services.forms.deleteForm(form.slug);
|
|
373
|
+
removed.forms.push(form.slug);
|
|
374
|
+
}
|
|
375
|
+
} catch (err) {
|
|
376
|
+
fastify?.log.warn(`[plugins] Could not remove forms for "${pluginName}": ${err.message}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return removed;
|
|
381
|
+
}
|
|
382
|
+
|
|
234
383
|
/**
|
|
235
384
|
* Run a lifecycle hook (onEnable or onDisable) for a plugin if it exports one.
|
|
236
385
|
* Dynamically imports plugin.js and calls the named export with a context object.
|
|
@@ -241,6 +390,7 @@ export async function getInjectionSnippets() {
|
|
|
241
390
|
* @param {import('fastify').FastifyInstance} fastify
|
|
242
391
|
* @returns {Promise<void>}
|
|
243
392
|
*/
|
|
393
|
+
|
|
244
394
|
export async function runLifecycleHook(name, hook, fastify) {
|
|
245
395
|
// Validate hook name
|
|
246
396
|
if (!['onEnable', 'onDisable'].includes(hook)) return;
|
|
@@ -285,7 +435,22 @@ export async function runLifecycleHook(name, hook, fastify) {
|
|
|
285
435
|
}
|
|
286
436
|
});
|
|
287
437
|
|
|
288
|
-
|
|
438
|
+
if (hook === 'onEnable') {
|
|
439
|
+
const setup = await setupPlugin(name, services);
|
|
440
|
+
if (setup.collections.length) fastify.log.info(`[plugins] Created collections for "${name}": ${setup.collections.join(', ')}`);
|
|
441
|
+
if (setup.forms.length) fastify.log.info(`[plugins] Created forms for "${name}": ${setup.forms.join(', ')}`);
|
|
442
|
+
if (setup.pages.length) fastify.log.info(`[plugins] Created pages for "${name}": ${setup.pages.join(', ')}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (hook === 'onDisable') {
|
|
446
|
+
const torn = await teardownPlugin(name, services, fastify);
|
|
447
|
+
if (torn.pages.length) fastify.log.info(`[plugins] Removed pages for "${name}": ${torn.pages.join(', ')}`);
|
|
448
|
+
if (torn.forms.length) fastify.log.info(`[plugins] Removed forms for "${name}": ${torn.forms.join(', ')}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (typeof mod[hook] === 'function') {
|
|
452
|
+
await mod[hook]({ fastify, services });
|
|
453
|
+
}
|
|
289
454
|
} catch (err) {
|
|
290
455
|
fastify.log.error(`Plugin "${name}" lifecycle hook "${hook}" failed: ${err.message}`);
|
|
291
456
|
}
|