@reldens/cms 0.9.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,20 +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.partialsExtensions = sc.get(props, 'partialsExtensions', ['.html', '.mustache', '.template']);
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', {});
23
26
  this.partialsCache = {};
24
27
  this.domainPartialsCache = new Map();
25
28
  this.domainTemplatesMap = new Map();
26
- this.error = false;
29
+ this.entityAccessCache = new Map();
27
30
  }
28
31
 
29
32
  async initialize()
30
33
  {
31
34
  if(!this.app || !this.dataServer){
32
- 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.');
33
44
  return false;
34
45
  }
35
46
  if(!FileHandler.exists(this.templatesPath)){
@@ -42,6 +53,7 @@ class Frontend
42
53
  }
43
54
  await this.loadPartials();
44
55
  await this.setupDomainTemplates();
56
+ await this.loadEntityAccessRules();
45
57
  this.setupStaticAssets();
46
58
  this.app.get('*', async (req, res) => {
47
59
  return await this.handleRequest(req, res);
@@ -49,22 +61,36 @@ class Frontend
49
61
  return true;
50
62
  }
51
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
+
52
77
  async loadPartials()
53
78
  {
54
79
  let partialsPath = FileHandler.joinPaths(this.templatesPath, 'partials');
55
80
  FileHandler.createFolder(partialsPath);
56
- let partialFiles = FileHandler.getFilesInFolder(partialsPath, this.partialsExtensions);
81
+ let partialFiles = FileHandler.getFilesInFolder(partialsPath, this.templateExtensions);
57
82
  for(let file of partialFiles){
58
- let partialName = '';
59
- for(let extension of this.partialsExtensions){
60
- if(file.endsWith(extension)){
61
- partialName = file.replace(extension, '');
62
- break;
63
- }
83
+ let partialName = this.extractTemplateName(file);
84
+ if(!partialName){
85
+ continue;
64
86
  }
65
87
  let partialPath = FileHandler.joinPaths(partialsPath, file);
66
88
  let partialContent = FileHandler.readFile(partialPath);
67
- this.partialsCache[partialName] = partialContent.toString();
89
+ if(!partialContent){
90
+ Logger.error('Failed to read partial: '+partialPath);
91
+ continue;
92
+ }
93
+ this.partialsCache[partialName] = partialContent;
68
94
  }
69
95
  }
70
96
 
@@ -75,18 +101,19 @@ class Frontend
75
101
  return;
76
102
  }
77
103
  let domainPartials = {};
78
- let partialFiles = FileHandler.getFilesInFolder(domainPartialsPath, this.partialsExtensions);
104
+ let partialFiles = FileHandler.getFilesInFolder(domainPartialsPath, this.templateExtensions);
79
105
  for(let file of partialFiles){
80
- let partialName = '';
81
- for(let extension of this.partialsExtensions){
82
- if(file.endsWith(extension)){
83
- partialName = file.replace(extension, '');
84
- break;
85
- }
106
+ let partialName = this.extractTemplateName(file);
107
+ if(!partialName){
108
+ continue;
86
109
  }
87
110
  let partialPath = FileHandler.joinPaths(domainPartialsPath, file);
88
111
  let partialContent = FileHandler.readFile(partialPath);
89
- domainPartials[partialName] = partialContent.toString();
112
+ if(!partialContent){
113
+ Logger.error('Failed to read domain partial: '+partialPath);
114
+ continue;
115
+ }
116
+ domainPartials[partialName] = partialContent;
90
117
  }
91
118
  this.domainPartialsCache.set(domain, domainPartials);
92
119
  }
@@ -105,6 +132,16 @@ class Frontend
105
132
  }
106
133
  }
107
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
+
108
145
  getDomainFromRequest(req)
109
146
  {
110
147
  let host = req.get('host');
@@ -114,34 +151,80 @@ class Frontend
114
151
  return host.split(':')[0];
115
152
  }
116
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
+
117
167
  getPartialsForDomain(domain)
118
168
  {
119
- let domainPartials = this.domainPartialsCache.get(domain);
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
+ }
120
174
  if(!domainPartials){
121
175
  return this.partialsCache;
122
176
  }
123
177
  return Object.assign({}, this.partialsCache, domainPartials);
124
178
  }
125
179
 
126
- findTemplatePathForDomain(templateName, domain)
180
+ findTemplatePath(templateName, domain)
127
181
  {
128
- if(!domain){
129
- return this.getDefaultTemplatePath(templateName);
130
- }
131
- let domainPath = this.domainTemplatesMap.get(domain);
132
- if(!domainPath){
133
- return this.getDefaultTemplatePath(templateName);
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
+ }
134
200
  }
135
- let domainTemplatePath = FileHandler.joinPaths(domainPath, templateName + '.html');
136
- if(!FileHandler.exists(domainTemplatePath)){
137
- return this.getDefaultTemplatePath(templateName);
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
+ }
138
211
  }
139
- return domainTemplatePath;
212
+ return false;
140
213
  }
141
214
 
142
- getDefaultTemplatePath(templateName)
215
+ findLayoutPath(layoutName, domain)
143
216
  {
144
- return FileHandler.joinPaths(this.templatesPath, templateName + '.html');
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);
145
228
  }
146
229
 
147
230
  setupStaticAssets()
@@ -158,37 +241,22 @@ class Frontend
158
241
 
159
242
  async handleRequest(req, res)
160
243
  {
161
- let path = req.path;
162
- if('/favicon.ico' === path){
163
- return res.status(404).send('');
164
- }
165
244
  try {
245
+ let path = req.path;
166
246
  let domain = this.getDomainFromRequest(req);
167
247
  let route = await this.findRouteByPath(path);
168
248
  if(route){
169
- Logger.debug('Found route for path: '+path, route);
170
- return await this.renderContentFromRoute(res, route, domain);
249
+ return await this.renderRoute(route, domain, res);
171
250
  }
172
- let pathSegments = path.split('/').filter(segment => segment !== '');
173
- if(0 < pathSegments.length){
174
- let entityResult = await this.findEntityByPath(pathSegments);
175
- if(entityResult){
176
- Logger.debug('Found entity for path segments: '+pathSegments.join('/'));
177
- return await this.renderContentFromEntity(
178
- res,
179
- entityResult.entity,
180
- entityResult.entityName,
181
- domain
182
- );
183
- }
251
+ let entityResult = await this.findEntityByPath(path);
252
+ if(entityResult){
253
+ return await this.renderEntity(entityResult, domain, res);
184
254
  }
185
255
  let templatePath = this.findTemplateByPath(path, domain);
186
256
  if(templatePath){
187
- Logger.debug('Found template for path: '+path+' at: '+templatePath);
188
- return await this.renderTemplateOnly(res, templatePath, domain);
257
+ return await this.renderTemplate(templatePath, domain, res);
189
258
  }
190
- Logger.debug('No template found for path: '+path+', rendering 404');
191
- return await this.renderNotFoundPage(res, domain);
259
+ return await this.renderNotFound(domain, res);
192
260
  } catch (error) {
193
261
  Logger.error('Request handling error: '+error.message);
194
262
  return res.status(500).send('Internal server error');
@@ -212,32 +280,34 @@ class Frontend
212
280
  return false;
213
281
  }
214
282
 
215
- 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)
216
292
  {
217
- if(1 > pathSegments.length){
218
- Logger.debug('No path segments provided');
293
+ let pathSegments = path.split('/').filter(segment => '' !== segment);
294
+ if(2 > pathSegments.length){
219
295
  return false;
220
296
  }
221
297
  let entityName = pathSegments[0];
222
- let entityId = 2 > pathSegments.length ? false : pathSegments[1];
223
- if(!entityId){
224
- Logger.debug('No entity ID in path segments');
298
+ if(!await this.isEntityAccessible(entityName)){
225
299
  return false;
226
300
  }
301
+ let entityId = pathSegments[1];
227
302
  let entity = this.dataServer.getEntity(entityName);
228
303
  if(!entity){
229
- Logger.debug('Entity not found: '+entityName);
230
304
  return false;
231
305
  }
232
306
  let loadedEntity = await entity.loadById(entityId);
233
307
  if(!loadedEntity){
234
- Logger.debug('Entity not loaded by ID: '+entityId);
235
308
  return false;
236
309
  }
237
- return {
238
- entity: loadedEntity,
239
- entityName
240
- };
310
+ return {entity: loadedEntity, entityName};
241
311
  }
242
312
 
243
313
  findTemplateByPath(path, domain)
@@ -245,101 +315,155 @@ class Frontend
245
315
  if('/' === path){
246
316
  path = '/index';
247
317
  }
248
- let templatePath = path.endsWith('/')
249
- ? path.slice(0, -1)
250
- : path;
251
- templatePath = templatePath.startsWith('/')
252
- ? templatePath.substring(1)
253
- : templatePath;
254
- let fullPath = this.findTemplatePathForDomain(templatePath, domain);
255
- if(FileHandler.exists(fullPath)){
256
- 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;
257
322
  }
258
- return false;
323
+ return this.findTemplatePath(templatePath, domain);
324
+ }
325
+
326
+ async renderEnhanced(template, data, partials)
327
+ {
328
+ return this.renderEngine.render(
329
+ await this.processCustomTemplateFunctions(template),
330
+ data,
331
+ partials
332
+ );
259
333
  }
260
334
 
261
- async renderContentFromRoute(res, route, domain)
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)
262
363
  {
263
364
  if(!route.router || !route.content_id){
264
- Logger.debug('Route missing router or content_id');
265
- return await this.renderNotFoundPage(res, domain);
365
+ return await this.renderNotFound(domain, res);
266
366
  }
267
367
  let entity = this.dataServer.getEntity(route.router);
268
368
  if(!entity){
269
- Logger.debug('Entity not found: '+route.router);
270
- return await this.renderNotFoundPage(res, domain);
369
+ return await this.renderNotFound(domain, res);
271
370
  }
272
371
  let content = await entity.loadById(route.content_id);
273
372
  if(!content){
274
- Logger.debug('Content not found for ID: '+route.content_id+' in entity: '+route.router);
275
- return await this.renderNotFoundPage(res, domain);
276
- }
277
- let templateName = content.template || route.router;
278
- let templatePath = this.findTemplatePathForDomain(templateName, domain);
279
- if(!FileHandler.exists(templatePath)){
280
- templatePath = this.findTemplatePathForDomain('page', domain);
281
- if(!FileHandler.exists(templatePath)){
282
- Logger.debug('Neither template found: '+templateName+'.html nor page.html');
283
- return await this.renderNotFoundPage(res, domain);
284
- }
373
+ return await this.renderNotFound(domain, res);
374
+ }
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
+ );
382
+ }
383
+
384
+ async renderEntity(entityResult, domain, res)
385
+ {
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);
285
401
  }
286
- let template = FileHandler.readFile(templatePath).toString();
287
- let data = {
288
- ...route,
289
- ...content,
290
- current_year: new Date().getFullYear()
291
- };
292
- let partials = this.getPartialsForDomain(domain);
293
- let rendered = mustache.render(template, data, partials);
294
- return res.send(rendered);
402
+ return await this.renderWithLayout({content}, data, 'default', domain, res);
295
403
  }
296
404
 
297
- async renderContentFromEntity(res, entity, entityName, domain)
405
+ async renderContentWithTemplate(templatePath, data, domain)
298
406
  {
299
- let templatePath = this.findTemplatePathForDomain(entityName, domain);
300
- if(!FileHandler.exists(templatePath)){
301
- templatePath = this.findTemplatePathForDomain('page', domain);
302
- if(!FileHandler.exists(templatePath)){
303
- return await this.renderNotFoundPage(res, domain);
304
- }
407
+ let template = FileHandler.readFile(templatePath);
408
+ if(!template){
409
+ Logger.error('Failed to read template: ' + templatePath);
410
+ return false;
305
411
  }
306
- let template = FileHandler.readFile(templatePath).toString();
307
- let data = {
308
- ...entity,
309
- title: entity.title || entity.name || entityName,
310
- current_year: new Date().getFullYear()
311
- };
312
- let partials = this.getPartialsForDomain(domain);
313
- let rendered = mustache.render(template, data, partials);
314
- return res.send(rendered);
412
+ return await this.renderEnhanced(template, data, this.getPartialsForDomain(domain));
315
413
  }
316
414
 
317
- async renderTemplateOnly(res, templatePath, domain)
415
+ async renderWithLayout(content, data, layoutName, domain, res)
318
416
  {
319
- let template = FileHandler.readFile(templatePath).toString();
320
- let data = {
321
- title: 'Page Title',
322
- current_year: new Date().getFullYear()
323
- };
324
- let partials = this.getPartialsForDomain(domain);
325
- let rendered = mustache.render(template, data, partials);
326
- return res.send(rendered);
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
+ );
327
452
  }
328
453
 
329
- async renderNotFoundPage(res, domain)
454
+ async renderNotFound(domain, res)
330
455
  {
331
- let templatePath = this.findTemplatePathForDomain('404', domain);
332
- if(!FileHandler.exists(templatePath)){
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){
333
463
  return res.status(404).send('Page not found');
334
464
  }
335
- let template = FileHandler.readFile(templatePath).toString();
336
- let data = {
337
- title: '404 - Page Not Found',
338
- current_year: new Date().getFullYear()
339
- };
340
- let partials = this.getPartialsForDomain(domain);
341
- let rendered = mustache.render(template, data, partials);
342
- return res.status(404).send(rendered);
465
+ res.status(404);
466
+ return await this.renderWithLayout({content}, data, 'default', domain, res);
343
467
  }
344
468
 
345
469
  }