@reldens/cms 0.16.0 → 0.19.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 (38) hide show
  1. package/README.md +92 -9
  2. package/admin/reldens-admin-client.css +55 -0
  3. package/admin/reldens-admin-client.js +24 -0
  4. package/admin/templates/cache-clean-button.html +4 -0
  5. package/admin/templates/clear-all-cache-button.html +18 -0
  6. package/admin/templates/fields/view/textarea.html +1 -1
  7. package/bin/reldens-cms-generate-entities.js +85 -18
  8. package/bin/reldens-cms.js +6 -6
  9. package/lib/admin-manager/contents-builder.js +257 -0
  10. package/lib/admin-manager/router-contents.js +618 -0
  11. package/lib/admin-manager/router.js +208 -0
  12. package/lib/admin-manager-validator.js +2 -1
  13. package/lib/admin-manager.js +116 -990
  14. package/lib/admin-translations.js +9 -1
  15. package/lib/cache/add-cache-button-subscriber.js +149 -0
  16. package/lib/cache/cache-manager.js +168 -0
  17. package/lib/cache/cache-routes-handler.js +99 -0
  18. package/lib/cms-pages-route-manager.js +45 -21
  19. package/lib/frontend.js +288 -71
  20. package/lib/installer.js +5 -2
  21. package/lib/json-fields-parser.js +74 -0
  22. package/lib/manager.js +49 -4
  23. package/lib/pagination-handler.js +243 -0
  24. package/lib/search-renderer.js +116 -0
  25. package/lib/search.js +344 -0
  26. package/lib/template-engine/collections-single-transformer.js +53 -0
  27. package/lib/template-engine/collections-transformer-base.js +84 -0
  28. package/lib/template-engine/collections-transformer.js +353 -0
  29. package/lib/template-engine/entities-transformer.js +65 -0
  30. package/lib/template-engine/partials-transformer.js +171 -0
  31. package/lib/template-engine.js +53 -387
  32. package/lib/templates-list.js +2 -0
  33. package/migrations/default-homepage.sql +6 -6
  34. package/migrations/install.sql +21 -20
  35. package/package.json +4 -4
  36. package/templates/page.html +19 -2
  37. package/templates/partials/entriesListView.html +14 -0
  38. package/templates/partials/pagedCollection.html +33 -0
package/lib/manager.js CHANGED
@@ -17,6 +17,7 @@ const { AdminManager } = require('./admin-manager');
17
17
  const { CmsPagesRouteManager } = require('./cms-pages-route-manager');
18
18
  const { Installer } = require('./installer');
19
19
  const { Frontend } = require('./frontend');
20
+ const { CacheManager } = require('./cache/cache-manager');
20
21
  const { EventsManagerSingleton, Logger, sc } = require('@reldens/utils');
21
22
  const { DriversMap } = require('@reldens/storage');
22
23
  const { AppServerFactory, FileHandler, Encryptor } = require('@reldens/server-utils');
@@ -46,7 +47,7 @@ class Manager
46
47
  this.projectAdminPath = FileHandler.joinPaths(this.projectRoot, 'admin');
47
48
  this.projectAdminTemplatesPath = FileHandler.joinPaths(this.projectAdminPath, 'templates');
48
49
  this.mimeTypes = sc.get(props, 'mimeTypes', MimeTypes);
49
- this.allowedExtensions = sc.get(props, 'allowedExtensions', AllowedExtensions)
50
+ this.allowedExtensions = sc.get(props, 'allowedExtensions', AllowedExtensions);
50
51
  this.adminRoleId = sc.get(props, 'adminRoleId', 99);
51
52
  this.mappedAdminTemplates = TemplatesToPathMapper.map(this.adminTemplatesList, this.projectAdminTemplatesPath);
52
53
  this.stylesFilePath = sc.get(props, 'stylesFilePath', '/css/reldens-admin-client.css');
@@ -58,6 +59,7 @@ class Manager
58
59
  this.domainMapping = sc.get(props, 'domainMapping', sc.toJson(process.env.RELDENS_DOMAIN_MAPPING));
59
60
  this.siteKeyMapping = sc.get(props, 'siteKeyMapping', sc.toJson(process.env.RELDENS_SITE_KEY_MAPPING));
60
61
  this.templateExtensions = sc.get(props, 'templateExtensions', ['.html', '.template']);
62
+ this.cache = sc.get(props, 'cache', false);
61
63
  this.app = sc.get(props, 'app', false);
62
64
  this.appServer = sc.get(props, 'appServer', false);
63
65
  this.dataServer = sc.get(props, 'dataServer', false);
@@ -65,8 +67,22 @@ class Manager
65
67
  this.frontend = sc.get(props, 'frontend', false);
66
68
  this.renderEngine = sc.get(props, 'renderEngine', mustache);
67
69
  this.prismaClient = sc.get(props, 'prismaClient', false);
70
+ this.developmentPatterns = sc.get(props, 'developmentPatterns', [
71
+ 'localhost',
72
+ '127.0.0.1',
73
+ '.local',
74
+ '.test',
75
+ '.dev',
76
+ '.staging'
77
+ ]);
78
+ this.developmentEnvironments = sc.get(props, 'developmentEnvironments', ['development', 'dev', 'test']);
79
+ this.developmentPorts = sc.get(props, 'developmentPorts', [3000, 8080, 8081]);
80
+ this.developmentMultiplier = sc.get(props, 'developmentMultiplier', 10);
81
+ this.appServerConfig = sc.get(props, 'appServerConfig', {});
82
+ this.developmentExternalDomains = sc.get(props, 'developmentExternalDomains', {});
68
83
  this.appServerFactory = new AppServerFactory();
69
84
  this.adminEntitiesGenerator = new AdminEntitiesGenerator();
85
+ this.cacheManager = new CacheManager({projectRoot: this.projectRoot, enabled: this.cache});
70
86
  this.installer = new Installer({
71
87
  projectRoot: this.projectRoot,
72
88
  prismaClient: this.prismaClient,
@@ -169,7 +185,8 @@ class Manager
169
185
  async start()
170
186
  {
171
187
  if(!this.useProvidedServer){
172
- let createdAppServer = this.appServerFactory.createAppServer();
188
+ let appServerConfig = this.buildAppServerConfiguration();
189
+ let createdAppServer = this.appServerFactory.createAppServer(appServerConfig);
173
190
  if(this.appServerFactory.error.message){
174
191
  Logger.error('App server error: '+this.appServerFactory.error.message);
175
192
  return false;
@@ -196,6 +213,30 @@ class Manager
196
213
  }
197
214
  }
198
215
 
216
+ buildAppServerConfiguration()
217
+ {
218
+ let baseConfig = {
219
+ port: this.config.port,
220
+ useHttps: this.config.host.startsWith('https://'),
221
+ domainMapping: this.domainMapping || {},
222
+ defaultDomain: this.defaultDomain,
223
+ developmentPatterns: this.developmentPatterns,
224
+ developmentEnvironments: this.developmentEnvironments,
225
+ developmentPorts: this.developmentPorts,
226
+ developmentMultiplier: this.developmentMultiplier,
227
+ developmentExternalDomains: this.developmentExternalDomains
228
+ };
229
+ let appServerConfig = Object.assign({}, baseConfig, this.appServerConfig);
230
+ if(this.domainMapping && 'object' === typeof this.domainMapping){
231
+ let mappingKeys = Object.keys(this.domainMapping);
232
+ for(let domain of mappingKeys){
233
+ this.appServerFactory.addDevelopmentDomain(domain);
234
+ }
235
+ this.appServerFactory.setDomainMapping(this.domainMapping);
236
+ }
237
+ return appServerConfig;
238
+ }
239
+
199
240
  async initializeCmsAfterInstall(props)
200
241
  {
201
242
  try {
@@ -322,7 +363,8 @@ class Manager
322
363
  user: this.config.database.user,
323
364
  password: this.config.database.password
324
365
  },
325
- rawEntities: this.rawRegisteredEntities
366
+ rawEntities: this.rawRegisteredEntities,
367
+ entitiesConfig: this.entitiesConfig
326
368
  };
327
369
  let driverClass = DriversMap[this.config.database.driver];
328
370
  if(!driverClass){
@@ -395,6 +437,7 @@ class Manager
395
437
  adminRoleId: this.adminRoleId,
396
438
  stylesFilePath: this.stylesFilePath,
397
439
  scriptsFilePath: this.scriptsFilePath,
440
+ cacheManager: this.cacheManager,
398
441
  branding: {
399
442
  companyName: this.companyName,
400
443
  logo: this.logo,
@@ -439,7 +482,9 @@ class Manager
439
482
  defaultDomain: this.defaultDomain,
440
483
  domainMapping: this.domainMapping,
441
484
  siteKeyMapping: this.siteKeyMapping,
442
- templateExtensions: this.templateExtensions
485
+ templateExtensions: this.templateExtensions,
486
+ entitiesConfig: this.entitiesConfig,
487
+ cacheManager: this.cacheManager
443
488
  });
444
489
  return await this.frontend.initialize();
445
490
  }
@@ -0,0 +1,243 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - PaginationHandler
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+
9
+ class PaginationHandler
10
+ {
11
+
12
+ constructor()
13
+ {
14
+ this.defaultLimit = 10;
15
+ this.prevPageLabel = 'Previous';
16
+ this.nextPageLabel = 'Next';
17
+ }
18
+
19
+ sanitizeCollectionKey(collectionKey)
20
+ {
21
+ if(!collectionKey || 'object' !== typeof collectionKey){
22
+ return {};
23
+ }
24
+ let sanitizedKey = {};
25
+ let page = sc.parseNumber(sc.get(collectionKey, 'page', 0));
26
+ if(1 <= page){
27
+ sanitizedKey.page = page;
28
+ }
29
+ let limit = sc.parseNumber(sc.get(collectionKey, 'limit', 0));
30
+ if(0 < limit){
31
+ sanitizedKey.limit = limit;
32
+ }
33
+ let sortBy = sc.get(collectionKey, 'sortBy', '');
34
+ if(sc.isString(sortBy) && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(sortBy)){
35
+ sanitizedKey.sortBy = sortBy;
36
+ }
37
+ let sortDirection = sc.get(collectionKey, 'sortDirection');
38
+ if('asc' === sortDirection || 'desc' === sortDirection){
39
+ sanitizedKey.sortDirection = sortDirection;
40
+ }
41
+ let filters = sc.get(collectionKey, 'filters', false);
42
+ if('object' === typeof filters){
43
+ sanitizedKey.filters = this.sanitizeFilters(filters);
44
+ }
45
+ return sanitizedKey;
46
+ }
47
+
48
+ sanitizeFilters(filters)
49
+ {
50
+ let sanitizedFilters = {};
51
+ for(let key of Object.keys(filters)){
52
+ if(!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)){
53
+ continue;
54
+ }
55
+ let value = filters[key];
56
+ if(sc.isString(value) || sc.isNumber(value) || sc.isBoolean(value)){
57
+ sanitizedFilters[key] = value;
58
+ }
59
+ }
60
+ return sanitizedFilters;
61
+ }
62
+
63
+ extractCollectionKeyFromRequest(req, collectionId)
64
+ {
65
+ if(!req || !req.query){
66
+ return {};
67
+ }
68
+ let paramName = collectionId + '-key';
69
+ let paramValue = sc.get(req.query, paramName, '');
70
+ if(!paramValue){
71
+ return {};
72
+ }
73
+ let decodedValue = decodeURIComponent(paramValue);
74
+ let parsedKey = sc.parseJson(decodedValue, {});
75
+ if(!parsedKey){
76
+ Logger.warning('Invalid collection key JSON: ' + decodedValue);
77
+ return {};
78
+ }
79
+ return this.sanitizeCollectionKey(parsedKey);
80
+ }
81
+
82
+ mergeCollectionParameters(templateParams, requestParams)
83
+ {
84
+ let templateFilters = sc.get(templateParams, 'filters', {});
85
+ let requestFilters = sc.get(requestParams, 'filters', {});
86
+ let mergedFilters = Object.assign({}, templateFilters, requestFilters);
87
+ return {
88
+ limit: sc.get(requestParams, 'limit', sc.get(templateParams, 'limit', this.defaultLimit)),
89
+ page: sc.get(requestParams, 'page', 1),
90
+ sortBy: sc.get(requestParams, 'sortBy', sc.get(templateParams, 'sortBy', 'id')),
91
+ sortDirection: sc.get(requestParams, 'sortDirection', sc.get(templateParams, 'sortDirection', 'asc')),
92
+ filters: mergedFilters
93
+ };
94
+ }
95
+
96
+ calculatePaginationData(totalRecords, currentPage, limit, baseUrl, collectionId, params, templateDefaults)
97
+ {
98
+ if(0 >= totalRecords){
99
+ return this.createEmptyPaginationData();
100
+ }
101
+ let totalPages = Math.ceil(totalRecords / limit);
102
+ if(currentPage > totalPages){
103
+ currentPage = totalPages;
104
+ }
105
+ let offset = (currentPage - 1) * limit;
106
+ let hasNextPage = currentPage < totalPages;
107
+ let hasPrevPage = 1 < currentPage;
108
+ let prevPageUrl = '';
109
+ let nextPageUrl = '';
110
+ if(hasPrevPage){
111
+ prevPageUrl = this.buildPageUrl(baseUrl, collectionId, params, currentPage - 1, templateDefaults);
112
+ }
113
+ if(hasNextPage){
114
+ nextPageUrl = this.buildPageUrl(baseUrl, collectionId, params, currentPage + 1, templateDefaults);
115
+ }
116
+ let prevPages = this.calculatePrevPages(currentPage, params, baseUrl, collectionId, templateDefaults);
117
+ let nextPages = this.calculateNextPages(
118
+ currentPage,
119
+ totalPages,
120
+ params,
121
+ baseUrl,
122
+ collectionId,
123
+ templateDefaults
124
+ );
125
+ return {
126
+ currentPage,
127
+ totalPages,
128
+ totalRecords,
129
+ limit,
130
+ offset,
131
+ hasNextPage,
132
+ hasPrevPage,
133
+ prevPageUrl,
134
+ nextPageUrl,
135
+ prevPageLabel: this.prevPageLabel,
136
+ nextPageLabel: this.nextPageLabel,
137
+ prevPages,
138
+ nextPages
139
+ };
140
+ }
141
+
142
+ createEmptyPaginationData()
143
+ {
144
+ return {
145
+ currentPage: 1,
146
+ totalPages: 0,
147
+ totalRecords: 0,
148
+ limit: this.defaultLimit,
149
+ offset: 0,
150
+ hasNextPage: false,
151
+ hasPrevPage: false,
152
+ prevPageUrl: '',
153
+ nextPageUrl: '',
154
+ prevPageLabel: this.prevPageLabel,
155
+ nextPageLabel: this.nextPageLabel,
156
+ prevPages: [],
157
+ nextPages: []
158
+ };
159
+ }
160
+
161
+ calculatePrevPages(currentPage, params, baseUrl, collectionId, templateDefaults)
162
+ {
163
+ let prevPagesCount = sc.parseNumber(sc.get(params, 'prevPages', 2));
164
+ if(!prevPagesCount || 0 >= prevPagesCount || 1 >= currentPage){
165
+ return [];
166
+ }
167
+ let prevPages = [];
168
+ let startPage = Math.max(1, currentPage - prevPagesCount);
169
+ for(let page = startPage; page < currentPage; page++){
170
+ prevPages.push({
171
+ pageLabel: page,
172
+ pageUrl: this.buildPageUrl(baseUrl, collectionId, params, page, templateDefaults)
173
+ });
174
+ }
175
+ return prevPages;
176
+ }
177
+
178
+ calculateNextPages(currentPage, totalPages, params, baseUrl, collectionId, templateDefaults)
179
+ {
180
+ let nextPagesCount = sc.parseNumber(sc.get(params, 'nextPages', 2));
181
+ if(!nextPagesCount || 0 >= nextPagesCount || currentPage >= totalPages){
182
+ return [];
183
+ }
184
+ let nextPages = [];
185
+ let endPage = Math.min(totalPages, currentPage + nextPagesCount);
186
+ for(let page = currentPage + 1; page <= endPage; page++){
187
+ nextPages.push({
188
+ pageLabel: page,
189
+ pageUrl: this.buildPageUrl(baseUrl, collectionId, params, page, templateDefaults)
190
+ });
191
+ }
192
+ return nextPages;
193
+ }
194
+
195
+ buildPageUrl(baseUrl, collectionId, params, page, templateDefaults)
196
+ {
197
+ let urlParams = {};
198
+ urlParams.page = page;
199
+ let templateFilters = sc.get(templateDefaults, 'filters', {});
200
+ let templateLimit = sc.get(templateDefaults, 'limit', this.defaultLimit);
201
+ let templateSortBy = sc.get(templateDefaults, 'sortBy', 'id');
202
+ let templateSortDirection = sc.get(templateDefaults, 'sortDirection', 'asc');
203
+ let currentFilters = sc.get(params, 'filters', {});
204
+ for(let filterKey of Object.keys(currentFilters)){
205
+ if(!sc.hasOwn(templateFilters, filterKey) || templateFilters[filterKey] !== currentFilters[filterKey]){
206
+ if(!sc.hasOwn(urlParams, 'filters')){
207
+ urlParams.filters = {};
208
+ }
209
+ urlParams.filters[filterKey] = currentFilters[filterKey];
210
+ }
211
+ }
212
+ if(params.limit !== templateLimit){
213
+ urlParams.limit = params.limit;
214
+ }
215
+ if(params.sortBy !== templateSortBy){
216
+ urlParams.sortBy = params.sortBy;
217
+ }
218
+ if(params.sortDirection !== templateSortDirection){
219
+ urlParams.sortDirection = params.sortDirection;
220
+ }
221
+ let collectionKey = encodeURIComponent(JSON.stringify(urlParams));
222
+ let paramName = collectionId + '-key';
223
+ let separator = -1 !== baseUrl.indexOf('?') ? '&' : '?';
224
+ return baseUrl + separator + paramName + '=' + collectionKey;
225
+ }
226
+
227
+ async getCollectionTotal(entity, filters)
228
+ {
229
+ if(!entity || !sc.isFunction(entity.count)){
230
+ Logger.critical('Entity does not support count method');
231
+ return 0;
232
+ }
233
+ try {
234
+ return await entity.count(filters || {});
235
+ } catch (error) {
236
+ Logger.critical('Failed to count collection records: ' + error.message);
237
+ return 0;
238
+ }
239
+ }
240
+
241
+ }
242
+
243
+ module.exports.PaginationHandler = PaginationHandler;
@@ -0,0 +1,116 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - SearchRenderer
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+
9
+ class SearchRenderer
10
+ {
11
+
12
+ constructor(props)
13
+ {
14
+ this.renderEngine = sc.get(props, 'renderEngine', false);
15
+ this.getPartials = sc.get(props, 'getPartials', false);
16
+ }
17
+
18
+ async renderSearchResults(searchResults, config, domain, req)
19
+ {
20
+ if(!searchResults || !sc.isArray(searchResults) || 0 === searchResults.length){
21
+ return this.renderNoSearchResults(domain);
22
+ }
23
+ let renderedContent = '';
24
+ for(let entityResult of searchResults){
25
+ if(!sc.hasOwn(entityResult, 'results') || !sc.isArray(entityResult.results)){
26
+ continue;
27
+ }
28
+ if(0 === entityResult.results.length){
29
+ renderedContent += this.renderNoSearchResults(domain);
30
+ continue;
31
+ }
32
+ let partialName = sc.get(config, 'render.partial', 'entriesListView');
33
+ let partialTemplate = this.loadPartialTemplate(partialName, domain);
34
+ if(!partialTemplate){
35
+ Logger.error('Search result partial template not found: ' + partialName);
36
+ continue;
37
+ }
38
+ if(sc.hasOwn(entityResult, 'pagination')){
39
+ let entityContent = await this.renderSearchEntityResults(entityResult.results, partialTemplate, domain);
40
+ let totalPages = sc.get(entityResult.pagination, 'totalPages', 1);
41
+ if(1 < totalPages){
42
+ let paginationContainer = sc.get(config, 'render.paginationContainer', 'pagedCollection');
43
+ let paginationTemplate = this.loadPaginationTemplate(paginationContainer, domain);
44
+ let paginationData = Object.assign({}, entityResult.pagination, {
45
+ collectionContentForCurrentPage: entityContent,
46
+ hasResults: 0 < entityResult.results.length,
47
+ noResultsMessage: entityResult.noResultsMessage || 'No results found.'
48
+ });
49
+ renderedContent += this.renderEngine.render(paginationTemplate, paginationData, this.getPartialsForDomain(domain));
50
+ continue;
51
+ }
52
+ renderedContent += entityContent;
53
+ continue;
54
+ }
55
+ renderedContent += await this.renderSearchEntityResults(entityResult.results, partialTemplate, domain);
56
+ }
57
+ return renderedContent;
58
+ }
59
+
60
+ async renderSearchEntityResults(results, partialTemplate, domain)
61
+ {
62
+ let renderedContent = '';
63
+ for(let row of results){
64
+ renderedContent += this.renderEngine.render(partialTemplate, {row}, this.getPartialsForDomain(domain));
65
+ }
66
+ return renderedContent;
67
+ }
68
+
69
+ renderNoSearchResults(domain)
70
+ {
71
+ let noResultsTemplate = this.loadPartialTemplate('noResults', domain);
72
+ if(noResultsTemplate){
73
+ return this.renderEngine.render(
74
+ noResultsTemplate,
75
+ {
76
+ message: 'No search results found.',
77
+ cssClass: 'no-search-results',
78
+ alertClass: 'alert-info'
79
+ },
80
+ this.getPartialsForDomain(domain)
81
+ );
82
+ }
83
+ return '<div class="no-search-results alert alert-info">No search results found.</div>';
84
+ }
85
+
86
+ loadPartialTemplate(partialName, domain)
87
+ {
88
+ let partials = this.getPartialsForDomain(domain);
89
+ return sc.get(partials, partialName, false);
90
+ }
91
+
92
+ loadPaginationTemplate(containerName, domain)
93
+ {
94
+ if(!containerName || '' === containerName){
95
+ containerName = 'pagedCollection';
96
+ }
97
+ let partialContent = this.loadPartialTemplate(containerName, domain);
98
+ if(partialContent){
99
+ return partialContent;
100
+ }
101
+ Logger.critical('Pagination template not found: ' + containerName + '. Please create the template file.');
102
+ return '{{&collectionContentForCurrentPage}}';
103
+ }
104
+
105
+ getPartialsForDomain(domain)
106
+ {
107
+ if(!this.getPartials){
108
+ Logger.error('getPartials function not provided to SearchRenderer');
109
+ return {};
110
+ }
111
+ return this.getPartials(domain);
112
+ }
113
+
114
+ }
115
+
116
+ module.exports.SearchRenderer = SearchRenderer;