@reldens/cms 0.11.0 → 0.13.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 CHANGED
@@ -19,6 +19,7 @@ A powerful, flexible Content Management System built with Node.js, featuring an
19
19
  - **Template fallback system** (domain → default → base)
20
20
  - **Layout system** with body content layouts and page wrappers
21
21
  - **Reusable content blocks** with `{{ entity() }}` template functions
22
+ - **Collection rendering** with filtering and loop support
22
23
  - **Entity access control** for public/private content
23
24
  - **Static asset serving** with Express integration as default
24
25
  - **Template engine** with Mustache integration as default
@@ -44,7 +45,7 @@ A powerful, flexible Content Management System built with Node.js, featuring an
44
45
 
45
46
  ### - Configuration & Architecture
46
47
  - **Environment-based configuration** (.env file)
47
- - **Modular service architecture** (Frontend, AdminManager, DataServer)
48
+ - **Modular service architecture** (Frontend, AdminManager, DataServer, TemplateEngine)
48
49
  - **Event-driven system** with hooks for customization
49
50
  - **Extensible authentication** (database users or custom callbacks)
50
51
  - **File security** with path validation and dangerous key filtering
@@ -127,15 +128,43 @@ const cms = new Manager({
127
128
  ## Enhanced Templating System
128
129
 
129
130
  ### Template Functions
130
- Templates now support dynamic content blocks and entity rendering:
131
+ Templates support dynamic content blocks, entity rendering, and collections:
132
+
133
+ **Single Entity Rendering:**
131
134
  ```html
132
- <!-- Render content blocks -->
133
- {{ entity('cms_blocks', 'header-main') }}
134
- {{ entity('cms_blocks', 'sidebar-left') }}
135
+ <!-- Render by any identifier field (like 'name' for content blocks) -->
136
+ {{ entity('cmsBlocks', 'header-main', 'name') }}
137
+ {{ entity('cmsBlocks', 'sidebar-left', 'name') }}
135
138
 
136
- <!-- Render other entities -->
139
+ <!-- Render by ID (default identifier) -->
137
140
  {{ entity('products', '123') }}
138
- {{ entity('cms_pages', '1') }}
141
+ {{ entity('cmsPages', '1') }}
142
+ ```
143
+
144
+ **Single Field Collections:**
145
+ ```html
146
+ <!-- Extract and concatenate a single field from multiple records -->
147
+ {{ collection('cmsBlocks', {"status": "active", "category": "navigation"}, 'content') }}
148
+ {{ collection('products', {"featured": true}, 'title') }}
149
+ ```
150
+
151
+ **Loop Collections:**
152
+ ```html
153
+ <!-- Loop through records with full template rendering -->
154
+ {{ #collection('cmsBlocks', {"status": "active"}) }}
155
+ <div class="block">
156
+ <h3>{{row.title}}</h3>
157
+ <div class="content">{{row.content}}</div>
158
+ </div>
159
+ {{ /collection('cmsBlocks') }}
160
+
161
+ {{ #collection('products', {"category": "electronics"}) }}
162
+ <div class="product">
163
+ <h4>{{row.name}}</h4>
164
+ <p>Price: ${{row.price}}</p>
165
+ <img src="{{row.image}}" alt="{{row.name}}">
166
+ </div>
167
+ {{ /collection('products') }}
139
168
  ```
140
169
 
141
170
  ### Layout System
@@ -159,13 +188,13 @@ The CMS uses a two-tier layout system:
159
188
 
160
189
  **layouts/default.html** - Body content only:
161
190
  ```html
162
- {{ entity('cms_blocks', 'header-main') }}
191
+ {{ entity('cmsBlocks', 'header-main', 'name') }}
163
192
 
164
193
  <main id="main" class="main-container">
165
194
  <div class="container">
166
195
  <div class="row">
167
196
  <div class="col-md-3">
168
- {{ entity('cms_blocks', 'sidebar-left') }}
197
+ {{ entity('cmsBlocks', 'sidebar-left', 'name') }}
169
198
  </div>
170
199
  <div class="col-md-9">
171
200
  {{{content}}}
@@ -174,7 +203,7 @@ The CMS uses a two-tier layout system:
174
203
  </div>
175
204
  </main>
176
205
 
177
- {{ entity('cms_blocks', 'footer-main') }}
206
+ {{ entity('cmsBlocks', 'footer-main', 'name') }}
178
207
  ```
179
208
 
180
209
  Pages can use different layouts by setting the `layout` field in `cms_pages`:
@@ -306,7 +335,14 @@ The installer provides checkboxes for:
306
335
  - `handleRequest(req, res)` - Main request handler
307
336
  - `findRouteByPath(path)` - Database route lookup
308
337
  - `findEntityByPath(path)` - Entity-based URL handling
309
- - `processCustomTemplateFunctions(template)` - Process {{ entity() }} functions
338
+
339
+ ### TemplateEngine Class
340
+ - `render(template, data, partials)` - Main template rendering with enhanced functions
341
+ - `processEntityFunctions(template)` - Process {{ entity() }} functions
342
+ - `processSingleFieldCollections(template)` - Process single field collections
343
+ - `processLoopCollections(template)` - Process loop collections
344
+ - `fetchEntityForTemplate(tableName, identifier, identifierField)` - Load single entity
345
+ - `fetchCollectionForTemplate(tableName, filtersJson)` - Load entity collections
310
346
 
311
347
  ### AdminManager Class
312
348
  - `setupAdmin()` - Initialize admin panel
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ *
5
+ * Reldens - CMS - Generate Entities CLI
6
+ *
7
+ */
8
+
9
+ const { Manager } = require('../index');
10
+ const { Logger } = require('@reldens/utils');
11
+ const readline = require('readline');
12
+
13
+ class CmsEntitiesGenerator
14
+ {
15
+
16
+ constructor()
17
+ {
18
+ this.args = process.argv.slice(2);
19
+ this.projectRoot = process.cwd();
20
+ this.isOverride = this.args.includes('--override');
21
+ }
22
+
23
+ async run()
24
+ {
25
+ if(this.isOverride){
26
+ let confirmed = await this.confirmOverride();
27
+ if(!confirmed){
28
+ Logger.info('Operation cancelled by user.');
29
+ return false;
30
+ }
31
+ }
32
+ let manager = new Manager({projectRoot: this.projectRoot});
33
+ if(!manager.isInstalled()){
34
+ Logger.error('CMS is not installed. Please run installation first.');
35
+ return false;
36
+ }
37
+ Logger.debug('Reldens CMS Manager instance created for entities generation.');
38
+ await manager.initializeDataServer();
39
+ let success = await manager.installer.generateEntities(manager.dataServer, this.isOverride);
40
+ if(!success){
41
+ Logger.error('Entities generation failed.');
42
+ return false;
43
+ }
44
+ Logger.info('Entities generation completed successfully!');
45
+ return true;
46
+ }
47
+
48
+ async confirmOverride()
49
+ {
50
+ let rl = readline.createInterface({
51
+ input: process.stdin,
52
+ output: process.stdout
53
+ });
54
+ return new Promise((resolve) => {
55
+ Logger.warning('WARNING: Using --override will regenerate ALL entities and overwrite existing files.');
56
+ rl.question('Are you sure you want to continue? (yes/no): ', (answer) => {
57
+ rl.close();
58
+ resolve('yes' === answer.toLowerCase() || 'y' === answer.toLowerCase());
59
+ });
60
+ });
61
+ }
62
+
63
+ }
64
+
65
+ let generator = new CmsEntitiesGenerator();
66
+ generator.run().then((success) => {
67
+ if(!success){
68
+ process.exit(1);
69
+ }
70
+ process.exit(0);
71
+ }).catch((error) => {
72
+ Logger.critical('Error during entities generation: '+error.message);
73
+ process.exit(1);
74
+ });
@@ -791,6 +791,12 @@ class AdminManager
791
791
  return 'text';
792
792
  }
793
793
  }
794
+ if('textarea' === propertyType){
795
+ if('edit' === templateType){
796
+ return 'textarea';
797
+ }
798
+ return 'text';
799
+ }
794
800
  if(-1 !== ['reference', 'number', 'datetime'].indexOf(propertyType)){
795
801
  propertyType = 'text';
796
802
  }
@@ -1001,10 +1007,7 @@ class AdminManager
1001
1007
  return {label: option[relationTitleProperty]+' (ID: '+value+')', value, selected}
1002
1008
  });
1003
1009
  }
1004
- return await this.render(
1005
- this.adminFilesContents.fields.view[this.propertyType(resourceProperty)],
1006
- {fieldName: propertyKey, fieldValue}
1007
- );
1010
+ return fieldValue;
1008
1011
  }
1009
1012
 
1010
1013
  async fetchRelationOptions(relationDriverResource)
package/lib/frontend.js CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  const { FileHandler } = require('@reldens/server-utils');
8
8
  const { Logger, sc } = require('@reldens/utils');
9
+ const { TemplateEngine } = require('./template-engine');
9
10
 
10
11
  class Frontend
11
12
  {
@@ -27,6 +28,7 @@ class Frontend
27
28
  this.domainPartialsCache = new Map();
28
29
  this.domainTemplatesMap = new Map();
29
30
  this.entityAccessCache = new Map();
31
+ this.templateEngine = false;
30
32
  }
31
33
 
32
34
  async initialize()
@@ -51,6 +53,10 @@ class Frontend
51
53
  Logger.error('Public folder not found: '+this.publicPath);
52
54
  return false;
53
55
  }
56
+ this.templateEngine = new TemplateEngine({
57
+ renderEngine: this.renderEngine,
58
+ dataServer: this.dataServer
59
+ });
54
60
  await this.loadPartials();
55
61
  await this.setupDomainTemplates();
56
62
  await this.loadEntityAccessRules();
@@ -323,42 +329,6 @@ class Frontend
323
329
  return this.findTemplatePath(templatePath, domain);
324
330
  }
325
331
 
326
- async renderEnhanced(template, data, partials)
327
- {
328
- return this.renderEngine.render(
329
- await this.processCustomTemplateFunctions(template),
330
- data,
331
- partials
332
- );
333
- }
334
-
335
- async processCustomTemplateFunctions(template)
336
- {
337
- let entityRegex = /\{\{\s*entity\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*\)\s*\}\}/g;
338
- let processedTemplate = template;
339
- for(let match of template.matchAll(entityRegex)){
340
- let tableName = match[1];
341
- let identifier = match[2];
342
- let entityData = await this.fetchEntityForTemplate(tableName, identifier);
343
- processedTemplate = processedTemplate.replace(match[0], sc.get(entityData, 'content', ''));
344
- }
345
- return processedTemplate;
346
- }
347
-
348
- async fetchEntityForTemplate(tableName, identifier)
349
- {
350
- let entity = this.dataServer.getEntity(tableName);
351
- if(!entity){
352
- Logger.warning('Entity not found in dataServer: '+tableName);
353
- return {};
354
- }
355
- let result = await entity.loadOneBy('name', identifier);
356
- if(!result){
357
- result = await entity.loadById(identifier);
358
- }
359
- return result || {};
360
- }
361
-
362
332
  async renderRoute(route, domain, res)
363
333
  {
364
334
  if(!route.router || !route.content_id){
@@ -409,7 +379,7 @@ class Frontend
409
379
  Logger.error('Failed to read template: ' + templatePath);
410
380
  return false;
411
381
  }
412
- return await this.renderEnhanced(template, data, this.getPartialsForDomain(domain));
382
+ return await this.templateEngine.render(template, data, this.getPartialsForDomain(domain));
413
383
  }
414
384
 
415
385
  async renderWithLayout(content, data, layoutName, domain, res)
@@ -419,7 +389,7 @@ class Frontend
419
389
  if(layoutPath){
420
390
  let layoutTemplate = FileHandler.readFile(layoutPath);
421
391
  if(layoutTemplate){
422
- layoutContent = await this.renderEnhanced(
392
+ layoutContent = await this.templateEngine.render(
423
393
  layoutTemplate,
424
394
  Object.assign({}, data, {
425
395
  content: sc.get(content, 'content', '')
@@ -440,7 +410,7 @@ class Frontend
440
410
  return res.send(layoutContent);
441
411
  }
442
412
  return res.send(
443
- await this.renderEnhanced(
413
+ await this.templateEngine.render(
444
414
  pageTemplate,
445
415
  Object.assign({}, data, {
446
416
  content: layoutContent,
package/lib/installer.js CHANGED
@@ -234,8 +234,7 @@ class Installer
234
234
  Logger.error('SQL file "'+fileName+'" not found.');
235
235
  return '/?error=sql-file-not-found&file-name='+fileName;
236
236
  }
237
- let queryResult = await dbDriver.rawQuery(sqlFileContent.toString());
238
- if(!queryResult){
237
+ if(!await dbDriver.rawQuery(sqlFileContent)){
239
238
  Logger.error('SQL file "'+fileName+'" raw execution failed.');
240
239
  return '/?error=sql-file-execution-error&file-name='+fileName;
241
240
  }
@@ -243,9 +242,9 @@ class Installer
243
242
  return '';
244
243
  }
245
244
 
246
- async generateEntities(server)
245
+ async generateEntities(server, isOverride = false)
247
246
  {
248
- let generator = new EntitiesGenerator({server, projectPath: this.projectRoot});
247
+ let generator = new EntitiesGenerator({server, projectPath: this.projectRoot, isOverride});
249
248
  let success = await generator.generate();
250
249
  if(!success){
251
250
  Logger.error('Entities generation failed.');
package/lib/manager.js CHANGED
@@ -55,7 +55,7 @@ class Manager
55
55
  this.defaultDomain = sc.get(props, 'defaultDomain', (process.env.RELDENS_DEFAULT_DOMAIN || ''));
56
56
  this.domainMapping = sc.get(props, 'domainMapping', sc.toJson(process.env.RELDENS_DOMAIN_MAPPING));
57
57
  this.siteKeyMapping = sc.get(props, 'siteKeyMapping', sc.toJson(process.env.RELDENS_SITE_KEY_MAPPING));
58
- this.templateExtensions = sc.get(props, 'templateExtensions', ['.html', '.mustache', '.template']);
58
+ this.templateExtensions = sc.get(props, 'templateExtensions', ['.html', '.template']);
59
59
  this.app = sc.get(props, 'app', false);
60
60
  this.appServer = sc.get(props, 'appServer', false);
61
61
  this.dataServer = sc.get(props, 'dataServer', false);
@@ -0,0 +1,175 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - TemplateEngine
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+
9
+ class TemplateEngine
10
+ {
11
+
12
+ constructor(props)
13
+ {
14
+ this.renderEngine = sc.get(props, 'renderEngine', false);
15
+ this.dataServer = sc.get(props, 'dataServer', false);
16
+ }
17
+
18
+ async render(template, data, partials)
19
+ {
20
+ if(!this.renderEngine){
21
+ Logger.error('Render engine not provided');
22
+ return '';
23
+ }
24
+ if(!sc.isFunction(this.renderEngine.render)){
25
+ Logger.error('Render engine does not contain a render method');
26
+ return '';
27
+ }
28
+ return this.renderEngine.render(
29
+ await this.processLoopCollections(
30
+ await this.processSingleFieldCollections(
31
+ await this.processEntityFunctions(template)
32
+ )
33
+ ),
34
+ data,
35
+ partials
36
+ );
37
+ }
38
+
39
+ getEntityRegex()
40
+ {
41
+ return /\{\{\s*entity\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"]\s*)?\)\s*\}\}/g;
42
+ }
43
+
44
+ getSingleFieldCollectionRegex()
45
+ {
46
+ return /\{\{\s*collection\(\s*['"]([^'"]+)['"]\s*,\s*(\{[^}]*\})\s*,\s*['"]([^'"]+)['"]\s*\)\s*\}\}/g;
47
+ }
48
+
49
+ getLoopCollectionStartRegex()
50
+ {
51
+ return /\{\{\s*#collection\(\s*['"]([^'"]+)['"]\s*,\s*(\{[^}]*\})\s*\)\s*\}\}/g;
52
+ }
53
+
54
+ getLoopCollectionEndRegex(tableName)
55
+ {
56
+ return new RegExp('\\{\\{\\s*\\/collection\\(\\s*[\'"]'
57
+ +this.escapeRegex(tableName)
58
+ +'[\'"]\\s*\\)\\s*\\}\\}'
59
+ );
60
+ }
61
+
62
+ async processEntityFunctions(template)
63
+ {
64
+ let processedTemplate = template;
65
+ for(let match of template.matchAll(this.getEntityRegex())){
66
+ let tableName = match[1];
67
+ let identifier = match[2];
68
+ let identifierField = match[3] || 'id';
69
+ let entityData = await this.fetchEntityForTemplate(tableName, identifier, identifierField);
70
+ processedTemplate = processedTemplate.replace(match[0], sc.get(entityData, 'content', ''));
71
+ }
72
+ return processedTemplate;
73
+ }
74
+
75
+ async processSingleFieldCollections(template)
76
+ {
77
+ let processedTemplate = template;
78
+ for(let match of template.matchAll(this.getSingleFieldCollectionRegex())){
79
+ let tableName = match[1];
80
+ let filtersJson = match[2];
81
+ let fieldName = match[3];
82
+ processedTemplate = processedTemplate.replace(
83
+ match[0],
84
+ this.extractFieldValues(
85
+ await this.fetchCollectionForTemplate(tableName, filtersJson),
86
+ fieldName
87
+ )
88
+ );
89
+ }
90
+ return processedTemplate;
91
+ }
92
+
93
+ async processLoopCollections(template)
94
+ {
95
+ let processedTemplate = template;
96
+ let matches = [...template.matchAll(this.getLoopCollectionStartRegex())];
97
+ for(let i = matches.length - 1; i >= 0; i--){
98
+ let startMatch = matches[i];
99
+ let tableName = startMatch[1];
100
+ let filtersJson = startMatch[2];
101
+ let loopResult = await this.processLoopCollection(processedTemplate, startMatch, tableName, filtersJson);
102
+ if(false !== loopResult){
103
+ processedTemplate = loopResult;
104
+ }
105
+ }
106
+ return processedTemplate;
107
+ }
108
+
109
+ async processLoopCollection(template, startMatch, tableName, filtersJson)
110
+ {
111
+ let startPos = startMatch.index;
112
+ let startEnd = startPos + startMatch[0].length;
113
+ let endMatch = this.getLoopCollectionEndRegex(tableName).exec(template.substring(startEnd));
114
+ if(!endMatch){
115
+ Logger.warning('No matching end tag found for collection: '+tableName);
116
+ return false;
117
+ }
118
+ let endPos = startEnd + endMatch.index;
119
+ let renderedContent = await this.renderCollectionLoop(
120
+ template.substring(startEnd, endPos),
121
+ await this.fetchCollectionForTemplate(tableName, filtersJson)
122
+ );
123
+ return template.substring(0, startPos) + renderedContent + template.substring(endPos + endMatch[0].length);
124
+ }
125
+
126
+ async renderCollectionLoop(loopContent, collectionData)
127
+ {
128
+ let renderedContent = '';
129
+ for(let row of collectionData){
130
+ renderedContent += this.renderEngine.render(loopContent, {row}, {});
131
+ }
132
+ return renderedContent;
133
+ }
134
+
135
+ extractFieldValues(collectionData, fieldName)
136
+ {
137
+ let fieldValues = '';
138
+ for(let row of collectionData){
139
+ fieldValues += sc.get(row, fieldName, '');
140
+ }
141
+ return fieldValues;
142
+ }
143
+
144
+ escapeRegex(string)
145
+ {
146
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
147
+ }
148
+
149
+ async fetchEntityForTemplate(tableName, identifier, identifierField)
150
+ {
151
+ let entity = this.dataServer.getEntity(tableName);
152
+ if(!entity){
153
+ Logger.warning('Entity not found in dataServer: '+tableName);
154
+ return false;
155
+ }
156
+ return await entity.loadOneBy(identifierField, identifier);
157
+ }
158
+
159
+ async fetchCollectionForTemplate(tableName, filtersJson)
160
+ {
161
+ let entity = this.dataServer.getEntity(tableName);
162
+ if(!entity){
163
+ Logger.warning('Entity not found in dataServer: '+tableName);
164
+ return [];
165
+ }
166
+ let filters = sc.toJson(filtersJson);
167
+ if(!filters){
168
+ return await entity.loadAll();
169
+ }
170
+ return await entity.load(filters);
171
+ }
172
+
173
+ }
174
+
175
+ module.exports.TemplateEngine = TemplateEngine;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/cms",
3
3
  "scope": "@reldens",
4
- "version": "0.11.0",
4
+ "version": "0.13.0",
5
5
  "description": "Reldens - CMS",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -11,7 +11,8 @@
11
11
  "node": ">=20.0.0"
12
12
  },
13
13
  "bin": {
14
- "reldens-cms": "bin/reldens-cms.js"
14
+ "reldens-cms": "bin/reldens-cms.js",
15
+ "reldens-cms-generate-entities": "bin/reldens-cms-generate-entities.js"
15
16
  },
16
17
  "repository": {
17
18
  "type": "git",
@@ -33,7 +34,7 @@
33
34
  },
34
35
  "dependencies": {
35
36
  "@reldens/server-utils": "^0.18.0",
36
- "@reldens/storage": "^0.47.0",
37
+ "@reldens/storage": "^0.49.0",
37
38
  "@reldens/utils": "^0.49.0",
38
39
  "dotenv": "^16.5.0",
39
40
  "mustache": "^4.2.0"
@@ -1,13 +1,10 @@
1
-
2
- {{>header}}
3
-
4
- {{ entity('cmsBlocks', 'header-main') }}
1
+ {{ entity('cmsBlocks', 'header-main', 'name') }}
5
2
 
6
3
  <main id="main" class="main-container">
7
4
  <div class="container">
8
5
  <div class="row">
9
6
  <div class="col-md-3">
10
- {{ entity('cmsBlocks', 'sidebar-left') }}
7
+ {{ entity('cmsBlocks', 'sidebar-left', 'name') }}
11
8
  </div>
12
9
  <div class="col-md-9">
13
10
  {{{content}}}
@@ -16,6 +13,4 @@
16
13
  </div>
17
14
  </main>
18
15
 
19
- {{ entity('cmsBlocks', 'footer-main') }}
20
-
21
- {{>footer}}
16
+ {{ entity('cmsBlocks', 'footer-main', 'name') }}