@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
@@ -21,7 +21,9 @@ class AdminTranslations
21
21
  reldensGithubText: 'Need a new feature?'
22
22
  +' Would you like to contribute with code?'
23
23
  +' Find the source code or create an issue in GitHub',
24
- reldensLoading: 'Loading...'
24
+ reldensLoading: 'Loading...',
25
+ confirmClearCacheMessage: 'Are you sure you want to clear all cache? This action cannot be undone.',
26
+ clearCacheWarning: 'This will clear all cached routes and may impact site performance temporarily.'
25
27
  },
26
28
  labels: {
27
29
  navigation: 'Reldens - CMS',
@@ -32,6 +34,12 @@ class AdminTranslations
32
34
  shuttingDown: 'Server is shutting down in:',
33
35
  submitShutdownLabel: 'Shutdown Server',
34
36
  submitCancelLabel: 'Cancel Server Shutdown',
37
+ cleanCache: 'Clean Cache',
38
+ clearAllCache: 'Clear All Cache',
39
+ confirmClearCache: 'Confirm Clear Cache',
40
+ warning: 'Warning:',
41
+ cancel: 'Cancel',
42
+ confirm: 'Continue',
35
43
  }
36
44
  };
37
45
  for(let i of Object.keys(translations)){
@@ -0,0 +1,149 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - AddCacheButtonSubscriber
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+
9
+ class AddCacheButtonSubscriber
10
+ {
11
+
12
+ constructor(props = {})
13
+ {
14
+ this.events = sc.get(props, 'events', false);
15
+ this.cacheManager = sc.get(props, 'cacheManager', false);
16
+ this.renderCallback = sc.get(props, 'renderCallback', false);
17
+ this.cacheCleanButton = sc.get(props, 'cacheCleanButton', '');
18
+ this.clearAllCacheButton = sc.get(props, 'clearAllCacheButton', '');
19
+ this.translations = sc.get(props, 'translations', {});
20
+ this.cacheCleanRoute = sc.get(props, 'cacheCleanRoute', '');
21
+ this.clearAllCacheRoute = sc.get(props, 'clearAllCacheRoute', '');
22
+ this.setupEvents();
23
+ }
24
+
25
+ setupEvents()
26
+ {
27
+ if(!this.cacheManager){
28
+ Logger.error('Cache Manager not found on AddCacheButtonSubscriber.');
29
+ return false;
30
+ }
31
+ if(!this.cacheManager.isEnabled()){
32
+ Logger.debug('Cache Manager not enabled.');
33
+ return false;
34
+ }
35
+ if(!this.events){
36
+ Logger.error('Events Manager not found on AddCacheButtonSubscriber.');
37
+ return false;
38
+ }
39
+ //Logger.debug('Listening events PropertiesPopulation.');
40
+ this.events.on('reldens.adminViewPropertiesPopulation', this.populateViewFields.bind(this));
41
+ this.events.on('reldens.adminEditPropertiesPopulation', this.populateEditFields.bind(this));
42
+ this.events.on('reldens.adminListPropertiesPopulation', this.populateListFields.bind(this));
43
+ }
44
+
45
+ async populateViewFields(event)
46
+ {
47
+ let cacheButton = await this.generateCacheCleanButton(event);
48
+ if(!cacheButton){
49
+ //Logger.info('Missing cache button contents on AddCacheButtonSubscriber.');
50
+ return false;
51
+ }
52
+ if(!event.renderedViewProperties.extraContentForView){
53
+ event.renderedViewProperties.extraContentForView = '';
54
+ }
55
+ event.renderedViewProperties.extraContentForView += cacheButton;
56
+ //Logger.debug('Clean cache button ready on view.');
57
+ return true;
58
+ }
59
+
60
+ async populateEditFields(event)
61
+ {
62
+ let cacheButton = await this.generateCacheCleanButton(event);
63
+ if(!cacheButton){
64
+ //Logger.info('Missing cache button contents on AddCacheButtonSubscriber.');
65
+ return false;
66
+ }
67
+ if(!event.renderedEditProperties.extraContentForEdit){
68
+ event.renderedEditProperties.extraContentForEdit = '';
69
+ }
70
+ event.renderedEditProperties.extraContentForEdit += cacheButton;
71
+ //Logger.debug('Clean cache button ready on edit.');
72
+ return true;
73
+ }
74
+
75
+ async populateListFields(event)
76
+ {
77
+ let clearAllButton = await this.generateClearAllCacheButton(event);
78
+ if(!clearAllButton){
79
+ return false;
80
+ }
81
+ if(!event.listProperties.extraContentForList){
82
+ event.listProperties.extraContentForList = '';
83
+ }
84
+ event.listProperties.extraContentForList += clearAllButton;
85
+ return true;
86
+ }
87
+
88
+ async generateCacheCleanButton(event)
89
+ {
90
+ if('routes' !== event.driverResource.id()){
91
+ return false;
92
+ }
93
+ if(!event.loadedEntity){
94
+ Logger.error('Missing loaded entity on AddCacheButtonSubscriber.');
95
+ return false;
96
+ }
97
+ if(!event.loadedEntity.id){
98
+ Logger.error('Missing loaded entity ID on AddCacheButtonSubscriber.');
99
+ return false;
100
+ }
101
+ if(!this.cacheCleanButton){
102
+ Logger.error('Cache clean button template content not found');
103
+ return '';
104
+ }
105
+ if(!this.renderCallback){
106
+ Logger.error('Render callback not available for cache button');
107
+ return '';
108
+ }
109
+ return await this.renderCallback(
110
+ this.cacheCleanButton,
111
+ {
112
+ cacheCleanRoute: this.cacheCleanRoute,
113
+ routeId: event.loadedEntity.id,
114
+ buttonText: sc.get(this.translations.labels, 'cleanCache', 'cleanCache')
115
+ }
116
+ );
117
+ }
118
+
119
+ async generateClearAllCacheButton(event)
120
+ {
121
+ if('routes' !== event.driverResource.id()){
122
+ return false;
123
+ }
124
+ if(!this.clearAllCacheButton){
125
+ Logger.error('Clear all cache button template content not found');
126
+ return '';
127
+ }
128
+ if(!this.renderCallback){
129
+ Logger.error('Render callback not available for clear all cache button');
130
+ return '';
131
+ }
132
+ return await this.renderCallback(
133
+ this.clearAllCacheButton,
134
+ {
135
+ buttonText: sc.get(this.translations.labels, 'clearAllCache', 'clearAllCache'),
136
+ clearAllCacheRoute: this.clearAllCacheRoute,
137
+ confirmTitle: sc.get(this.translations.labels, 'confirmClearCache', 'confirmClearCache'),
138
+ confirmMessage: sc.get(this.translations.messages, 'confirmClearCacheMessage', 'confirmClearCacheMessage'),
139
+ warningText: sc.get(this.translations.labels, 'warning', 'warning'),
140
+ warningMessage: sc.get(this.translations.messages, 'clearCacheWarning', 'clearCacheWarning'),
141
+ cancelText: sc.get(this.translations.labels, 'cancel', 'cancel'),
142
+ confirmText: sc.get(this.translations.labels, 'confirm', 'confirm')
143
+ }
144
+ );
145
+ }
146
+
147
+ }
148
+
149
+ module.exports.AddCacheButtonSubscriber = AddCacheButtonSubscriber;
@@ -0,0 +1,168 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - CacheManager
4
+ *
5
+ */
6
+
7
+ const { FileHandler } = require('@reldens/server-utils');
8
+ const { Logger, sc } = require('@reldens/utils');
9
+
10
+ class CacheManager
11
+ {
12
+
13
+ constructor(props = {})
14
+ {
15
+ this.projectRoot = sc.get(props, 'projectRoot', './');
16
+ this.cacheBasePath = FileHandler.joinPaths(this.projectRoot, '.reldens_cms_cache');
17
+ this.enabled = sc.get(props, 'enabled', true);
18
+ }
19
+
20
+ generateCacheKey(domain, path)
21
+ {
22
+ let domainFolder = domain || 'default';
23
+ let pathSegments = path.split('/').filter(segment => '' !== segment);
24
+ if(0 === pathSegments.length){
25
+ pathSegments = ['index'];
26
+ }
27
+ let fileName = pathSegments.pop() + '.html';
28
+ let folderPath = FileHandler.joinPaths(this.cacheBasePath, domainFolder, ...pathSegments);
29
+ return {
30
+ folderPath,
31
+ fileName,
32
+ fullPath: FileHandler.joinPaths(folderPath, fileName)
33
+ };
34
+ }
35
+
36
+ async get(domain, path)
37
+ {
38
+ if(!this.enabled){
39
+ return false;
40
+ }
41
+ let cacheInfo = this.generateCacheKey(domain, path);
42
+ if(!FileHandler.exists(cacheInfo.fullPath)){
43
+ return false;
44
+ }
45
+ let cachedContent = FileHandler.readFile(cacheInfo.fullPath);
46
+ if(!cachedContent){
47
+ Logger.debug('Failed to read cached file: '+cacheInfo.fullPath);
48
+ return false;
49
+ }
50
+ return cachedContent;
51
+ }
52
+
53
+ async set(domain, path, content)
54
+ {
55
+ if(!this.enabled){
56
+ return false;
57
+ }
58
+ let cacheInfo = this.generateCacheKey(domain, path);
59
+ if(!FileHandler.createFolder(cacheInfo.folderPath)){
60
+ Logger.error('Failed to create cache folder: '+cacheInfo.folderPath);
61
+ return false;
62
+ }
63
+ if(!FileHandler.writeFile(cacheInfo.fullPath, content)){
64
+ Logger.error('Failed to write cache file: '+cacheInfo.fullPath);
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+
70
+ findAllCacheFilesForPath(domain, path)
71
+ {
72
+ let cacheInfo = this.generateCacheKey(domain, path);
73
+ if(!FileHandler.exists(cacheInfo.folderPath)){
74
+ return [];
75
+ }
76
+ let allFiles = FileHandler.readFolder(cacheInfo.folderPath);
77
+ if(0 === allFiles.length){
78
+ return [];
79
+ }
80
+ let baseFileName = cacheInfo.fileName.replace('.html', '');
81
+ let matchingFiles = [];
82
+ for(let file of allFiles){
83
+ if(file === cacheInfo.fileName){
84
+ matchingFiles.push(FileHandler.joinPaths(cacheInfo.folderPath, file));
85
+ continue;
86
+ }
87
+ if(file.startsWith(baseFileName + '_') && file.endsWith('.html')){
88
+ matchingFiles.push(FileHandler.joinPaths(cacheInfo.folderPath, file));
89
+ }
90
+ }
91
+ return matchingFiles;
92
+ }
93
+
94
+ async delete(domain, path)
95
+ {
96
+ let cacheByDomains = this.fetchCachePathsByDomain(domain, path);
97
+ for(let cacheInfo of cacheByDomains){
98
+ let allCacheFiles = this.findAllCacheFilesForPath(cacheInfo.domain || domain, path);
99
+ if(0 === allCacheFiles.length){
100
+ let singleCacheInfo = this.generateCacheKey(cacheInfo.domain || domain, path);
101
+ if(!FileHandler.exists(singleCacheInfo.fullPath)){
102
+ Logger.debug('No cache files found for: '+path);
103
+ continue;
104
+ }
105
+ allCacheFiles = [singleCacheInfo.fullPath];
106
+ }
107
+ for(let cacheFilePath of allCacheFiles){
108
+ if(!FileHandler.exists(cacheFilePath)){
109
+ Logger.debug('File does not exist: '+cacheFilePath);
110
+ continue;
111
+ }
112
+ if(!FileHandler.remove(cacheFilePath)){
113
+ Logger.error('Failed to delete cache file: '+cacheFilePath);
114
+ return false;
115
+ }
116
+ Logger.debug('Deleted cache file: '+cacheFilePath);
117
+ }
118
+ }
119
+ return true;
120
+ }
121
+
122
+ fetchCachePathsByDomain(domain, path)
123
+ {
124
+ let cacheByDomains = [];
125
+ if(domain && 'default' !== domain){
126
+ cacheByDomains.push({domain: domain});
127
+ return cacheByDomains;
128
+ }
129
+ if(!FileHandler.exists(this.cacheBasePath)){
130
+ return cacheByDomains;
131
+ }
132
+ let cachedDomainFolders = FileHandler.readFolder(this.cacheBasePath);
133
+ for(let cachedDomainFolder of cachedDomainFolders) {
134
+ cacheByDomains.push({domain: cachedDomainFolder});
135
+ }
136
+ return cacheByDomains;
137
+ }
138
+
139
+ async clear()
140
+ {
141
+ if(!FileHandler.exists(this.cacheBasePath)){
142
+ return true;
143
+ }
144
+ if(!FileHandler.remove(this.cacheBasePath)){
145
+ Logger.error('Failed to clear cache folder: '+this.cacheBasePath);
146
+ return false;
147
+ }
148
+ return true;
149
+ }
150
+
151
+ isEnabled()
152
+ {
153
+ return this.enabled;
154
+ }
155
+
156
+ enable()
157
+ {
158
+ this.enabled = true;
159
+ }
160
+
161
+ disable()
162
+ {
163
+ this.enabled = false;
164
+ }
165
+
166
+ }
167
+
168
+ module.exports.CacheManager = CacheManager;
@@ -0,0 +1,99 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - CacheRoutesHandler
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+
9
+ class CacheRoutesHandler
10
+ {
11
+
12
+ constructor(props)
13
+ {
14
+ this.router = sc.get(props, 'router', false);
15
+ this.dataServer = sc.get(props, 'dataServer', false);
16
+ /** @type {CacheManager} **/
17
+ this.cacheManager = sc.get(props, 'cacheManager', false);
18
+ this.rootPath = sc.get(props, 'rootPath', '');
19
+ this.cacheCleanPath = '/cache-clean';
20
+ this.cacheCleanRoute = this.rootPath+this.cacheCleanPath;
21
+ this.clearAllCachePath = '/cache-clear-all';
22
+ this.clearAllCacheRoute = this.rootPath+this.clearAllCachePath;
23
+ this.setupRoutes();
24
+ }
25
+
26
+ setupRoutes()
27
+ {
28
+ if(!this.cacheManager){
29
+ Logger.error('Cache Manager not found on CacheRoutesHandler.');
30
+ return false;
31
+ }
32
+ if(!this.cacheManager.isEnabled()){
33
+ Logger.debug('Cache Manager not enabled.');
34
+ return false;
35
+ }
36
+ if(!this.router){
37
+ Logger.error('Router not found on CacheRoutesHandler.');
38
+ return false;
39
+ }
40
+ this.router.adminRouter.post(
41
+ this.cacheCleanPath,
42
+ this.router.isAuthenticated.bind(this.router),
43
+ async (req, res) => {
44
+ return await this.processCacheClean(req, res);
45
+ }
46
+ );
47
+ this.router.adminRouter.post(
48
+ this.clearAllCachePath,
49
+ this.router.isAuthenticated.bind(this.router),
50
+ async (req, res) => {
51
+ return await this.processClearAllCache(req, res);
52
+ }
53
+ );
54
+ return true;
55
+ }
56
+
57
+ async processCacheClean(req, res)
58
+ {
59
+ if(!this.cacheManager){
60
+ return res.json({error: 'Cache manager not available'});
61
+ }
62
+ let routeId = sc.get(req.body, 'routeId', '');
63
+ if(!routeId){
64
+ return res.json({error: 'Route ID is required'});
65
+ }
66
+ let routesEntity = this.dataServer.getEntity('routes');
67
+ if(!routesEntity){
68
+ return res.json({error: 'Routes entity not found'});
69
+ }
70
+ let route = await routesEntity.loadById(routeId);
71
+ if(!route){
72
+ return res.json({error: 'Route not found'});
73
+ }
74
+ let domain = sc.get(route, 'domain', '');
75
+ let path = route.path;
76
+ let cleanResult = await this.cacheManager.delete(domain, path);
77
+ if(!cleanResult){
78
+ return res.json({error: 'Failed to clean cache'});
79
+ }
80
+ return res.redirect(this.rootPath+'/routes/view'+'?id='+routeId+'&result=success');
81
+ }
82
+
83
+ async processClearAllCache(req, res)
84
+ {
85
+ if(!this.cacheManager){
86
+ return res.json({error: 'Cache manager not available'});
87
+ }
88
+ let clearResult = await this.cacheManager.clear();
89
+ if(!clearResult){
90
+ Logger.error('Failed to clear all cache');
91
+ return res.redirect(this.rootPath+'/routes?result=errorClearAllCache');
92
+ }
93
+ Logger.info('All cache cleared successfully');
94
+ return res.redirect(this.rootPath+'/routes?result=success');
95
+ }
96
+
97
+ }
98
+
99
+ module.exports.CacheRoutesHandler = CacheRoutesHandler;
@@ -26,17 +26,19 @@ class CmsPagesRouteManager
26
26
  async populateViewFields(event)
27
27
  {
28
28
  if('cms_pages' !== event.driverResource.id()){
29
- return;
29
+ return false;
30
+ }
31
+ if(!event.loadedEntity || !event.loadedEntity.route_id){
32
+ event.renderedViewProperties.routePath = '';
33
+ event.renderedViewProperties.routeDomain = '';
34
+ return true;
30
35
  }
31
36
  let routesRepository = this.dataServer.getEntity('routes');
32
37
  if(!routesRepository){
33
38
  Logger.error('Routes repository not found.');
34
39
  return false;
35
40
  }
36
- let existingRoute = await routesRepository.loadOne({
37
- router: 'cmsPages',
38
- cms_page_id: Number(event.renderedViewProperties.id)
39
- });
41
+ let existingRoute = await routesRepository.loadById(event.loadedEntity.route_id);
40
42
  event.renderedViewProperties.routePath = sc.get(existingRoute, 'path', '');
41
43
  event.renderedViewProperties.routeDomain = sc.get(existingRoute, 'domain', '');
42
44
  return true;
@@ -45,17 +47,19 @@ class CmsPagesRouteManager
45
47
  async populateEditFields(event)
46
48
  {
47
49
  if('cms_pages' !== event.driverResource.id()){
48
- return;
50
+ return false;
51
+ }
52
+ if(!event.loadedEntity || !event.loadedEntity.route_id){
53
+ event.renderedEditProperties.routePath = '';
54
+ event.renderedEditProperties.routeDomain = '';
55
+ return true;
49
56
  }
50
57
  let routesRepository = this.dataServer.getEntity('routes');
51
58
  if(!routesRepository){
52
59
  Logger.error('Routes repository not found.');
53
60
  return false;
54
61
  }
55
- let existingRoute = await routesRepository.loadOne({
56
- router: 'cmsPages',
57
- cms_page_id: Number(event.renderedEditProperties.idValue)
58
- });
62
+ let existingRoute = await routesRepository.loadById(event.loadedEntity.route_id);
59
63
  event.renderedEditProperties.routePath = sc.get(existingRoute, 'path', '');
60
64
  event.renderedEditProperties.routeDomain = sc.get(existingRoute, 'domain', '');
61
65
  return true;
@@ -72,28 +76,48 @@ class CmsPagesRouteManager
72
76
  Logger.error('Routes repository not found.');
73
77
  return;
74
78
  }
79
+ let path = sc.get(req.body, 'routePath', this.generateDefaultRoutePath(entityData));
80
+ if(!path || '' === path.trim()){
81
+ Logger.debug('No valid path available for CMS page route creation. Skipping route management.');
82
+ return;
83
+ }
75
84
  let cmsPageId = Number(entityData.id);
76
- let existingRoute = await routesRepository.loadOne({router: 'cmsPages', cms_page_id: cmsPageId});
77
- let patchData = {
78
- path: sc.get(req.body, 'routePath', this.generateDefaultRoutePath(entityData)),
85
+ let routePatchData = {
86
+ path,
79
87
  router: sc.get(req.body, 'routeRouter', 'cmsPages'),
80
- cms_page_id: cmsPageId,
81
88
  cache_ttl_seconds: Number(sc.get(req.body, 'routeCacheTtl', 3600)),
82
89
  enabled: Number(sc.get(req.body, 'routeEnabled', 1)),
83
90
  domain: sc.get(req.body, 'routeDomain', null)
84
91
  };
85
- let result = existingRoute
86
- ? await routesRepository.updateById(existingRoute.id, patchData)
87
- : await routesRepository.create(patchData);
88
- if (!result){
89
- Logger.error('Route could not be saved.', patchData, existingRoute);
92
+ let routeResult = false;
93
+ if(entityData.route_id){
94
+ routeResult = await routesRepository.updateById(entityData.route_id, routePatchData);
95
+ }
96
+ if(!routeResult){
97
+ routeResult = await routesRepository.create(routePatchData);
98
+ if(routeResult){
99
+ let pagesRepository = this.dataServer.getEntity('cmsPages');
100
+ if(pagesRepository){
101
+ let pageResult = await pagesRepository.updateById(cmsPageId, {route_id: routeResult.id});
102
+ if(!pageResult){
103
+ Logger.error('Page could not be updated with route ID.', routeResult);
104
+ }
105
+ }
106
+ }
90
107
  }
91
- return result;
108
+ if(!routeResult){
109
+ Logger.error('Route could not be saved.', routePatchData);
110
+ }
111
+ return routeResult;
92
112
  }
93
113
 
94
114
  generateDefaultRoutePath(pageData)
95
115
  {
96
- return '/' + sc.get(pageData, 'title', 'page').toLowerCase()
116
+ let title = sc.get(pageData, 'title', '');
117
+ if(!title || '' === title.trim()){
118
+ return '';
119
+ }
120
+ return '/' + title.toLowerCase()
97
121
  .replace(/[^a-z0-9\s-]/g, '')
98
122
  .replace(/\s+/g, '-')
99
123
  .replace(/-+/g, '-')