domma-cms 0.2.1 → 0.5.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/README.md +3 -3
- package/admin/css/admin.css +1 -1200
- package/admin/dist/domma/domma-tools.css +2313 -0
- package/admin/dist/domma/domma-tools.min.js +10 -0
- package/admin/index.html +4 -0
- package/admin/js/api.js +1 -242
- package/admin/js/app.js +9 -279
- package/admin/js/config/sidebar-config.js +1 -115
- package/admin/js/lib/card.js +1 -63
- package/admin/js/lib/image-editor.js +1 -869
- package/admin/js/lib/markdown-toolbar.js +54 -421
- package/admin/js/templates/action-editor.html +171 -0
- package/admin/js/templates/actions-list.html +19 -0
- package/admin/js/templates/api-reference.html +1411 -0
- package/admin/js/templates/block-editor.html +158 -0
- package/admin/js/templates/blocks.html +8 -0
- package/admin/js/templates/collection-editor.html +47 -0
- package/admin/js/templates/collection-entries.html +3 -0
- package/admin/js/templates/collections.html +51 -4
- package/admin/js/templates/documentation.html +258 -0
- package/admin/js/templates/form-editor.html +238 -0
- package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
- package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
- package/admin/js/templates/layouts.html +44 -7
- package/admin/js/templates/login.html +29 -4
- package/admin/js/templates/my-profile.html +17 -0
- package/admin/js/templates/page-editor.html +48 -0
- package/admin/js/templates/pages.html +6 -1
- package/admin/js/templates/pro-docs.html +259 -0
- package/admin/js/templates/role-editor.html +59 -0
- package/admin/js/templates/roles.html +10 -0
- package/admin/js/templates/settings.html +137 -18
- package/admin/js/templates/tutorials.html +81 -0
- package/admin/js/templates/user-editor.html +7 -0
- package/admin/js/templates/users.html +3 -1
- package/admin/js/templates/view-editor.html +201 -0
- package/admin/js/templates/view-preview.html +51 -0
- package/admin/js/templates/views-list.html +19 -0
- package/admin/js/views/action-editor.js +1 -0
- package/admin/js/views/actions-list.js +1 -0
- package/admin/js/views/api-reference.js +1 -0
- package/admin/js/views/block-editor.js +8 -0
- package/admin/js/views/blocks.js +4 -0
- package/admin/js/views/collection-editor.js +3 -487
- package/admin/js/views/collection-entries.js +1 -484
- package/admin/js/views/collections.js +1 -153
- package/admin/js/views/dashboard.js +1 -56
- package/admin/js/views/documentation.js +1 -12
- package/admin/js/views/form-editor.js +8 -0
- package/admin/js/views/form-submissions.js +1 -0
- package/admin/js/views/forms.js +1 -0
- package/admin/js/views/index.js +1 -39
- package/admin/js/views/layouts.js +9 -42
- package/admin/js/views/login.js +7 -251
- package/admin/js/views/media.js +1 -240
- package/admin/js/views/my-profile.js +1 -0
- package/admin/js/views/navigation.js +14 -212
- package/admin/js/views/page-editor.js +72 -661
- package/admin/js/views/pages.js +5 -72
- package/admin/js/views/plugins.js +13 -90
- package/admin/js/views/pro-docs.js +1 -0
- package/admin/js/views/role-editor.js +1 -0
- package/admin/js/views/roles.js +4 -0
- package/admin/js/views/settings.js +3 -199
- package/admin/js/views/tutorials.js +1 -12
- package/admin/js/views/user-editor.js +1 -88
- package/admin/js/views/users.js +4 -76
- package/admin/js/views/view-editor.js +1 -0
- package/admin/js/views/view-preview.js +1 -0
- package/admin/js/views/views-list.js +1 -0
- package/bin/cli.js +1 -1
- package/config/auth.json +2 -17
- package/config/connections.json.bak +9 -0
- package/config/connections.json.example +9 -0
- package/config/navigation.json +15 -0
- package/config/plugins.json +19 -29
- package/config/server.json +6 -6
- package/config/site.json +17 -6
- package/package.json +24 -10
- package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
- package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
- package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
- package/plugins/domma-effects/public/celebrations/index.js +1 -535
- package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
- package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
- package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
- package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
- package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
- package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
- package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
- package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
- package/plugins/example-analytics/stats.json +21 -12
- package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
- package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
- package/plugins/theme-roller/config.js +1 -0
- package/plugins/theme-roller/plugin.js +233 -0
- package/plugins/theme-roller/plugin.json +31 -0
- package/plugins/theme-roller/public/active-theme.css +0 -0
- package/plugins/theme-roller/public/inject-head-late.html +1 -0
- package/public/css/forms.css +1 -0
- package/public/css/site.css +1 -302
- package/public/js/btt.js +1 -90
- package/public/js/cookie-consent.js +1 -61
- package/public/js/form-logic-engine.js +1 -0
- package/public/js/forms.js +1 -0
- package/public/js/site.js +1 -204
- package/scripts/build.js +194 -129
- package/scripts/pro.js +254 -0
- package/scripts/reset.js +33 -8
- package/scripts/seed.js +343 -78
- package/scripts/setup.js +5 -4
- package/server/middleware/auth.js +136 -97
- package/server/routes/api/actions.js +200 -0
- package/server/routes/api/auth.js +292 -116
- package/server/routes/api/blocks.js +84 -0
- package/server/routes/api/collections.js +88 -23
- package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
- package/server/routes/api/layouts.js +49 -25
- package/server/routes/api/media.js +118 -93
- package/server/routes/api/navigation.js +40 -37
- package/server/routes/api/pages.js +132 -118
- package/server/routes/api/plugins.js +6 -3
- package/server/routes/api/settings.js +104 -89
- package/server/routes/api/users.js +27 -21
- package/server/routes/api/views.js +148 -0
- package/server/routes/public.js +124 -108
- package/server/server.js +269 -173
- package/server/services/actions.js +387 -0
- package/server/services/adapterRegistry.js +98 -0
- package/server/services/adapters/FileAdapter.js +192 -0
- package/server/services/adapters/MongoAdapter.js +220 -0
- package/server/services/blocks.js +162 -0
- package/server/services/collections.js +74 -86
- package/server/services/connectionManager.js +102 -0
- package/server/services/content.js +312 -307
- package/{plugins/form-builder → server/services}/email.js +126 -103
- package/server/services/forms.js +173 -0
- package/server/services/markdown.js +1378 -648
- package/server/services/permissionRegistry.js +173 -0
- package/server/services/presetCollections.js +251 -0
- package/server/services/renderer.js +75 -1
- package/server/services/roles.js +227 -0
- package/server/services/rowAccess.js +104 -0
- package/server/services/userProfiles.js +199 -0
- package/server/services/users.js +281 -212
- package/server/services/views.js +280 -0
- package/server/templates/page.html +119 -113
- package/plugins/form-builder/admin/templates/form-editor.html +0 -171
- package/plugins/form-builder/admin/templates/form-settings.html +0 -29
- package/plugins/form-builder/admin/views/form-editor.js +0 -1442
- package/plugins/form-builder/admin/views/form-settings.js +0 -38
- package/plugins/form-builder/admin/views/form-submissions.js +0 -295
- package/plugins/form-builder/admin/views/forms-list.js +0 -164
- package/plugins/form-builder/config.js +0 -9
- package/plugins/form-builder/data/forms/consent.json +0 -104
- package/plugins/form-builder/data/forms/contact-details.json +0 -63
- package/plugins/form-builder/data/forms/contacts.json +0 -66
- package/plugins/form-builder/data/submissions/consent.json +0 -13
- package/plugins/form-builder/data/submissions/contact-details.json +0 -1
- package/plugins/form-builder/data/submissions/contacts.json +0 -26
- package/plugins/form-builder/plugin.json +0 -52
- package/plugins/form-builder/public/form-logic-engine.js +0 -568
- package/plugins/form-builder/public/inject-body.html +0 -352
- package/plugins/form-builder/public/inject-head.html +0 -58
- package/plugins/form-builder/public/package.json +0 -1
- package/scripts/copy-domma.js +0 -48
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collections Service
|
|
3
3
|
* Schema-first data store — one directory per collection under content/collections/{slug}/.
|
|
4
|
-
* Each collection has a schema.json (field definitions + API access config)
|
|
5
|
-
*
|
|
4
|
+
* Each collection has a schema.json (field definitions + API access config).
|
|
5
|
+
*
|
|
6
|
+
* Entry storage is delegated to the active adapter (FileAdapter or MongoAdapter).
|
|
7
|
+
* Schema management (schema.json) is always file-based.
|
|
6
8
|
*/
|
|
7
9
|
import fs from 'fs/promises';
|
|
8
10
|
import path from 'path';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
+
import {v4 as uuidv4} from 'uuid';
|
|
12
|
+
import {config} from '../config.js';
|
|
13
|
+
import {getAdapter, invalidate} from './adapterRegistry.js';
|
|
11
14
|
|
|
12
15
|
const COLLECTIONS_DIR = path.resolve(config.content.collectionsDir);
|
|
13
16
|
|
|
14
17
|
// ---------------------------------------------------------------------------
|
|
15
|
-
// Internal helpers
|
|
18
|
+
// Internal helpers (schema only)
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
17
20
|
|
|
18
21
|
async function ensureDir() {
|
|
@@ -27,10 +30,6 @@ function schemaPath(slug) {
|
|
|
27
30
|
return path.join(collectionDir(slug), 'schema.json');
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
function dataPath(slug) {
|
|
31
|
-
return path.join(collectionDir(slug), 'data.json');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
33
|
function slugify(str) {
|
|
35
34
|
return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
36
35
|
}
|
|
@@ -44,19 +43,6 @@ async function writeSchema(schema) {
|
|
|
44
43
|
await fs.writeFile(schemaPath(schema.slug), JSON.stringify(schema, null, 2) + '\n', 'utf8');
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
async function readData(slug) {
|
|
48
|
-
try {
|
|
49
|
-
const raw = await fs.readFile(dataPath(slug), 'utf8');
|
|
50
|
-
return JSON.parse(raw);
|
|
51
|
-
} catch {
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function writeData(slug, entries) {
|
|
57
|
-
await fs.writeFile(dataPath(slug), JSON.stringify(entries, null, 2) + '\n', 'utf8');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
46
|
/**
|
|
61
47
|
* Default API access configuration for a new collection.
|
|
62
48
|
*
|
|
@@ -72,7 +58,7 @@ function defaultApiAccess() {
|
|
|
72
58
|
}
|
|
73
59
|
|
|
74
60
|
// ---------------------------------------------------------------------------
|
|
75
|
-
// Schema (collection) operations
|
|
61
|
+
// Schema (collection) operations — always file-based
|
|
76
62
|
// ---------------------------------------------------------------------------
|
|
77
63
|
|
|
78
64
|
/**
|
|
@@ -91,9 +77,10 @@ export async function listCollections() {
|
|
|
91
77
|
}
|
|
92
78
|
|
|
93
79
|
const results = await Promise.allSettled(slugs.map(async (slug) => {
|
|
94
|
-
const schema
|
|
95
|
-
const
|
|
96
|
-
|
|
80
|
+
const schema = await readSchema(slug);
|
|
81
|
+
const adapter = await getAdapter(slug);
|
|
82
|
+
const entryCount = await adapter.count(slug);
|
|
83
|
+
return { ...schema, entryCount };
|
|
97
84
|
}));
|
|
98
85
|
|
|
99
86
|
return results
|
|
@@ -128,7 +115,7 @@ export async function getCollection(slug) {
|
|
|
128
115
|
* @returns {Promise<object>} Created schema
|
|
129
116
|
* @throws {Error} If a collection with that slug already exists
|
|
130
117
|
*/
|
|
131
|
-
export async function createCollection({
|
|
118
|
+
export async function createCollection({title, slug, description = '', fields = [], api = {}, storage}) {
|
|
132
119
|
await ensureDir();
|
|
133
120
|
const finalSlug = slug ? slugify(slug) : slugify(title);
|
|
134
121
|
if (!finalSlug) throw new Error('Could not derive a slug from the title');
|
|
@@ -148,12 +135,13 @@ export async function createCollection({ title, slug, description = '', fields =
|
|
|
148
135
|
description: description.trim(),
|
|
149
136
|
fields,
|
|
150
137
|
api: { ...defaultApiAccess(), ...api },
|
|
138
|
+
storage: storage || {adapter: 'file'},
|
|
151
139
|
createdAt: now,
|
|
152
140
|
updatedAt: now
|
|
153
141
|
};
|
|
154
142
|
|
|
155
143
|
await writeSchema(schema);
|
|
156
|
-
await
|
|
144
|
+
await (await getAdapter(finalSlug)).insertMany(finalSlug, []);
|
|
157
145
|
return schema;
|
|
158
146
|
}
|
|
159
147
|
|
|
@@ -179,6 +167,10 @@ export async function updateCollection(slug, updates) {
|
|
|
179
167
|
};
|
|
180
168
|
|
|
181
169
|
await writeSchema(updated);
|
|
170
|
+
|
|
171
|
+
// Invalidate cached adapter in case storage config changed.
|
|
172
|
+
invalidate(slug);
|
|
173
|
+
|
|
182
174
|
return updated;
|
|
183
175
|
}
|
|
184
176
|
|
|
@@ -192,11 +184,23 @@ export async function updateCollection(slug, updates) {
|
|
|
192
184
|
export async function deleteCollection(slug) {
|
|
193
185
|
const schema = await getCollection(slug);
|
|
194
186
|
if (!schema) throw new Error(`Collection "${slug}" not found`);
|
|
187
|
+
|
|
188
|
+
// Clear adapter data first (handles MongoDB collections on non-file adapters).
|
|
189
|
+
try {
|
|
190
|
+
const adapter = await getAdapter(slug);
|
|
191
|
+
if (adapter.constructor.name !== 'FileAdapter') {
|
|
192
|
+
await adapter.clear(slug);
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// Ignore errors — directory removal handles file-backed data.
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
invalidate(slug);
|
|
195
199
|
await fs.rm(collectionDir(slug), { recursive: true, force: true });
|
|
196
200
|
}
|
|
197
201
|
|
|
198
202
|
// ---------------------------------------------------------------------------
|
|
199
|
-
//
|
|
203
|
+
// Validation
|
|
200
204
|
// ---------------------------------------------------------------------------
|
|
201
205
|
|
|
202
206
|
/**
|
|
@@ -218,6 +222,10 @@ export function validateEntryData(schema, data) {
|
|
|
218
222
|
return { valid: errors.length === 0, errors };
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Entry operations — delegated to storage adapter
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
221
229
|
/**
|
|
222
230
|
* List entries with optional pagination, sorting, and search.
|
|
223
231
|
*
|
|
@@ -230,29 +238,11 @@ export function validateEntryData(schema, data) {
|
|
|
230
238
|
* @param {string} [opts.search]
|
|
231
239
|
* @returns {Promise<{ entries: object[], total: number, page: number, limit: number }>}
|
|
232
240
|
*/
|
|
233
|
-
export async function listEntries(slug,
|
|
241
|
+
export async function listEntries(slug, opts = {}) {
|
|
234
242
|
const schema = await getCollection(slug);
|
|
235
243
|
if (!schema) throw new Error(`Collection "${slug}" not found`);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (search) {
|
|
240
|
-
const term = search.toLowerCase();
|
|
241
|
-
entries = entries.filter(entry => {
|
|
242
|
-
return Object.values(entry.data || {}).some(v => String(v).toLowerCase().includes(term));
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
entries.sort((a, b) => {
|
|
247
|
-
const aVal = sort === 'createdAt' ? a.meta?.createdAt : (a.data?.[sort] ?? '');
|
|
248
|
-
const bVal = sort === 'createdAt' ? b.meta?.createdAt : (b.data?.[sort] ?? '');
|
|
249
|
-
const cmp = String(aVal).localeCompare(String(bVal));
|
|
250
|
-
return order === 'desc' ? -cmp : cmp;
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
const total = entries.length;
|
|
254
|
-
const offset = (page - 1) * limit;
|
|
255
|
-
return { entries: entries.slice(offset, offset + limit), total, page, limit };
|
|
244
|
+
const adapter = await getAdapter(slug);
|
|
245
|
+
return adapter.list(slug, opts);
|
|
256
246
|
}
|
|
257
247
|
|
|
258
248
|
/**
|
|
@@ -263,8 +253,8 @@ export async function listEntries(slug, { page = 1, limit = 50, sort = 'createdA
|
|
|
263
253
|
* @returns {Promise<object|null>}
|
|
264
254
|
*/
|
|
265
255
|
export async function getEntry(slug, entryId) {
|
|
266
|
-
const
|
|
267
|
-
return
|
|
256
|
+
const adapter = await getAdapter(slug);
|
|
257
|
+
return adapter.get(slug, entryId);
|
|
268
258
|
}
|
|
269
259
|
|
|
270
260
|
/**
|
|
@@ -285,17 +275,15 @@ export async function createEntry(slug, data, { createdBy = null, source = 'admi
|
|
|
285
275
|
const { valid, errors } = validateEntryData(schema, data);
|
|
286
276
|
if (!valid) throw new Error(`Validation failed: ${errors.join('; ')}`);
|
|
287
277
|
|
|
288
|
-
const now
|
|
278
|
+
const now = new Date().toISOString();
|
|
289
279
|
const entry = {
|
|
290
|
-
id:
|
|
280
|
+
id: uuidv4(),
|
|
291
281
|
data,
|
|
292
282
|
meta: { createdAt: now, updatedAt: now, createdBy, source }
|
|
293
283
|
};
|
|
294
284
|
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
await writeData(slug, entries);
|
|
298
|
-
return entry;
|
|
285
|
+
const adapter = await getAdapter(slug);
|
|
286
|
+
return adapter.insert(slug, entry);
|
|
299
287
|
}
|
|
300
288
|
|
|
301
289
|
/**
|
|
@@ -311,21 +299,20 @@ export async function updateEntry(slug, entryId, data) {
|
|
|
311
299
|
const schema = await getCollection(slug);
|
|
312
300
|
if (!schema) throw new Error(`Collection "${slug}" not found`);
|
|
313
301
|
|
|
314
|
-
const entries = await readData(slug);
|
|
315
|
-
const idx = entries.findIndex(e => e.id === entryId);
|
|
316
|
-
if (idx === -1) throw new Error('Entry not found');
|
|
317
|
-
|
|
318
302
|
const { valid, errors } = validateEntryData(schema, data);
|
|
319
303
|
if (!valid) throw new Error(`Validation failed: ${errors.join('; ')}`);
|
|
320
304
|
|
|
321
|
-
|
|
322
|
-
|
|
305
|
+
const adapter = await getAdapter(slug);
|
|
306
|
+
const existing = await adapter.get(slug, entryId);
|
|
307
|
+
if (!existing) throw new Error('Entry not found');
|
|
308
|
+
|
|
309
|
+
const updated = {
|
|
310
|
+
...existing,
|
|
323
311
|
data,
|
|
324
|
-
meta: { ...
|
|
312
|
+
meta: { ...existing.meta, updatedAt: new Date().toISOString() }
|
|
325
313
|
};
|
|
326
314
|
|
|
327
|
-
|
|
328
|
-
return entries[idx];
|
|
315
|
+
return adapter.update(slug, entryId, updated);
|
|
329
316
|
}
|
|
330
317
|
|
|
331
318
|
/**
|
|
@@ -337,11 +324,8 @@ export async function updateEntry(slug, entryId, data) {
|
|
|
337
324
|
* @throws {Error} If entry not found
|
|
338
325
|
*/
|
|
339
326
|
export async function deleteEntry(slug, entryId) {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
if (idx === -1) throw new Error('Entry not found');
|
|
343
|
-
entries.splice(idx, 1);
|
|
344
|
-
await writeData(slug, entries);
|
|
327
|
+
const adapter = await getAdapter(slug);
|
|
328
|
+
return adapter.remove(slug, entryId);
|
|
345
329
|
}
|
|
346
330
|
|
|
347
331
|
/**
|
|
@@ -353,7 +337,8 @@ export async function deleteEntry(slug, entryId) {
|
|
|
353
337
|
export async function clearEntries(slug) {
|
|
354
338
|
const schema = await getCollection(slug);
|
|
355
339
|
if (!schema) throw new Error(`Collection "${slug}" not found`);
|
|
356
|
-
await
|
|
340
|
+
const adapter = await getAdapter(slug);
|
|
341
|
+
return adapter.clear(slug);
|
|
357
342
|
}
|
|
358
343
|
|
|
359
344
|
// ---------------------------------------------------------------------------
|
|
@@ -368,9 +353,11 @@ export async function clearEntries(slug) {
|
|
|
368
353
|
* @returns {Promise<string>}
|
|
369
354
|
*/
|
|
370
355
|
export async function exportEntries(slug, format = 'json') {
|
|
371
|
-
const schema
|
|
356
|
+
const schema = await getCollection(slug);
|
|
372
357
|
if (!schema) throw new Error(`Collection "${slug}" not found`);
|
|
373
|
-
|
|
358
|
+
|
|
359
|
+
const adapter = await getAdapter(slug);
|
|
360
|
+
const entries = await adapter.all(slug);
|
|
374
361
|
|
|
375
362
|
if (format === 'csv') {
|
|
376
363
|
const fields = schema.fields.map(f => f.name);
|
|
@@ -399,32 +386,33 @@ export async function exportEntries(slug, format = 'json') {
|
|
|
399
386
|
* @returns {Promise<{ imported: number, skipped: number, errors: string[] }>}
|
|
400
387
|
*/
|
|
401
388
|
export async function importEntries(slug, incoming, { createdBy = null } = {}) {
|
|
402
|
-
const schema
|
|
389
|
+
const schema = await getCollection(slug);
|
|
403
390
|
if (!schema) throw new Error(`Collection "${slug}" not found`);
|
|
404
391
|
|
|
405
|
-
const
|
|
406
|
-
const now = new Date().toISOString();
|
|
407
|
-
|
|
408
|
-
let imported = 0;
|
|
392
|
+
const now = new Date().toISOString();
|
|
409
393
|
let skipped = 0;
|
|
410
394
|
const errors = [];
|
|
395
|
+
const valid = [];
|
|
411
396
|
|
|
412
397
|
for (const item of incoming) {
|
|
413
398
|
const data = item.data || item;
|
|
414
|
-
const { valid, errors: valErrors } = validateEntryData(schema, data);
|
|
415
|
-
if (!
|
|
399
|
+
const { valid: ok, errors: valErrors } = validateEntryData(schema, data);
|
|
400
|
+
if (!ok) {
|
|
416
401
|
skipped++;
|
|
417
402
|
errors.push(valErrors.join('; '));
|
|
418
403
|
continue;
|
|
419
404
|
}
|
|
420
|
-
|
|
405
|
+
valid.push({
|
|
421
406
|
id: uuidv4(),
|
|
422
407
|
data,
|
|
423
408
|
meta: { createdAt: now, updatedAt: now, createdBy, source: 'import' }
|
|
424
409
|
});
|
|
425
|
-
imported++;
|
|
426
410
|
}
|
|
427
411
|
|
|
428
|
-
await
|
|
429
|
-
|
|
412
|
+
const adapter = await getAdapter(slug);
|
|
413
|
+
if (valid.length > 0) {
|
|
414
|
+
await adapter.insertMany(slug, valid);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { imported: valid.length, skipped, errors };
|
|
430
418
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Manager (Pro feature)
|
|
3
|
+
*
|
|
4
|
+
* Manages MongoDB client lifecycle for named database connections.
|
|
5
|
+
* Loaded dynamically via import() — never required in the free version.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* await initialise(connectionsConfig) // on server startup
|
|
9
|
+
* const db = getDb('default') // in adapters
|
|
10
|
+
* await shutdown() // on server close
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** @type {Map<string, { client: import('mongodb').MongoClient, db: import('mongodb').Db }>} */
|
|
14
|
+
const clients = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialise all named MongoDB connections from config.
|
|
18
|
+
* Connects lazily — each client is created but connect() is deferred to first use.
|
|
19
|
+
*
|
|
20
|
+
* @param {Record<string, { type: string, uri: string, database: string, options?: object }>} connectionsConfig
|
|
21
|
+
* @returns {Promise<void>}
|
|
22
|
+
*/
|
|
23
|
+
export async function initialise(connectionsConfig) {
|
|
24
|
+
// Dynamic import — mongodb package is optional and not loaded in the free version.
|
|
25
|
+
const { MongoClient } = await import('mongodb');
|
|
26
|
+
|
|
27
|
+
for (const [name, cfg] of Object.entries(connectionsConfig)) {
|
|
28
|
+
if (cfg.type !== 'mongodb') {
|
|
29
|
+
console.warn(`[connectionManager] Skipping connection "${name}": unsupported type "${cfg.type}"`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!cfg.uri || !cfg.database) {
|
|
34
|
+
console.warn(`[connectionManager] Skipping connection "${name}": missing uri or database`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const client = new MongoClient(cfg.uri, cfg.options || {});
|
|
40
|
+
await client.connect();
|
|
41
|
+
const db = client.db(cfg.database);
|
|
42
|
+
clients.set(name, { client, db });
|
|
43
|
+
console.log(`[connectionManager] Connected: ${name} → ${cfg.database}`);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(`[connectionManager] Failed to connect "${name}": ${err.message}`);
|
|
46
|
+
// Don't throw — allow server to start with degraded MongoDB support.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the MongoClient for a named connection.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} name
|
|
55
|
+
* @returns {import('mongodb').MongoClient}
|
|
56
|
+
* @throws {Error} If the connection is unknown or failed to connect
|
|
57
|
+
*/
|
|
58
|
+
export function getClient(name) {
|
|
59
|
+
const conn = clients.get(name);
|
|
60
|
+
if (!conn) throw new Error(`[connectionManager] Unknown connection: "${name}". Is it configured in config/connections.json?`);
|
|
61
|
+
return conn.client;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the Db instance for a named connection.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} name
|
|
68
|
+
* @returns {import('mongodb').Db}
|
|
69
|
+
* @throws {Error} If the connection is unknown or failed to connect
|
|
70
|
+
*/
|
|
71
|
+
export function getDb(name) {
|
|
72
|
+
const conn = clients.get(name);
|
|
73
|
+
if (!conn) throw new Error(`[connectionManager] Unknown connection: "${name}". Is it configured in config/connections.json?`);
|
|
74
|
+
return conn.db;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check whether a named connection is active.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} name
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
export function isConnected(name) {
|
|
84
|
+
return clients.has(name);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Close all active MongoDB connections gracefully.
|
|
89
|
+
* Called by the Fastify onClose hook.
|
|
90
|
+
*
|
|
91
|
+
* @returns {Promise<void>}
|
|
92
|
+
*/
|
|
93
|
+
export async function shutdown() {
|
|
94
|
+
const closePromises = [];
|
|
95
|
+
for (const [name, { client }] of clients) {
|
|
96
|
+
closePromises.push(
|
|
97
|
+
client.close().then(() => console.log(`[connectionManager] Closed: ${name}`))
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
await Promise.allSettled(closePromises);
|
|
101
|
+
clients.clear();
|
|
102
|
+
}
|