@reldens/cms 0.8.0 → 0.10.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/lib/frontend.js CHANGED
@@ -6,7 +6,6 @@
6
6
 
7
7
  const { FileHandler } = require('@reldens/server-utils');
8
8
  const { Logger, sc } = require('@reldens/utils');
9
- const mustache = require('mustache');
10
9
 
11
10
  class Frontend
12
11
  {
@@ -16,16 +15,32 @@ class Frontend
16
15
  this.app = sc.get(props, 'app', false);
17
16
  this.appServerFactory = sc.get(props, 'appServerFactory', false);
18
17
  this.dataServer = sc.get(props, 'dataServer', false);
18
+ this.renderEngine = sc.get(props, 'renderEngine', false);
19
19
  this.projectRoot = sc.get(props, 'projectRoot', './');
20
20
  this.templatesPath = FileHandler.joinPaths(this.projectRoot, 'templates');
21
21
  this.publicPath = FileHandler.joinPaths(this.projectRoot, 'public');
22
- this.error = false;
22
+ this.templateExtensions = sc.get(props, 'templateExtensions', ['.html', '.mustache', '.template']);
23
+ this.defaultDomain = sc.get(props, 'defaultDomain', 'default');
24
+ this.domainMapping = sc.get(props, 'domainMapping', {});
25
+ this.siteKeyMapping = sc.get(props, 'siteKeyMapping', {});
26
+ this.partialsCache = {};
27
+ this.domainPartialsCache = new Map();
28
+ this.domainTemplatesMap = new Map();
29
+ this.entityAccessCache = new Map();
23
30
  }
24
31
 
25
32
  async initialize()
26
33
  {
27
34
  if(!this.app || !this.dataServer){
28
- this.error = 'Missing app or dataServer';
35
+ Logger.error('Missing app or dataServer');
36
+ return false;
37
+ }
38
+ if(!this.renderEngine){
39
+ Logger.error('Please, provide a renderEngine, it must contain a "render" method.');
40
+ return false;
41
+ }
42
+ if(!sc.isFunction(this.renderEngine.render)){
43
+ Logger.error('The provided renderEngine does not contain a "render" method.');
29
44
  return false;
30
45
  }
31
46
  if(!FileHandler.exists(this.templatesPath)){
@@ -36,6 +51,9 @@ class Frontend
36
51
  Logger.error('Public folder not found: '+this.publicPath);
37
52
  return false;
38
53
  }
54
+ await this.loadPartials();
55
+ await this.setupDomainTemplates();
56
+ await this.loadEntityAccessRules();
39
57
  this.setupStaticAssets();
40
58
  this.app.get('*', async (req, res) => {
41
59
  return await this.handleRequest(req, res);
@@ -43,6 +61,172 @@ class Frontend
43
61
  return true;
44
62
  }
45
63
 
64
+ async loadEntityAccessRules()
65
+ {
66
+ let accessEntity = this.dataServer.getEntity('cmsEntityAccess');
67
+ if(!accessEntity){
68
+ Logger.warning('CMS Entity Access not found.');
69
+ return;
70
+ }
71
+ let accessRules = await accessEntity.loadAll();
72
+ for(let rule of accessRules){
73
+ this.entityAccessCache.set(rule.entity_name, rule.is_public);
74
+ }
75
+ }
76
+
77
+ async loadPartials()
78
+ {
79
+ let partialsPath = FileHandler.joinPaths(this.templatesPath, 'partials');
80
+ FileHandler.createFolder(partialsPath);
81
+ let partialFiles = FileHandler.getFilesInFolder(partialsPath, this.templateExtensions);
82
+ for(let file of partialFiles){
83
+ let partialName = this.extractTemplateName(file);
84
+ if(!partialName){
85
+ continue;
86
+ }
87
+ let partialPath = FileHandler.joinPaths(partialsPath, file);
88
+ let partialContent = FileHandler.readFile(partialPath);
89
+ if(!partialContent){
90
+ Logger.error('Failed to read partial: '+partialPath);
91
+ continue;
92
+ }
93
+ this.partialsCache[partialName] = partialContent;
94
+ }
95
+ }
96
+
97
+ async loadDomainPartials(domain, domainPath)
98
+ {
99
+ let domainPartialsPath = FileHandler.joinPaths(domainPath, 'partials');
100
+ if(!FileHandler.exists(domainPartialsPath)){
101
+ return;
102
+ }
103
+ let domainPartials = {};
104
+ let partialFiles = FileHandler.getFilesInFolder(domainPartialsPath, this.templateExtensions);
105
+ for(let file of partialFiles){
106
+ let partialName = this.extractTemplateName(file);
107
+ if(!partialName){
108
+ continue;
109
+ }
110
+ let partialPath = FileHandler.joinPaths(domainPartialsPath, file);
111
+ let partialContent = FileHandler.readFile(partialPath);
112
+ if(!partialContent){
113
+ Logger.error('Failed to read domain partial: '+partialPath);
114
+ continue;
115
+ }
116
+ domainPartials[partialName] = partialContent;
117
+ }
118
+ this.domainPartialsCache.set(domain, domainPartials);
119
+ }
120
+
121
+ async setupDomainTemplates()
122
+ {
123
+ let domainsPath = FileHandler.joinPaths(this.templatesPath, 'domains');
124
+ if(!FileHandler.exists(domainsPath)){
125
+ return;
126
+ }
127
+ let domainFolders = FileHandler.fetchSubFoldersList(domainsPath);
128
+ for(let domain of domainFolders){
129
+ let domainPath = FileHandler.joinPaths(domainsPath, domain);
130
+ this.domainTemplatesMap.set(domain, domainPath);
131
+ await this.loadDomainPartials(domain, domainPath);
132
+ }
133
+ }
134
+
135
+ extractTemplateName(filename)
136
+ {
137
+ for(let extension of this.templateExtensions){
138
+ if(filename.endsWith(extension)){
139
+ return filename.replace(extension, '');
140
+ }
141
+ }
142
+ return false;
143
+ }
144
+
145
+ getDomainFromRequest(req)
146
+ {
147
+ let host = req.get('host');
148
+ if(!host){
149
+ return false;
150
+ }
151
+ return host.split(':')[0];
152
+ }
153
+
154
+ resolveDomainToFolder(domain)
155
+ {
156
+ if(!domain){
157
+ domain = this.defaultDomain;
158
+ }
159
+ return sc.get(this.domainMapping, domain, domain);
160
+ }
161
+
162
+ resolveDomainToSiteKey(domain)
163
+ {
164
+ return sc.get(this.siteKeyMapping, this.resolveDomainToFolder(domain), 'default');
165
+ }
166
+
167
+ getPartialsForDomain(domain)
168
+ {
169
+ let resolvedDomain = this.resolveDomainToFolder(domain);
170
+ let domainPartials = this.domainPartialsCache.get(resolvedDomain);
171
+ if(!domainPartials && this.defaultDomain && resolvedDomain !== this.defaultDomain){
172
+ domainPartials = this.domainPartialsCache.get(this.defaultDomain);
173
+ }
174
+ if(!domainPartials){
175
+ return this.partialsCache;
176
+ }
177
+ return Object.assign({}, this.partialsCache, domainPartials);
178
+ }
179
+
180
+ findTemplatePath(templateName, domain)
181
+ {
182
+ let resolvedDomain = this.resolveDomainToFolder(domain);
183
+ if(resolvedDomain){
184
+ let domainPath = this.domainTemplatesMap.get(resolvedDomain);
185
+ if(domainPath){
186
+ let domainTemplatePath = this.findTemplateInPath(templateName, domainPath);
187
+ if(domainTemplatePath){
188
+ return domainTemplatePath;
189
+ }
190
+ }
191
+ if(this.defaultDomain && resolvedDomain !== this.defaultDomain){
192
+ let defaultDomainPath = this.domainTemplatesMap.get(this.defaultDomain);
193
+ if(defaultDomainPath){
194
+ let defaultTemplatePath = this.findTemplateInPath(templateName, defaultDomainPath);
195
+ if(defaultTemplatePath){
196
+ return defaultTemplatePath;
197
+ }
198
+ }
199
+ }
200
+ }
201
+ return this.findTemplateInPath(templateName, this.templatesPath);
202
+ }
203
+
204
+ findTemplateInPath(templateName, basePath)
205
+ {
206
+ for(let extension of this.templateExtensions){
207
+ let templatePath = FileHandler.joinPaths(basePath, templateName + extension);
208
+ if(FileHandler.exists(templatePath)){
209
+ return templatePath;
210
+ }
211
+ }
212
+ return false;
213
+ }
214
+
215
+ findLayoutPath(layoutName, domain)
216
+ {
217
+ let resolvedDomain = this.resolveDomainToFolder(domain);
218
+ if(resolvedDomain){
219
+ let domainPath = this.domainTemplatesMap.get(resolvedDomain);
220
+ if(domainPath){
221
+ let domainLayoutPath = this.findTemplateInPath('layouts/' + layoutName, domainPath);
222
+ if(domainLayoutPath){
223
+ return domainLayoutPath;
224
+ }
225
+ }
226
+ }
227
+ return this.findTemplateInPath('layouts/' + layoutName, this.templatesPath);
228
+ }
229
+
46
230
  setupStaticAssets()
47
231
  {
48
232
  if(!this.app || !this.appServerFactory || !this.publicPath){
@@ -57,35 +241,22 @@ class Frontend
57
241
 
58
242
  async handleRequest(req, res)
59
243
  {
60
- let path = req.path;
61
- if('/favicon.ico' === path){
62
- return res.status(404).send('');
63
- }
64
244
  try {
245
+ let path = req.path;
246
+ let domain = this.getDomainFromRequest(req);
65
247
  let route = await this.findRouteByPath(path);
66
248
  if(route){
67
- Logger.debug('Found route for path: '+path, route);
68
- return await this.renderContentFromRoute(res, route);
249
+ return await this.renderRoute(route, domain, res);
69
250
  }
70
- let pathSegments = path.split('/').filter(segment => segment !== '');
71
- if(0 < pathSegments.length){
72
- let entityResult = await this.findEntityByPath(pathSegments);
73
- if(entityResult){
74
- Logger.debug('Found entity for path segments: '+pathSegments.join('/'));
75
- return await this.renderContentFromEntity(
76
- res,
77
- entityResult.entity,
78
- entityResult.entityName
79
- );
80
- }
251
+ let entityResult = await this.findEntityByPath(path);
252
+ if(entityResult){
253
+ return await this.renderEntity(entityResult, domain, res);
81
254
  }
82
- let templatePath = this.findTemplateByPath(path);
255
+ let templatePath = this.findTemplateByPath(path, domain);
83
256
  if(templatePath){
84
- Logger.debug('Found template for path: '+path+' at: '+templatePath);
85
- return await this.renderTemplateOnly(res, templatePath);
257
+ return await this.renderTemplate(templatePath, domain, res);
86
258
  }
87
- Logger.debug('No template found for path: '+path+', rendering 404');
88
- return await this.renderNotFoundPage(res);
259
+ return await this.renderNotFound(domain, res);
89
260
  } catch (error) {
90
261
  Logger.error('Request handling error: '+error.message);
91
262
  return res.status(500).send('Internal server error');
@@ -109,130 +280,190 @@ class Frontend
109
280
  return false;
110
281
  }
111
282
 
112
- async findEntityByPath(pathSegments)
283
+ async isEntityAccessible(entityName)
284
+ {
285
+ if(this.entityAccessCache.has(entityName)){
286
+ return this.entityAccessCache.get(entityName);
287
+ }
288
+ return false;
289
+ }
290
+
291
+ async findEntityByPath(path)
113
292
  {
114
- if(1 > pathSegments.length){
115
- Logger.debug('No path segments provided');
293
+ let pathSegments = path.split('/').filter(segment => '' !== segment);
294
+ if(2 > pathSegments.length){
116
295
  return false;
117
296
  }
118
297
  let entityName = pathSegments[0];
119
- let entityId = 2 > pathSegments.length ? false : pathSegments[1];
120
- if(!entityId){
121
- Logger.debug('No entity ID in path segments');
298
+ if(!await this.isEntityAccessible(entityName)){
122
299
  return false;
123
300
  }
301
+ let entityId = pathSegments[1];
124
302
  let entity = this.dataServer.getEntity(entityName);
125
303
  if(!entity){
126
- Logger.debug('Entity not found: '+entityName);
127
304
  return false;
128
305
  }
129
306
  let loadedEntity = await entity.loadById(entityId);
130
307
  if(!loadedEntity){
131
- Logger.debug('Entity not loaded by ID: '+entityId);
132
308
  return false;
133
309
  }
134
- return {
135
- entity: loadedEntity,
136
- entityName
137
- };
310
+ return {entity: loadedEntity, entityName};
138
311
  }
139
312
 
140
- findTemplateByPath(path)
313
+ findTemplateByPath(path, domain)
141
314
  {
142
315
  if('/' === path){
143
316
  path = '/index';
144
317
  }
145
- let templatePath = path.endsWith('/')
146
- ? path.slice(0, -1)
147
- : path;
148
- templatePath = templatePath.startsWith('/')
149
- ? templatePath.substring(1)
150
- : templatePath;
151
- let fullPath = FileHandler.joinPaths(this.templatesPath, templatePath + '.html');
152
- if(FileHandler.exists(fullPath)){
153
- return fullPath;
318
+ let templatePath = path.endsWith('/') ? path.slice(0, -1) : path;
319
+ templatePath = templatePath.startsWith('/') ? templatePath.substring(1) : templatePath;
320
+ if('page' === templatePath){
321
+ return false;
154
322
  }
155
- return false;
323
+ return this.findTemplatePath(templatePath, domain);
156
324
  }
157
325
 
158
- async renderContentFromRoute(res, route)
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
+ async renderRoute(route, domain, res)
159
363
  {
160
364
  if(!route.router || !route.content_id){
161
- Logger.debug('Route missing router or content_id');
162
- return await this.renderNotFoundPage(res);
365
+ return await this.renderNotFound(domain, res);
163
366
  }
164
367
  let entity = this.dataServer.getEntity(route.router);
165
368
  if(!entity){
166
- Logger.debug('Entity not found: '+route.router);
167
- return await this.renderNotFoundPage(res);
369
+ return await this.renderNotFound(domain, res);
168
370
  }
169
371
  let content = await entity.loadById(route.content_id);
170
372
  if(!content){
171
- Logger.debug('Content not found for ID: '+route.content_id+' in entity: '+route.router);
172
- return await this.renderNotFoundPage(res);
173
- }
174
- let templateName = content.template || route.router;
175
- let templatePath = FileHandler.joinPaths(this.templatesPath, templateName + '.html');
176
- if(!FileHandler.exists(templatePath)){
177
- templatePath = FileHandler.joinPaths(this.templatesPath, 'page.html');
178
- if(!FileHandler.exists(templatePath)){
179
- Logger.debug('Neither template found: '+templateName+'.html nor page.html');
180
- return await this.renderNotFoundPage(res);
181
- }
373
+ return await this.renderNotFound(domain, res);
182
374
  }
183
- let template = FileHandler.readFile(templatePath).toString();
184
- let data = {
185
- ...route,
186
- ...content,
187
- current_year: new Date().getFullYear()
188
- };
189
- let rendered = mustache.render(template, data);
190
- return res.send(rendered);
375
+ return await this.renderWithLayout(
376
+ content,
377
+ Object.assign({}, route, content, {currentYear: new Date().getFullYear()}),
378
+ sc.get(content, 'layout', 'default'),
379
+ domain,
380
+ res
381
+ );
191
382
  }
192
383
 
193
- async renderContentFromEntity(res, entity, entityName)
384
+ async renderEntity(entityResult, domain, res)
194
385
  {
195
- let templatePath = FileHandler.joinPaths(this.templatesPath, entityName + '.html');
196
- if(!FileHandler.exists(templatePath)){
197
- templatePath = FileHandler.joinPaths(this.templatesPath, 'page.html');
198
- if(!FileHandler.exists(templatePath)){
199
- return await this.renderNotFoundPage(res);
200
- }
386
+ return await this.renderWithLayout(
387
+ entityResult.entity,
388
+ Object.assign({}, entityResult.entity, {currentYear: new Date().getFullYear()}),
389
+ sc.get(entityResult.entity, 'layout', 'default'),
390
+ domain,
391
+ res
392
+ );
393
+ }
394
+
395
+ async renderTemplate(templatePath, domain, res)
396
+ {
397
+ let data = { currentYear: new Date().getFullYear() };
398
+ let content = await this.renderContentWithTemplate(templatePath, data, domain);
399
+ if(!content){
400
+ return res.status(500).send('Template error: '+templatePath);
201
401
  }
202
- let template = FileHandler.readFile(templatePath).toString();
203
- let data = {
204
- ...entity,
205
- title: entity.title || entity.name || entityName,
206
- current_year: new Date().getFullYear()
207
- };
208
- let rendered = mustache.render(template, data);
209
- return res.send(rendered);
402
+ return await this.renderWithLayout({content}, data, 'default', domain, res);
210
403
  }
211
404
 
212
- async renderTemplateOnly(res, templatePath)
405
+ async renderContentWithTemplate(templatePath, data, domain)
213
406
  {
214
- let template = FileHandler.readFile(templatePath).toString();
215
- let data = {
216
- title: 'Page Title',
217
- current_year: new Date().getFullYear()
218
- };
219
- let rendered = mustache.render(template, data);
220
- return res.send(rendered);
407
+ let template = FileHandler.readFile(templatePath);
408
+ if(!template){
409
+ Logger.error('Failed to read template: ' + templatePath);
410
+ return false;
411
+ }
412
+ return await this.renderEnhanced(template, data, this.getPartialsForDomain(domain));
221
413
  }
222
414
 
223
- async renderNotFoundPage(res)
415
+ async renderWithLayout(content, data, layoutName, domain, res)
224
416
  {
225
- let templatePath = FileHandler.joinPaths(this.templatesPath, '404.html');
226
- if(!FileHandler.exists(templatePath)){
417
+ let layoutPath = this.findLayoutPath(layoutName, domain);
418
+ let layoutContent = '';
419
+ if(layoutPath){
420
+ let layoutTemplate = FileHandler.readFile(layoutPath);
421
+ if(layoutTemplate){
422
+ layoutContent = await this.renderEnhanced(
423
+ layoutTemplate,
424
+ Object.assign({}, data, {
425
+ content: sc.get(content, 'content', '')
426
+ }),
427
+ this.getPartialsForDomain(domain)
428
+ );
429
+ }
430
+ }
431
+ if('' === layoutContent){
432
+ layoutContent = sc.get(content, 'content', '');
433
+ }
434
+ let pagePath = this.findTemplatePath('page', domain);
435
+ if(!pagePath){
436
+ return res.send(layoutContent);
437
+ }
438
+ let pageTemplate = FileHandler.readFile(pagePath);
439
+ if(!pageTemplate){
440
+ return res.send(layoutContent);
441
+ }
442
+ return res.send(
443
+ await this.renderEnhanced(
444
+ pageTemplate,
445
+ Object.assign({}, data, {
446
+ content: layoutContent,
447
+ siteHandle: this.resolveDomainToSiteKey(domain)
448
+ }),
449
+ this.getPartialsForDomain(domain)
450
+ )
451
+ );
452
+ }
453
+
454
+ async renderNotFound(domain, res)
455
+ {
456
+ let templatePath = this.findTemplatePath('404', domain);
457
+ if(!templatePath){
458
+ return res.status(404).send('Page not found');
459
+ }
460
+ let data = {title: '404 - Page Not Found', currentYear: new Date().getFullYear()};
461
+ let content = await this.renderContentWithTemplate(templatePath, data, domain);
462
+ if(!content){
227
463
  return res.status(404).send('Page not found');
228
464
  }
229
- let template = FileHandler.readFile(templatePath).toString();
230
- let data = {
231
- title: '404 - Page Not Found',
232
- current_year: new Date().getFullYear()
233
- };
234
- let rendered = mustache.render(template, data);
235
- return res.status(404).send(rendered);
465
+ res.status(404);
466
+ return await this.renderWithLayout({content}, data, 'default', domain, res);
236
467
  }
237
468
 
238
469
  }