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.
Files changed (166) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1200
  3. package/admin/dist/domma/domma-tools.css +2313 -0
  4. package/admin/dist/domma/domma-tools.min.js +10 -0
  5. package/admin/index.html +4 -0
  6. package/admin/js/api.js +1 -242
  7. package/admin/js/app.js +9 -279
  8. package/admin/js/config/sidebar-config.js +1 -115
  9. package/admin/js/lib/card.js +1 -63
  10. package/admin/js/lib/image-editor.js +1 -869
  11. package/admin/js/lib/markdown-toolbar.js +54 -421
  12. package/admin/js/templates/action-editor.html +171 -0
  13. package/admin/js/templates/actions-list.html +19 -0
  14. package/admin/js/templates/api-reference.html +1411 -0
  15. package/admin/js/templates/block-editor.html +158 -0
  16. package/admin/js/templates/blocks.html +8 -0
  17. package/admin/js/templates/collection-editor.html +47 -0
  18. package/admin/js/templates/collection-entries.html +3 -0
  19. package/admin/js/templates/collections.html +51 -4
  20. package/admin/js/templates/documentation.html +258 -0
  21. package/admin/js/templates/form-editor.html +238 -0
  22. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  23. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  24. package/admin/js/templates/layouts.html +44 -7
  25. package/admin/js/templates/login.html +29 -4
  26. package/admin/js/templates/my-profile.html +17 -0
  27. package/admin/js/templates/page-editor.html +48 -0
  28. package/admin/js/templates/pages.html +6 -1
  29. package/admin/js/templates/pro-docs.html +259 -0
  30. package/admin/js/templates/role-editor.html +59 -0
  31. package/admin/js/templates/roles.html +10 -0
  32. package/admin/js/templates/settings.html +137 -18
  33. package/admin/js/templates/tutorials.html +81 -0
  34. package/admin/js/templates/user-editor.html +7 -0
  35. package/admin/js/templates/users.html +3 -1
  36. package/admin/js/templates/view-editor.html +201 -0
  37. package/admin/js/templates/view-preview.html +51 -0
  38. package/admin/js/templates/views-list.html +19 -0
  39. package/admin/js/views/action-editor.js +1 -0
  40. package/admin/js/views/actions-list.js +1 -0
  41. package/admin/js/views/api-reference.js +1 -0
  42. package/admin/js/views/block-editor.js +8 -0
  43. package/admin/js/views/blocks.js +4 -0
  44. package/admin/js/views/collection-editor.js +3 -487
  45. package/admin/js/views/collection-entries.js +1 -484
  46. package/admin/js/views/collections.js +1 -153
  47. package/admin/js/views/dashboard.js +1 -56
  48. package/admin/js/views/documentation.js +1 -12
  49. package/admin/js/views/form-editor.js +8 -0
  50. package/admin/js/views/form-submissions.js +1 -0
  51. package/admin/js/views/forms.js +1 -0
  52. package/admin/js/views/index.js +1 -39
  53. package/admin/js/views/layouts.js +9 -42
  54. package/admin/js/views/login.js +7 -251
  55. package/admin/js/views/media.js +1 -240
  56. package/admin/js/views/my-profile.js +1 -0
  57. package/admin/js/views/navigation.js +14 -212
  58. package/admin/js/views/page-editor.js +72 -661
  59. package/admin/js/views/pages.js +5 -72
  60. package/admin/js/views/plugins.js +13 -90
  61. package/admin/js/views/pro-docs.js +1 -0
  62. package/admin/js/views/role-editor.js +1 -0
  63. package/admin/js/views/roles.js +4 -0
  64. package/admin/js/views/settings.js +3 -199
  65. package/admin/js/views/tutorials.js +1 -12
  66. package/admin/js/views/user-editor.js +1 -88
  67. package/admin/js/views/users.js +4 -76
  68. package/admin/js/views/view-editor.js +1 -0
  69. package/admin/js/views/view-preview.js +1 -0
  70. package/admin/js/views/views-list.js +1 -0
  71. package/bin/cli.js +1 -1
  72. package/config/auth.json +2 -17
  73. package/config/connections.json.bak +9 -0
  74. package/config/connections.json.example +9 -0
  75. package/config/navigation.json +15 -0
  76. package/config/plugins.json +19 -29
  77. package/config/server.json +6 -6
  78. package/config/site.json +17 -6
  79. package/package.json +24 -10
  80. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  81. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  82. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  83. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  84. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  85. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  86. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  87. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  88. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  89. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  90. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  91. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  92. package/plugins/example-analytics/stats.json +21 -12
  93. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  94. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  95. package/plugins/theme-roller/config.js +1 -0
  96. package/plugins/theme-roller/plugin.js +233 -0
  97. package/plugins/theme-roller/plugin.json +31 -0
  98. package/plugins/theme-roller/public/active-theme.css +0 -0
  99. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  100. package/public/css/forms.css +1 -0
  101. package/public/css/site.css +1 -302
  102. package/public/js/btt.js +1 -90
  103. package/public/js/cookie-consent.js +1 -61
  104. package/public/js/form-logic-engine.js +1 -0
  105. package/public/js/forms.js +1 -0
  106. package/public/js/site.js +1 -204
  107. package/scripts/build.js +194 -129
  108. package/scripts/pro.js +254 -0
  109. package/scripts/reset.js +33 -8
  110. package/scripts/seed.js +343 -78
  111. package/scripts/setup.js +5 -4
  112. package/server/middleware/auth.js +136 -97
  113. package/server/routes/api/actions.js +200 -0
  114. package/server/routes/api/auth.js +292 -116
  115. package/server/routes/api/blocks.js +84 -0
  116. package/server/routes/api/collections.js +88 -23
  117. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
  118. package/server/routes/api/layouts.js +49 -25
  119. package/server/routes/api/media.js +118 -93
  120. package/server/routes/api/navigation.js +40 -37
  121. package/server/routes/api/pages.js +132 -118
  122. package/server/routes/api/plugins.js +6 -3
  123. package/server/routes/api/settings.js +104 -89
  124. package/server/routes/api/users.js +27 -21
  125. package/server/routes/api/views.js +148 -0
  126. package/server/routes/public.js +124 -108
  127. package/server/server.js +269 -173
  128. package/server/services/actions.js +387 -0
  129. package/server/services/adapterRegistry.js +98 -0
  130. package/server/services/adapters/FileAdapter.js +192 -0
  131. package/server/services/adapters/MongoAdapter.js +220 -0
  132. package/server/services/blocks.js +162 -0
  133. package/server/services/collections.js +74 -86
  134. package/server/services/connectionManager.js +102 -0
  135. package/server/services/content.js +312 -307
  136. package/{plugins/form-builder → server/services}/email.js +126 -103
  137. package/server/services/forms.js +173 -0
  138. package/server/services/markdown.js +1378 -648
  139. package/server/services/permissionRegistry.js +173 -0
  140. package/server/services/presetCollections.js +251 -0
  141. package/server/services/renderer.js +75 -1
  142. package/server/services/roles.js +227 -0
  143. package/server/services/rowAccess.js +104 -0
  144. package/server/services/userProfiles.js +199 -0
  145. package/server/services/users.js +281 -212
  146. package/server/services/views.js +280 -0
  147. package/server/templates/page.html +119 -113
  148. package/plugins/form-builder/admin/templates/form-editor.html +0 -171
  149. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  150. package/plugins/form-builder/admin/views/form-editor.js +0 -1442
  151. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  152. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  153. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  154. package/plugins/form-builder/config.js +0 -9
  155. package/plugins/form-builder/data/forms/consent.json +0 -104
  156. package/plugins/form-builder/data/forms/contact-details.json +0 -63
  157. package/plugins/form-builder/data/forms/contacts.json +0 -66
  158. package/plugins/form-builder/data/submissions/consent.json +0 -13
  159. package/plugins/form-builder/data/submissions/contact-details.json +0 -1
  160. package/plugins/form-builder/data/submissions/contacts.json +0 -26
  161. package/plugins/form-builder/plugin.json +0 -52
  162. package/plugins/form-builder/public/form-logic-engine.js +0 -568
  163. package/plugins/form-builder/public/inject-body.html +0 -352
  164. package/plugins/form-builder/public/inject-head.html +0 -58
  165. package/plugins/form-builder/public/package.json +0 -1
  166. 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) and a data.json
5
- * (array of entries with id, data, and meta).
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 { v4 as uuidv4 } from 'uuid';
10
- import { config } from '../config.js';
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 = await readSchema(slug);
95
- const data = await readData(slug);
96
- return { ...schema, entryCount: data.length };
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({ title, slug, description = '', fields = [], api = {} }) {
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 writeData(finalSlug, []);
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
- // Entry operations
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, { page = 1, limit = 50, sort = 'createdAt', order = 'desc', search } = {}) {
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
- let entries = await readData(slug);
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 entries = await readData(slug);
267
- return entries.find(e => e.id === entryId) || null;
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 = new Date().toISOString();
278
+ const now = new Date().toISOString();
289
279
  const entry = {
290
- id: uuidv4(),
280
+ id: uuidv4(),
291
281
  data,
292
282
  meta: { createdAt: now, updatedAt: now, createdBy, source }
293
283
  };
294
284
 
295
- const entries = await readData(slug);
296
- entries.push(entry);
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
- entries[idx] = {
322
- ...entries[idx],
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: { ...entries[idx].meta, updatedAt: new Date().toISOString() }
312
+ meta: { ...existing.meta, updatedAt: new Date().toISOString() }
325
313
  };
326
314
 
327
- await writeData(slug, entries);
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 entries = await readData(slug);
341
- const idx = entries.findIndex(e => e.id === entryId);
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 writeData(slug, []);
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 = await getCollection(slug);
356
+ const schema = await getCollection(slug);
372
357
  if (!schema) throw new Error(`Collection "${slug}" not found`);
373
- const entries = await readData(slug);
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 = await getCollection(slug);
389
+ const schema = await getCollection(slug);
403
390
  if (!schema) throw new Error(`Collection "${slug}" not found`);
404
391
 
405
- const existing = await readData(slug);
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 (!valid) {
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
- existing.push({
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 writeData(slug, existing);
429
- return { imported, skipped, errors };
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
+ }