domma-cms 0.3.0 → 0.5.2

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 (150) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1
  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 -1
  7. package/admin/js/app.js +8 -4
  8. package/admin/js/config/sidebar-config.js +1 -1
  9. package/admin/js/lib/markdown-toolbar.js +18 -10
  10. package/admin/js/templates/action-editor.html +171 -0
  11. package/admin/js/templates/actions-list.html +19 -0
  12. package/admin/js/templates/api-reference.html +1411 -0
  13. package/admin/js/templates/block-editor.html +158 -0
  14. package/admin/js/templates/blocks.html +8 -0
  15. package/admin/js/templates/collection-editor.html +47 -0
  16. package/admin/js/templates/collection-entries.html +3 -0
  17. package/admin/js/templates/collections.html +51 -4
  18. package/admin/js/templates/documentation.html +258 -0
  19. package/{plugins/form-builder/admin → admin/js}/templates/form-editor.html +238 -199
  20. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  21. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  22. package/admin/js/templates/login.html +29 -4
  23. package/admin/js/templates/my-profile.html +17 -0
  24. package/admin/js/templates/page-editor.html +39 -0
  25. package/admin/js/templates/pages.html +6 -1
  26. package/admin/js/templates/pro-docs.html +259 -0
  27. package/admin/js/templates/role-editor.html +59 -0
  28. package/admin/js/templates/roles.html +10 -0
  29. package/admin/js/templates/settings.html +167 -23
  30. package/admin/js/templates/tutorials.html +81 -0
  31. package/admin/js/templates/user-editor.html +7 -0
  32. package/admin/js/templates/users.html +3 -26
  33. package/admin/js/templates/view-editor.html +201 -0
  34. package/admin/js/templates/view-preview.html +51 -0
  35. package/admin/js/templates/views-list.html +19 -0
  36. package/admin/js/views/action-editor.js +1 -0
  37. package/admin/js/views/actions-list.js +1 -0
  38. package/admin/js/views/api-reference.js +1 -0
  39. package/admin/js/views/block-editor.js +8 -0
  40. package/admin/js/views/blocks.js +4 -0
  41. package/admin/js/views/collection-editor.js +3 -3
  42. package/admin/js/views/collection-entries.js +1 -1
  43. package/admin/js/views/collections.js +1 -1
  44. package/admin/js/views/dashboard.js +1 -1
  45. package/admin/js/views/form-editor.js +8 -0
  46. package/admin/js/views/form-submissions.js +1 -0
  47. package/admin/js/views/forms.js +1 -0
  48. package/admin/js/views/index.js +1 -1
  49. package/admin/js/views/login.js +2 -2
  50. package/admin/js/views/media.js +1 -1
  51. package/admin/js/views/my-profile.js +1 -0
  52. package/admin/js/views/page-editor.js +34 -15
  53. package/admin/js/views/pages.js +5 -5
  54. package/admin/js/views/plugins.js +10 -10
  55. package/admin/js/views/pro-docs.js +1 -0
  56. package/admin/js/views/role-editor.js +1 -0
  57. package/admin/js/views/roles.js +4 -0
  58. package/admin/js/views/settings.js +3 -1
  59. package/admin/js/views/user-editor.js +1 -1
  60. package/admin/js/views/users.js +4 -7
  61. package/admin/js/views/view-editor.js +1 -0
  62. package/admin/js/views/view-preview.js +1 -0
  63. package/admin/js/views/views-list.js +1 -0
  64. package/bin/cli.js +1 -1
  65. package/config/auth.json +1 -0
  66. package/config/connections.json.bak +9 -0
  67. package/config/connections.json.example +9 -0
  68. package/config/navigation.json +5 -15
  69. package/config/plugins.json +19 -29
  70. package/config/server.json +6 -6
  71. package/config/site.json +16 -6
  72. package/package.json +25 -10
  73. package/plugins/example-analytics/stats.json +17 -12
  74. package/plugins/form-builder/data/forms/contacts.json +62 -62
  75. package/plugins/form-builder/data/forms/enquiries.json +103 -0
  76. package/plugins/form-builder/data/forms/feedback.json +17 -16
  77. package/plugins/form-builder/data/forms/notes.json +79 -0
  78. package/plugins/form-builder/data/forms/to-do.json +100 -0
  79. package/plugins/form-builder/data/submissions/contacts.json +1 -26
  80. package/plugins/form-builder/data/submissions/notes.json +1 -0
  81. package/plugins/form-builder/data/submissions/to-do.json +1 -0
  82. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  83. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  84. package/plugins/theme-roller/config.js +1 -0
  85. package/plugins/theme-roller/plugin.js +233 -0
  86. package/plugins/theme-roller/plugin.json +31 -0
  87. package/plugins/theme-roller/public/active-theme.css +0 -0
  88. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  89. package/public/css/forms.css +1 -0
  90. package/public/css/site.css +1 -1
  91. package/public/js/forms.js +1 -0
  92. package/public/js/site.js +1 -1
  93. package/scripts/build.js +194 -129
  94. package/scripts/pro.js +254 -0
  95. package/scripts/reset.js +33 -8
  96. package/scripts/seed.js +677 -128
  97. package/scripts/setup.js +1 -0
  98. package/server/middleware/auth.js +136 -120
  99. package/server/routes/api/actions.js +200 -0
  100. package/server/routes/api/auth.js +292 -146
  101. package/server/routes/api/blocks.js +84 -0
  102. package/server/routes/api/collections.js +79 -27
  103. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +491 -505
  104. package/server/routes/api/layouts.js +49 -39
  105. package/server/routes/api/media.js +118 -92
  106. package/server/routes/api/navigation.js +40 -36
  107. package/server/routes/api/pages.js +132 -118
  108. package/server/routes/api/plugins.js +6 -3
  109. package/server/routes/api/settings.js +104 -88
  110. package/server/routes/api/users.js +27 -19
  111. package/server/routes/api/views.js +148 -0
  112. package/server/routes/public.js +124 -108
  113. package/server/server.js +269 -181
  114. package/server/services/actions.js +387 -0
  115. package/server/services/adapterRegistry.js +98 -0
  116. package/server/services/adapters/FileAdapter.js +192 -0
  117. package/server/services/adapters/MongoAdapter.js +220 -0
  118. package/server/services/blocks.js +162 -0
  119. package/server/services/collections.js +74 -86
  120. package/server/services/connectionManager.js +102 -0
  121. package/server/services/content.js +312 -307
  122. package/server/services/email.js +126 -0
  123. package/server/services/forms.js +173 -0
  124. package/server/services/markdown.js +1378 -747
  125. package/server/services/permissionRegistry.js +173 -0
  126. package/server/services/presetCollections.js +251 -0
  127. package/server/services/renderer.js +98 -2
  128. package/server/services/roles.js +227 -0
  129. package/server/services/rowAccess.js +104 -0
  130. package/server/services/userProfiles.js +199 -0
  131. package/server/services/users.js +281 -212
  132. package/server/services/views.js +280 -0
  133. package/server/templates/page.html +124 -113
  134. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  135. package/plugins/form-builder/admin/views/form-editor.js +0 -1444
  136. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  137. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  138. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  139. package/plugins/form-builder/config.js +0 -9
  140. package/plugins/form-builder/data/forms/consent.json +0 -104
  141. package/plugins/form-builder/data/forms/contact-details.json +0 -99
  142. package/plugins/form-builder/data/submissions/consent.json +0 -13
  143. package/plugins/form-builder/plugin.json +0 -52
  144. package/plugins/form-builder/public/inject-body.html +0 -352
  145. package/plugins/form-builder/public/inject-head.html +0 -58
  146. package/plugins/form-builder/public/package.json +0 -1
  147. package/scripts/copy-domma.js +0 -48
  148. package/server/services/userTypes.js +0 -167
  149. /package/plugins/form-builder/data/submissions/{contact-details.json → enquiries.json} +0 -0
  150. /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
@@ -0,0 +1,220 @@
1
+ /**
2
+ * MongoAdapter — MongoDB storage adapter for Collection entries. (Pro feature)
3
+ *
4
+ * Each CMS collection maps to a MongoDB collection prefixed with `cms_`
5
+ * (e.g. slug "contacts" → MongoDB collection "cms_contacts").
6
+ *
7
+ * Entry format is preserved: { id, data, meta } — identical to FileAdapter output.
8
+ * MongoDB's _id field is stripped from all returned documents.
9
+ *
10
+ * Adapter interface:
11
+ * list(slug, opts) → { entries, total, page, limit }
12
+ * get(slug, entryId) → entry | null
13
+ * insert(slug, entry) → entry
14
+ * update(slug, entryId, e) → entry
15
+ * remove(slug, entryId) → void
16
+ * clear(slug) → void
17
+ * all(slug) → entry[]
18
+ * insertMany(slug, entries) → { imported, skipped, errors }
19
+ * count(slug) → number
20
+ */
21
+
22
+ /** Prefix applied to all CMS collection names in MongoDB to avoid collisions. */
23
+ const PREFIX = 'cms_';
24
+
25
+ /**
26
+ * Strip MongoDB's internal _id from a document before returning.
27
+ *
28
+ * @param {object} doc
29
+ * @returns {object}
30
+ */
31
+ function strip(doc) {
32
+ if (!doc) return null;
33
+ const { _id, ...rest } = doc;
34
+ return rest;
35
+ }
36
+
37
+ export class MongoAdapter {
38
+
39
+ /**
40
+ * @param {import('mongodb').Db} db - The Db instance from connectionManager
41
+ */
42
+ constructor(db) {
43
+ this._db = db;
44
+ this._ensured = new Set();
45
+ }
46
+
47
+ /**
48
+ * Get (and cache) the MongoDB collection for a slug, creating the unique
49
+ * index on `id` the first time it is accessed.
50
+ *
51
+ * @param {string} slug
52
+ * @returns {Promise<import('mongodb').Collection>}
53
+ */
54
+ async _col(slug) {
55
+ const col = this._db.collection(`${PREFIX}${slug}`);
56
+ if (!this._ensured.has(slug)) {
57
+ await col.createIndex({ id: 1 }, { unique: true, sparse: false });
58
+ this._ensured.add(slug);
59
+ }
60
+ return col;
61
+ }
62
+
63
+ /**
64
+ * List entries with optional pagination, sorting, and search.
65
+ *
66
+ * When no search term is provided, pagination and sorting are pushed to MongoDB
67
+ * for efficiency. When a search term is provided, all documents are fetched and
68
+ * filtered in memory — identical behaviour to FileAdapter. For large collections
69
+ * requiring efficient full-text search, configure a MongoDB Atlas Search index.
70
+ *
71
+ * @param {string} slug
72
+ * @param {object} [opts]
73
+ * @param {number} [opts.page=1]
74
+ * @param {number} [opts.limit=50]
75
+ * @param {string} [opts.sort='createdAt']
76
+ * @param {string} [opts.order='desc']
77
+ * @param {string} [opts.search]
78
+ * @returns {Promise<{ entries: object[], total: number, page: number, limit: number }>}
79
+ */
80
+ async list(slug, { page = 1, limit = 50, sort = 'createdAt', order = 'desc', search } = {}) {
81
+ const col = await this._col(slug);
82
+
83
+ const sortField = sort === 'createdAt' ? 'meta.createdAt' : `data.${sort}`;
84
+ const sortDir = order === 'desc' ? -1 : 1;
85
+
86
+ if (search) {
87
+ // Fetch all and filter in memory to avoid $where (often disabled in Atlas).
88
+ const all = await col
89
+ .find({}, { projection: { _id: 0 } })
90
+ .sort({ [sortField]: sortDir })
91
+ .toArray();
92
+
93
+ const term = search.toLowerCase();
94
+ const matched = all.filter(entry =>
95
+ Object.values(entry.data || {}).some(v => String(v).toLowerCase().includes(term))
96
+ );
97
+
98
+ const total = matched.length;
99
+ const offset = (page - 1) * limit;
100
+ return { entries: matched.slice(offset, offset + limit), total, page, limit };
101
+ }
102
+
103
+ const total = await col.countDocuments({});
104
+ const offset = (page - 1) * limit;
105
+
106
+ const docs = await col
107
+ .find({}, { projection: { _id: 0 } })
108
+ .sort({ [sortField]: sortDir })
109
+ .skip(offset)
110
+ .limit(limit)
111
+ .toArray();
112
+
113
+ return { entries: docs, total, page, limit };
114
+ }
115
+
116
+ /**
117
+ * Get a single entry by CMS entry ID.
118
+ *
119
+ * @param {string} slug
120
+ * @param {string} entryId
121
+ * @returns {Promise<object|null>}
122
+ */
123
+ async get(slug, entryId) {
124
+ const col = await this._col(slug);
125
+ const doc = await col.findOne({ id: entryId }, { projection: { _id: 0 } });
126
+ return doc || null;
127
+ }
128
+
129
+ /**
130
+ * Insert a pre-built entry (with id and meta already set by collections.js).
131
+ *
132
+ * @param {string} slug
133
+ * @param {object} entry
134
+ * @returns {Promise<object>}
135
+ */
136
+ async insert(slug, entry) {
137
+ const col = await this._col(slug);
138
+ await col.insertOne({ ...entry });
139
+ return entry;
140
+ }
141
+
142
+ /**
143
+ * Replace an existing entry's data and meta.
144
+ *
145
+ * @param {string} slug
146
+ * @param {string} entryId
147
+ * @param {object} updated - Full updated entry object
148
+ * @returns {Promise<object>}
149
+ * @throws {Error} If entry not found
150
+ */
151
+ async update(slug, entryId, updated) {
152
+ const col = await this._col(slug);
153
+ const result = await col.replaceOne({ id: entryId }, { ...updated });
154
+ if (result.matchedCount === 0) throw new Error('Entry not found');
155
+ return updated;
156
+ }
157
+
158
+ /**
159
+ * Delete a single entry by CMS entry ID.
160
+ *
161
+ * @param {string} slug
162
+ * @param {string} entryId
163
+ * @returns {Promise<void>}
164
+ * @throws {Error} If entry not found
165
+ */
166
+ async remove(slug, entryId) {
167
+ const col = await this._col(slug);
168
+ const result = await col.deleteOne({ id: entryId });
169
+ if (result.deletedCount === 0) throw new Error('Entry not found');
170
+ }
171
+
172
+ /**
173
+ * Delete all entries in a collection.
174
+ *
175
+ * @param {string} slug
176
+ * @returns {Promise<void>}
177
+ */
178
+ async clear(slug) {
179
+ const col = await this._col(slug);
180
+ await col.deleteMany({});
181
+ }
182
+
183
+ /**
184
+ * Return all entries (used for export).
185
+ *
186
+ * @param {string} slug
187
+ * @returns {Promise<object[]>}
188
+ */
189
+ async all(slug) {
190
+ const col = await this._col(slug);
191
+ return col.find({}, { projection: { _id: 0 } }).toArray();
192
+ }
193
+
194
+ /**
195
+ * Bulk-insert pre-built entries (already validated by collections.js).
196
+ *
197
+ * @param {string} slug
198
+ * @param {object[]} entries
199
+ * @returns {Promise<{ imported: number, skipped: number, errors: string[] }>}
200
+ */
201
+ async insertMany(slug, entries) {
202
+ if (entries.length === 0) return { imported: 0, skipped: 0, errors: [] };
203
+ const col = await this._col(slug);
204
+ await col.insertMany(entries.map(e => ({ ...e })));
205
+ return { imported: entries.length, skipped: 0, errors: [] };
206
+ }
207
+
208
+ /**
209
+ * Count total entries in a collection.
210
+ *
211
+ * @param {string} slug
212
+ * @returns {Promise<number>}
213
+ */
214
+ async count(slug) {
215
+ const col = await this._col(slug);
216
+ return col.countDocuments({});
217
+ }
218
+ }
219
+
220
+ export default MongoAdapter;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Blocks Service
3
+ * Seeds default block templates for built-in forms.
4
+ * Never overwrites existing files — user customisations are preserved.
5
+ */
6
+ import {access, writeFile} from 'fs/promises';
7
+ import path from 'path';
8
+ import {fileURLToPath} from 'url';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const BLOCKS_DIR = path.resolve(__dirname, '../../content/blocks');
12
+
13
+ /**
14
+ * Write a block file only if it does not already exist.
15
+ *
16
+ * @param {string} name - Block name (without .html extension)
17
+ * @param {string} content - HTML template content
18
+ */
19
+ async function seedBlock(name, content) {
20
+ const filePath = path.join(BLOCKS_DIR, `${name}.html`);
21
+ try {
22
+ await access(filePath);
23
+ // File exists — leave it alone
24
+ } catch {
25
+ await writeFile(filePath, content.trim() + '\n', 'utf8');
26
+ }
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Default block templates
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const BLOCKS = {
34
+
35
+ /**
36
+ * contact-card — for the Contacts form
37
+ * Fields: full_name, phone_number, email_address
38
+ */
39
+ 'contact-card': `
40
+ <div class="card mb-3">
41
+ <div class="card-body">
42
+ <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;flex-wrap:wrap;">
43
+ <div>
44
+ <h4 style="margin:0 0 .5rem;font-size:1.05rem;">{{full_name}}</h4>
45
+ <div style="display:flex;flex-direction:column;gap:.3rem;font-size:.875rem;">
46
+ <span><strong>Email:</strong> {{email_address}}</span>
47
+ <span><strong>Phone:</strong> {{phone_number}}</span>
48
+ </div>
49
+ </div>
50
+ <small class="text-muted" style="white-space:nowrap;">{{_createdAt}}</small>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ `,
55
+
56
+ /**
57
+ * enquiry-card — for the Enquiries form
58
+ * Fields: full_name, email, phone, subject, message
59
+ */
60
+ 'enquiry-card': `
61
+ <div class="card mb-3">
62
+ <div class="card-header" style="display:flex;justify-content:space-between;align-items:center;gap:.75rem;flex-wrap:wrap;">
63
+ <div>
64
+ <strong style="font-size:1rem;">{{subject}}</strong>
65
+ <span class="text-muted" style="font-size:.8rem;margin-left:.6rem;">from {{full_name}}</span>
66
+ </div>
67
+ <small class="text-muted">{{_createdAt}}</small>
68
+ </div>
69
+ <div class="card-body">
70
+ <p style="margin:0 0 1rem;line-height:1.65;">{{message}}</p>
71
+ <div style="display:flex;gap:1.5rem;font-size:.8rem;color:var(--dm-text-muted,#888);flex-wrap:wrap;">
72
+ <span><strong>Email:</strong> {{email}}</span>
73
+ <span><strong>Phone:</strong> {{phone}}</span>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ `,
78
+
79
+ /**
80
+ * feedback-card — for the Feedback form
81
+ * Fields: name, email, rating, category, subject, message, recommend
82
+ */
83
+ 'feedback-card': `
84
+ <div class="card mb-3">
85
+ <div class="card-header" style="display:flex;justify-content:space-between;align-items:center;gap:.75rem;flex-wrap:wrap;">
86
+ <div style="display:flex;align-items:center;gap:.65rem;flex-wrap:wrap;">
87
+ <strong>{{subject}}</strong>
88
+ <span class="badge badge-secondary" style="text-transform:capitalize;">{{category}}</span>
89
+ <span class="badge badge-info" style="text-transform:capitalize;">{{rating}}</span>
90
+ </div>
91
+ <small class="text-muted">{{_createdAt}}</small>
92
+ </div>
93
+ <div class="card-body">
94
+ <p style="margin:0 0 1rem;line-height:1.65;">{{message}}</p>
95
+ <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem;font-size:.8rem;color:var(--dm-text-muted,#888);">
96
+ <span>{{name}} &mdash; {{email}}</span>
97
+ <span>Would recommend: <strong>{{recommend}}</strong></span>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ `,
102
+
103
+ /**
104
+ * note-card — for the Notes form
105
+ * Fields: title, content, category, tags
106
+ */
107
+ 'note-card': `
108
+ <div class="card mb-3">
109
+ <div class="card-header" style="display:flex;justify-content:space-between;align-items:center;gap:.75rem;flex-wrap:wrap;">
110
+ <div style="display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;">
111
+ <strong style="font-size:1rem;">{{title}}</strong>
112
+ <span class="badge badge-secondary" style="text-transform:capitalize;">{{category}}</span>
113
+ </div>
114
+ <small class="text-muted">{{_createdAt}}</small>
115
+ </div>
116
+ <div class="card-body">
117
+ <p style="margin:0 0 .75rem;line-height:1.7;white-space:pre-line;">{{content}}</p>
118
+ <small class="text-muted">Tags: {{tags}}</small>
119
+ </div>
120
+ </div>
121
+ `,
122
+
123
+ /**
124
+ * todo-item — for the To-Do form
125
+ * Fields: title, description, status, priority, due_date, assigned_to
126
+ */
127
+ 'todo-item': `
128
+ <div class="card mb-2">
129
+ <div class="card-body" style="display:flex;align-items:flex-start;gap:1rem;flex-wrap:wrap;">
130
+ <div style="flex:1;min-width:0;">
131
+ <div style="display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;margin-bottom:.4rem;">
132
+ <strong style="font-size:.975rem;">{{title}}</strong>
133
+ <span class="badge badge-warning" style="text-transform:capitalize;">{{priority}}</span>
134
+ <span class="badge badge-success" style="text-transform:capitalize;">{{status}}</span>
135
+ </div>
136
+ <p style="margin:0 0 .5rem;font-size:.875rem;line-height:1.55;color:var(--dm-text-muted,#888);">{{description}}</p>
137
+ <div style="display:flex;gap:1.5rem;font-size:.8rem;color:var(--dm-text-muted,#888);flex-wrap:wrap;">
138
+ <span><strong>Due:</strong> {{due_date}}</span>
139
+ <span><strong>Assigned to:</strong> {{assigned_to}}</span>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ `,
145
+
146
+ };
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Public API
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * Seed all default block templates into content/blocks/.
154
+ * Skips any file that already exists to preserve user customisations.
155
+ */
156
+ export async function seedDefaultBlocks() {
157
+ for (const [name, content] of Object.entries(BLOCKS)) {
158
+ await seedBlock(name, content);
159
+ }
160
+ const names = Object.keys(BLOCKS).join(', ');
161
+ console.log(`[blocks] Seeded default blocks: ${names}`);
162
+ }