@reldens/cms 0.15.0 → 0.18.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 +150 -34
  2. package/admin/reldens-admin-client.css +39 -23
  3. package/admin/reldens-admin-client.js +7 -0
  4. package/admin/templates/cache-clean-button.html +4 -0
  5. package/admin/templates/edit.html +3 -1
  6. package/admin/templates/fields/view/textarea.html +1 -0
  7. package/admin/templates/sections/editForm/cms-pages.html +15 -0
  8. package/admin/templates/sections/viewForm/cms-pages.html +15 -0
  9. package/admin/templates/view.html +1 -0
  10. package/bin/reldens-cms-generate-entities.js +116 -5
  11. package/bin/reldens-cms.js +26 -8
  12. package/install/js/installer.js +5 -0
  13. package/install/success.html +1 -1
  14. package/lib/admin-manager/contents-builder.js +256 -0
  15. package/lib/admin-manager/router-contents.js +576 -0
  16. package/lib/admin-manager/router.js +208 -0
  17. package/lib/admin-manager.js +114 -944
  18. package/lib/cache/add-cache-button-subscriber.js +101 -0
  19. package/lib/cache/cache-manager.js +129 -0
  20. package/lib/cache/cache-routes-handler.js +76 -0
  21. package/lib/cms-pages-route-manager.js +117 -0
  22. package/lib/frontend.js +207 -64
  23. package/lib/installer.js +44 -20
  24. package/lib/json-fields-parser.js +74 -0
  25. package/lib/manager.js +55 -10
  26. package/lib/template-engine.js +361 -41
  27. package/lib/templates-list.js +10 -0
  28. package/migrations/default-blocks.sql +1 -1
  29. package/migrations/default-entity-access.sql +2 -2
  30. package/migrations/default-homepage.sql +27 -7
  31. package/migrations/install.sql +33 -36
  32. package/package.json +3 -3
  33. package/templates/index.js.dist +3 -3
  34. package/templates/js/scripts.js +5 -0
  35. package/templates/layouts/default.html +4 -4
  36. package/templates/page.html +14 -10
  37. package/templates/partials/footer.html +2 -23
  38. package/templates/partials/header.html +2 -35
@@ -4,8 +4,13 @@
4
4
  *
5
5
  */
6
6
 
7
- const { UploaderFactory, FileHandler } = require('@reldens/server-utils');
8
- const { PageRangeProvider, ValidatorInterface, Logger, sc } = require('@reldens/utils');
7
+ const { UploaderFactory } = require('@reldens/server-utils');
8
+ const { ValidatorInterface, Logger, sc } = require('@reldens/utils');
9
+ const { ContentsBuilder } = require('./admin-manager/contents-builder');
10
+ const { Router } = require('./admin-manager/router');
11
+ const { RouterContents } = require('./admin-manager/router-contents');
12
+ const { CacheRoutesHandler } = require('./cache/cache-routes-handler');
13
+ const { AddCacheButtonSubscriber } = require('./cache/add-cache-button-subscriber');
9
14
 
10
15
  class AdminManager
11
16
  {
@@ -35,6 +40,7 @@ class AdminManager
35
40
  this.autoSyncDistCallback = sc.get(configData, 'autoSyncDistCallback', false);
36
41
  this.branding = sc.get(configData, 'branding', {});
37
42
  this.entities = sc.get(configData, 'entities', {});
43
+ this.cacheManager = sc.get(configData, 'cacheManager', false);
38
44
  this.logoutPath = '/logout';
39
45
  this.loginPath = '/login';
40
46
  this.viewPath = '/view';
@@ -48,8 +54,90 @@ class AdminManager
48
54
  allowedExtensions: this.allowedExtensions,
49
55
  applySecureFileNames: sc.get(configData, 'applySecureFileNames', false)
50
56
  }));
51
- this.adminContents = {};
52
57
  this.blackList = {};
58
+ this.emitEvent = (eventName, eventData = {}) => this.events.emit(eventName, {adminManager: this, ...eventData});
59
+ this.contentsBuilder = new ContentsBuilder({
60
+ renderCallback: this.renderCallback,
61
+ adminFilesContents: this.adminFilesContents,
62
+ stylesFilePath: this.stylesFilePath,
63
+ scriptsFilePath: this.scriptsFilePath,
64
+ rootPath: this.rootPath,
65
+ branding: this.branding,
66
+ translations: this.translations,
67
+ resources: () => this.resources,
68
+ buildAdminCssOnActivation: this.buildAdminCssOnActivation,
69
+ buildAdminScriptsOnActivation: this.buildAdminScriptsOnActivation,
70
+ updateAdminAssetsDistOnActivation: this.updateAdminAssetsDistOnActivation,
71
+ emitEvent: this.emitEvent,
72
+ editPath: this.editPath,
73
+ savePath: this.savePath,
74
+ deletePath: this.deletePath,
75
+ fetchUploadProperties: this.fetchUploadProperties.bind(this),
76
+ fetchTranslation: this.fetchTranslation.bind(this),
77
+ fetchEntityIdPropertyKey: this.fetchEntityIdPropertyKey.bind(this)
78
+ });
79
+ this.router = new Router({
80
+ app: this.app,
81
+ applicationFramework: this.applicationFramework,
82
+ bodyParser: this.bodyParser,
83
+ session: this.session,
84
+ secret: this.secret,
85
+ rootPath: this.rootPath,
86
+ adminRoleId: this.adminRoleId,
87
+ authenticationCallback: this.authenticationCallback,
88
+ uploaderFactory: this.uploaderFactory,
89
+ buckets: this.buckets,
90
+ blackList: this.blackList,
91
+ loginPath: this.loginPath,
92
+ logoutPath: this.logoutPath,
93
+ viewPath: this.viewPath,
94
+ editPath: this.editPath,
95
+ savePath: this.savePath,
96
+ deletePath: this.deletePath,
97
+ resources: () => this.resources,
98
+ emitEvent: this.emitEvent,
99
+ fetchUploadProperties: this.fetchUploadProperties.bind(this),
100
+ adminContents: () => this.contentsBuilder.adminContents,
101
+ generateListRouteContent: (...args) => this.routerContents.generateListRouteContent(...args),
102
+ generateViewRouteContent: (...args) => this.routerContents.generateViewRouteContent(...args),
103
+ generateEditRouteContent: (...args) => this.routerContents.generateEditRouteContent(...args),
104
+ processDeleteEntities: (...args) => this.routerContents.processDeleteEntities(...args),
105
+ processSaveEntity: (...args) => this.routerContents.processSaveEntity(...args)
106
+ });
107
+ this.routerContents = new RouterContents({
108
+ dataServer: this.dataServer,
109
+ translations: this.translations,
110
+ rootPath: this.rootPath,
111
+ relations: () => this.relations,
112
+ resourcesByReference: () => this.resourcesByReference,
113
+ adminFilesContents: this.adminFilesContents,
114
+ autoSyncDistCallback: this.autoSyncDistCallback,
115
+ viewPath: this.viewPath,
116
+ editPath: this.editPath,
117
+ deletePath: this.deletePath,
118
+ emitEvent: (eventName, eventData = {}) => this.events.emit(eventName, {adminManager: this, ...eventData}),
119
+ adminContentsRender: (...args) => this.contentsBuilder.render(...args),
120
+ adminContentsRenderRoute: (...args) => this.contentsBuilder.renderRoute(...args),
121
+ adminContentsEntities: () => this.contentsBuilder.adminContents.entities,
122
+ adminContentsSideBar: () => this.contentsBuilder.adminContents.sideBar,
123
+ fetchTranslation: this.fetchTranslation.bind(this),
124
+ fetchEntityIdPropertyKey: this.fetchEntityIdPropertyKey.bind(this),
125
+ fetchUploadProperties: this.fetchUploadProperties.bind(this)
126
+ });
127
+ this.cacheRoutesHandler = new CacheRoutesHandler({
128
+ router: this.router,
129
+ rootPath: this.rootPath,
130
+ dataServer: this.dataServer,
131
+ cacheManager: this.cacheManager
132
+ });
133
+ this.addCacheButtonSubscriber = new AddCacheButtonSubscriber({
134
+ events: this.events,
135
+ cacheManager: this.cacheManager,
136
+ renderCallback: this.renderCallback,
137
+ cacheCleanButton: this.adminFilesContents.cacheCleanButton,
138
+ translations: this.translations,
139
+ cacheCleanRoute: this.cacheRoutesHandler.cacheCleanRoute
140
+ });
53
141
  }
54
142
 
55
143
  async setupAdmin()
@@ -60,255 +148,17 @@ class AdminManager
60
148
  this.resourcesByReference = {};
61
149
  this.resources = this.prepareResources(this.entities);
62
150
  this.relations = this.prepareRelations(this.entities);
63
- await this.buildAdminContents();
64
- await this.buildAdminScripts();
65
- await this.buildAdminCss();
66
- await this.updateAdminAssets();
67
- this.setupAdminRouter();
151
+ await this.contentsBuilder.buildAdminContents();
152
+ await this.contentsBuilder.buildAdminScripts();
153
+ await this.contentsBuilder.buildAdminCss();
154
+ await this.contentsBuilder.updateAdminAssets();
68
155
  await this.events.emit('reldens.setupAdminRouter', {adminManager: this});
69
- this.setupAdminRoutes();
156
+ this.router.setupAdminRoutes();
70
157
  await this.events.emit('reldens.setupAdminRoutes', {adminManager: this});
71
- await this.setupEntitiesRoutes();
158
+ await this.router.setupEntitiesRoutes();
72
159
  await this.events.emit('reldens.setupAdminManagers', {adminManager: this});
73
160
  }
74
161
 
75
- async buildAdminContents()
76
- {
77
- this.adminContents.layout = await this.buildLayout();
78
- this.adminContents.sideBar = await this.buildSideBar();
79
- this.adminContents.login = await this.buildLogin();
80
- this.adminContents.dashboard = await this.buildDashboard();
81
- this.adminContents.entities = await this.buildEntitiesContents();
82
- this.events.emit('reldens.buildAdminContentsAfter', {adminManager: this});
83
- }
84
-
85
- async buildLayout()
86
- {
87
- return await this.render(
88
- this.adminFilesContents.layout,
89
- {
90
- sideBar: '{{&sideBar}}',
91
- pageContent: '{{&pageContent}}',
92
- stylesFilePath: this.stylesFilePath,
93
- scriptsFilePath: this.scriptsFilePath,
94
- rootPath: this.rootPath,
95
- brandingCompanyName: this.branding.companyName,
96
- copyRight: this.branding.copyRight
97
- }
98
- );
99
- }
100
-
101
- async buildSideBar()
102
- {
103
- let navigationContents = {};
104
- let eventBuildSideBarBefore = {navigationContents, adminManager: this};
105
- await this.events.emit('reldens.eventBuildSideBarBefore', eventBuildSideBarBefore);
106
- navigationContents = eventBuildSideBarBefore.navigationContents;
107
- for(let driverResource of this.resources){
108
- let navigation = driverResource.options?.navigation;
109
- let name = this.translations.labels[driverResource.id()] || this.translations.labels[driverResource.entityKey];
110
- let path = this.rootPath+'/'+(driverResource.id().replace(/_/g, '-'));
111
- if(navigation?.name){
112
- if(!navigationContents[navigation.name]){
113
- navigationContents[navigation.name] = {};
114
- }
115
- navigationContents[navigation.name][driverResource.id()] = await this.render(
116
- this.adminFilesContents.sideBarItem,
117
- {name, path}
118
- );
119
- continue;
120
- }
121
- navigationContents[driverResource.id()] = await this.render(
122
- this.adminFilesContents.sideBarItem,
123
- {name, path}
124
- );
125
- }
126
- let eventAdminSideBarBeforeSubItems = {navigationContents, adminManager: this};
127
- await this.events.emit('reldens.adminSideBarBeforeSubItems', eventAdminSideBarBeforeSubItems);
128
- let navigationView = '';
129
- for(let id of Object.keys(navigationContents)){
130
- if(sc.isObject(navigationContents[id])){
131
- let subItems = '';
132
- for(let subId of Object.keys(navigationContents[id])){
133
- subItems += navigationContents[id][subId];
134
- }
135
- navigationView += await this.render(
136
- this.adminFilesContents.sideBarHeader,
137
- {name: id, subItems}
138
- );
139
- continue;
140
- }
141
- navigationView += navigationContents[id];
142
- }
143
- let eventAdminSideBarBeforeRender = {navigationContents, navigationView, adminManager: this};
144
- await this.events.emit('reldens.adminSideBarBeforeRender', eventAdminSideBarBeforeRender);
145
- return await this.render(
146
- this.adminFilesContents.sideBar,
147
- {
148
- rootPath: this.rootPath,
149
- navigationView: eventAdminSideBarBeforeRender.navigationView
150
- }
151
- );
152
- }
153
-
154
- async buildLogin()
155
- {
156
- return await this.renderRoute(this.adminFilesContents.login, '');
157
- }
158
-
159
- async buildDashboard()
160
- {
161
- return await this.renderRoute(this.adminFilesContents.dashboard, this.adminContents.sideBar);
162
- }
163
-
164
- async buildEntitiesContents()
165
- {
166
- let entitiesContents = {};
167
- for(let driverResource of this.resources){
168
- let templateTitle = this.translations.labels[driverResource.id()];
169
- let entityName = (driverResource.id().replace(/_/g, '-'));
170
- let entityListRoute = this.rootPath+'/'+entityName;
171
- let entityEditRoute = entityListRoute+this.editPath;
172
- let entitySaveRoute = entityListRoute+this.savePath;
173
- let entityDeleteRoute = entityListRoute+this.deletePath;
174
- let uploadProperties = this.fetchUploadProperties(driverResource);
175
- let multipartFormData = 0 < Object.keys(uploadProperties).length ? ' enctype="multipart/form-data"' : '';
176
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
177
- let editProperties = Object.keys(driverResource.options.properties);
178
- editProperties.splice(editProperties.indexOf(idProperty), 1);
179
- let filters = driverResource.options.filterProperties.map((property) => {
180
- return {
181
- propertyKey: property,
182
- name: this.fetchTranslation(property),
183
- value: '{{&'+property+'}}'
184
- };
185
- });
186
- let fields = driverResource.options.showProperties.map((property) => {
187
- return {
188
- name: this.fetchTranslation(property),
189
- value: '{{&'+property+'}}'
190
- };
191
- });
192
- let editFields = editProperties.map((property) => {
193
- return {
194
- name: this.fetchTranslation(property),
195
- value: '{{&'+property+'}}'
196
- };
197
- });
198
- let extraContentForList = sc.get(this.adminFilesContents?.sections?.list, driverResource.entityPath, '');
199
- let extraContentForView = await this.render(
200
- sc.get(this.adminFilesContents?.sections?.view, driverResource.entityPath, ''),
201
- {
202
- id: '{{&id}}',
203
- entitySerializedData: '{{&entitySerializedData}}'
204
- }
205
- );
206
- let extraContentForEdit = sc.get(this.adminFilesContents?.sections?.edit, driverResource.entityPath, '');
207
- entitiesContents[entityName] = {
208
- list: await this.render(
209
- this.adminFilesContents.list,
210
- {
211
- entityName,
212
- templateTitle,
213
- entityListRoute,
214
- entityEditRoute,
215
- filters,
216
- list: '{{&list}}',
217
- pagination: '{{&pagination}}',
218
- extraContent: extraContentForList,
219
- }
220
- ),
221
- view: await this.render(
222
- this.adminFilesContents.view,
223
- {
224
- entityName,
225
- templateTitle,
226
- entityDeleteRoute,
227
- entityListRoute,
228
- fields,
229
- id: '{{&id}}',
230
- entityEditRoute: '{{&entityEditRoute}}',
231
- entityNewRoute: '{{&entityNewRoute}}',
232
- extraContent: extraContentForView,
233
- }
234
- ),
235
- edit: await this.render(
236
- this.adminFilesContents.edit,
237
- {
238
- entityName,
239
- entitySaveRoute,
240
- multipartFormData,
241
- editFields,
242
- idValue: '{{&idValue}}',
243
- idProperty: '{{&idProperty}}',
244
- templateTitle: '{{&templateTitle}}',
245
- entityViewRoute: '{{&entityViewRoute}}',
246
- extraContent: extraContentForEdit,
247
- }
248
- )
249
- };
250
- }
251
- return entitiesContents;
252
- }
253
-
254
- fetchUploadProperties(driverResource)
255
- {
256
- if(!driverResource.options.uploadProperties){
257
- driverResource.options.uploadProperties = {};
258
- for(let propertyKey of Object.keys(driverResource.options.properties)){
259
- let property = driverResource.options.properties[propertyKey];
260
- if(property.isUpload){
261
- driverResource.options.uploadProperties[propertyKey] = property;
262
- }
263
- }
264
- }
265
- return driverResource.options.uploadProperties;
266
- }
267
-
268
- async render(content, params)
269
- {
270
- return await this.renderCallback(content, params);
271
- }
272
-
273
- async renderRoute(pageContent, sideBar)
274
- {
275
- return await this.render(
276
- this.adminContents.layout,
277
- {
278
- stylesFilePath: this.stylesFilePath,
279
- scriptsFilePath: this.scriptsFilePath,
280
- brandingCompanyName: this.branding.companyName,
281
- copyRight: this.branding.copyRight,
282
- pageContent,
283
- sideBar
284
- }
285
- );
286
- }
287
-
288
- async buildAdminScripts()
289
- {
290
- if(!sc.isFunction(this.buildAdminScriptsOnActivation)){
291
- return false;
292
- }
293
- return this.buildAdminScriptsOnActivation();
294
- }
295
-
296
- async updateAdminAssets()
297
- {
298
- if(!sc.isFunction(this.updateAdminAssetsDistOnActivation)){
299
- return false;
300
- }
301
- return this.updateAdminAssetsDistOnActivation();
302
- }
303
-
304
- async buildAdminCss()
305
- {
306
- if(!sc.isFunction(this.buildAdminCssOnActivation)){
307
- return false;
308
- }
309
- return this.buildAdminCssOnActivation();
310
- }
311
-
312
162
  prepareResources(rawResources)
313
163
  {
314
164
  let rawResourcesKeys = Object.keys(rawResources);
@@ -319,7 +169,6 @@ class AdminManager
319
169
  for(let i of rawResourcesKeys){
320
170
  let rawResource = rawResources[i];
321
171
  let tableName = rawResource.rawEntity.tableName();
322
- // @TODO - BETA - Refactor to add the ID property and composed labels (id + label), in the resource.
323
172
  let driverResource = {
324
173
  id: () => {
325
174
  return tableName;
@@ -350,7 +199,6 @@ class AdminManager
350
199
 
351
200
  prepareRelations()
352
201
  {
353
- // @TODO - BETA - Refactor, include in resources generation at once.
354
202
  let registeredRelations = {};
355
203
  for(let resource of this.resources){
356
204
  for(let propertyKey of Object.keys(resource.options.properties)){
@@ -375,645 +223,18 @@ class AdminManager
375
223
  return registeredRelations;
376
224
  }
377
225
 
378
- setupAdminRouter()
379
- {
380
- this.adminRouter = this.applicationFramework.Router();
381
- // apply session middleware only to /admin routes:
382
- if(this.session){
383
- if(!this.secret){
384
- Logger.warning('Admin Manager "secret" key was not provided.');
385
- }
386
- this.adminRouter.use(this.session({secret: this.secret, resave: false, saveUninitialized: true}));
387
- }
388
- this.adminRouter.use(this.bodyParser.json());
389
- }
390
-
391
- setupAdminRoutes()
392
- {
393
- this.adminRouter.get(this.loginPath, async (req, res) => {
394
- return res.send(this.adminContents.login);
395
- });
396
- // route for handling login:
397
- this.adminRouter.post(this.loginPath, async (req, res) => {
398
- let { email, password } = req.body;
399
- let loginResult = await this.authenticationCallback(email, password, this.adminRoleId);
400
- if(loginResult){
401
- req.session.user = loginResult;
402
- return res.redirect(this.rootPath);
403
- }
404
- return res.redirect(this.rootPath+this.loginPath+'?login-error=true');
405
- });
406
- // route for the admin panel dashboard:
407
- this.adminRouter.get('/', this.isAuthenticated.bind(this), async (req, res) => {
408
- return res.send(this.adminContents.dashboard);
409
- });
410
- // route for logout:
411
- this.adminRouter.get(this.logoutPath, (req, res) => {
412
- req.session.destroy();
413
- res.redirect(this.rootPath+this.loginPath);
414
- });
415
- this.app.use(this.rootPath, this.adminRouter);
416
- }
417
-
418
- async setupEntitiesRoutes()
419
- {
420
- if(!this.resources || 0 === this.resources.length){
421
- return;
422
- }
423
- for(let driverResource of this.resources){
424
- let entityPath = driverResource.entityPath;
425
- let entityRoute = '/'+entityPath;
426
- this.adminRouter.get(entityRoute, this.isAuthenticated.bind(this), async (req, res) => {
427
- let routeContents = await this.generateListRouteContent(req, driverResource, entityPath);
428
- return res.send(routeContents);
429
- });
430
- this.adminRouter.post(entityRoute, this.isAuthenticated.bind(this), async (req, res) => {
431
- let routeContents = await this.generateListRouteContent(req, driverResource, entityPath);
432
- return res.send(routeContents);
433
- });
434
- this.adminRouter.get(entityRoute+this.viewPath, this.isAuthenticated.bind(this), async (req, res) => {
435
- let routeContents = await this.generateViewRouteContent(req, driverResource, entityPath);
436
- if('' === routeContents){
437
- return res.redirect(this.rootPath+'/'+entityPath+'?result=errorView');
438
- }
439
- return res.send(routeContents);
440
- });
441
- this.adminRouter.get(entityRoute+this.editPath, this.isAuthenticated.bind(this), async (req, res) => {
442
- let routeContents = await this.generateEditRouteContent(req, driverResource, entityPath);
443
- if('' === routeContents){
444
- return res.redirect(this.rootPath+'/'+entityPath+'?result=errorEdit');
445
- }
446
- return res.send(routeContents);
447
- });
448
- this.setupSavePath(entityRoute, driverResource, entityPath);
449
- this.adminRouter.post(entityRoute+this.deletePath, this.isAuthenticated.bind(this), async (req, res) => {
450
- let redirectResult = await this.processDeleteEntities(req, res, driverResource, entityPath);
451
- return res.redirect(redirectResult);
452
- });
453
- await this.events.emit('reldens.setupEntitiesRoutes', {
454
- adminManager: this,
455
- entityPath,
456
- entityRoute,
457
- driverResource
458
- });
459
- }
460
- }
461
-
462
- setupSavePath(entityRoute, driverResource, entityPath)
463
- {
464
- let uploadProperties = this.fetchUploadProperties(driverResource);
465
- let uploadPropertiesKeys = Object.keys(uploadProperties || {});
466
- if(0 === uploadPropertiesKeys.length){
467
- this.adminRouter.post(
468
- entityRoute+this.savePath,
469
- this.isAuthenticated.bind(this),
470
- async (req, res) => {
471
- let redirectResult = await this.processSaveEntity(req, res, driverResource, entityPath);
472
- return res.redirect(redirectResult);
473
- }
474
- );
475
- return;
476
- }
477
- let fields = [];
478
- let allowedFileTypes = {};
479
- for(let uploadPropertyKey of uploadPropertiesKeys){
480
- let property = uploadProperties[uploadPropertyKey];
481
- allowedFileTypes[uploadPropertyKey] = property.allowedTypes || false;
482
- let field = {name: uploadPropertyKey};
483
- if(!property.isArray){
484
- field.maxCount = 1;
485
- }
486
- fields.push(field);
487
- this.buckets[uploadPropertyKey] = property.bucket;
488
- }
489
- this.adminRouter.post(
490
- entityRoute + this.savePath,
491
- this.isAuthenticated.bind(this),
492
- this.uploaderFactory.createUploader(fields, this.buckets, allowedFileTypes),
493
- async (req, res) => {
494
- let redirectResult = await this.processSaveEntity(req, res, driverResource, entityPath);
495
- return res.redirect(redirectResult);
496
- }
497
- );
498
- }
499
-
500
- async processDeleteEntities(req, res, driverResource, entityPath)
501
- {
502
- let ids = req?.body?.ids;
503
- if('string' === typeof ids){
504
- ids = ids.split(',');
505
- }
506
- let redirectPath = this.rootPath+'/'+entityPath+'?result=';
507
- let resultString = 'errorMissingId';
508
- if(!ids || 0 === ids.length){
509
- return redirectPath + resultString;
510
- }
511
- try {
512
- let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
513
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
514
- let idsFilter = {[idProperty]: {operator: 'IN', value: ids}};
515
- let loadedEntities = await entityRepository.load(idsFilter);
516
- await this.deleteEntitiesRelatedFiles(driverResource, loadedEntities);
517
- let deleteResult = await entityRepository.delete(idsFilter);
518
- resultString = deleteResult ? 'success' : 'errorStorageFailure';
519
- } catch (error) {
520
- resultString = 'errorDeleteFailure';
521
- }
522
- return redirectPath + resultString;
523
- }
524
-
525
- async deleteEntitiesRelatedFiles(driverResource, entities)
526
- {
527
- let resourcePropertiesKeys = Object.keys(driverResource.options.properties);
528
- for(let propertyKey of resourcePropertiesKeys){
529
- let property = driverResource.options.properties[propertyKey];
530
- if(!property.isUpload){
531
- continue;
532
- }
533
- for(let entity of entities){
534
- if(!property.isArray){
535
- FileHandler.remove([(property.bucket || ''), entity[propertyKey]]);
536
- continue;
537
- }
538
- let entityFiles = entity[propertyKey].split(property.isArray);
539
- for(let entityFile of entityFiles){
540
- FileHandler.remove([(property.bucket || ''), entityFile]);
541
- }
542
- }
543
- }
544
- }
545
-
546
- async processSaveEntity(req, res, driverResource, entityPath)
547
- {
548
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
549
- let id = (req?.body[idProperty] || '').toString();
550
- let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
551
- let resourceProperties = driverResource.options.properties;
552
- let entityDataPatch = this.preparePatchData(driverResource, idProperty, req, resourceProperties, id);
553
- if(!entityDataPatch){
554
- Logger.error('Bad patch data.', entityDataPatch);
555
- return this.rootPath+'/'+entityPath+'?result=saveBadPatchData';
556
- }
557
- let editRoute = this.generateEntityRoute('editPath', driverResource, idProperty);
558
- try {
559
- let saveResult = await this.saveEntity(id, entityRepository, entityDataPatch);
560
- if(!saveResult){
561
- Logger.error('Save result error.', saveResult, entityDataPatch);
562
- return editRoute+'?result=saveEntityStorageError';
563
- }
564
- if(sc.isFunction(this.autoSyncDistCallback)){
565
- let uploadProperties = this.fetchUploadProperties(driverResource);
566
- if(0 < Object.keys(uploadProperties).length){
567
- for(let uploadPropertyKey of Object.keys(uploadProperties)){
568
- let property = uploadProperties[uploadPropertyKey];
569
- await this.autoSyncDistCallback(
570
- property.bucket,
571
- saveResult[uploadPropertyKey],
572
- property.distFolder
573
- );
574
- }
575
- }
576
- }
577
- return this.generateEntityRoute('viewPath', driverResource, idProperty, saveResult) +'&result=success';
578
- } catch (error) {
579
- Logger.error('Save entity error.', error);
580
- return this.rootPath+'/'+entityPath+'?result=saveEntityError';
581
- }
582
- }
583
-
584
- async saveEntity(id, entityRepository, entityDataPatch)
585
- {
586
- if('' === id){
587
- return entityRepository.create(entityDataPatch);
588
- }
589
- return entityRepository.updateById(id, entityDataPatch);
590
- }
591
-
592
- preparePatchData(driverResource, idProperty, req, resourceProperties, id)
593
- {
594
- let entityDataPatch = {};
595
- for(let i of driverResource.options.editProperties){
596
- if(i === idProperty){
597
- continue;
598
- }
599
- let propertyUpdateValue = sc.get(req.body, i, null);
600
- let property = resourceProperties[i];
601
- let isNullValue = null === propertyUpdateValue;
602
- let propertyType = property.type || 'string';
603
- if(property.isUpload){
604
- propertyType = 'upload';
605
- propertyUpdateValue = this.prepareUploadPatchData(req, i, propertyUpdateValue, property);
606
- }
607
- if('number' === propertyType && !isNullValue){
608
- propertyUpdateValue = Number(propertyUpdateValue);
609
- }
610
- if('string' === propertyType && !isNullValue){
611
- propertyUpdateValue = String(propertyUpdateValue);
612
- }
613
- if('boolean' === propertyType){
614
- propertyUpdateValue = Boolean(propertyUpdateValue);
615
- }
616
- let isUploadCreate = property.isUpload && !id;
617
- if(property.isRequired && null === propertyUpdateValue && (!property.isUpload || isUploadCreate)){
618
- // missing required fields would break the update:
619
- Logger.critical('Bad patch data on update.', propertyUpdateValue, property);
620
- return false;
621
- }
622
- if(!property.isUpload || (property.isUpload && null !== propertyUpdateValue)){
623
- entityDataPatch[i] = propertyUpdateValue;
624
- }
625
- }
626
- return entityDataPatch;
627
- }
628
-
629
- prepareUploadPatchData(req, i, propertyUpdateValue, property)
630
- {
631
- let filesData = sc.get(req.files, i, null);
632
- if(null === filesData){
633
- return null;
634
- }
635
- let fileNames = [];
636
- for(let file of filesData){
637
- fileNames.push(file.filename);
638
- }
639
- return fileNames.join(property.isArray);
640
- }
641
-
642
- formatDateTimeForEdit(dateValue, isDisabled)
643
- {
644
- if(!dateValue || '' === dateValue){
645
- return '';
646
- }
647
- let date = new Date(dateValue);
648
- if(isNaN(date.getTime())){
649
- return '';
650
- }
651
- if(isDisabled){
652
- return sc.formatDate(date);
653
- }
654
- let year = date.getFullYear();
655
- let month = String(date.getMonth() + 1).padStart(2, '0');
656
- let day = String(date.getDate()).padStart(2, '0');
657
- let hours = String(date.getHours()).padStart(2, '0');
658
- let minutes = String(date.getMinutes()).padStart(2, '0');
659
- return year+'-'+month+'-'+day+'T'+hours+':'+minutes;
660
- }
661
-
662
- async generateEditRouteContent(req, driverResource, entityPath)
663
- {
664
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
665
- let idValue = String(sc.get(req?.query, idProperty, ''));
666
- let templateTitle = (!idValue ? 'Create' : 'Edit')+' '+this.translations.labels[driverResource.id()];
667
- let loadedEntity = !idValue ? null :await this.loadEntityById(driverResource, idValue);
668
- let entityViewRoute = !idValue
669
- ? this.rootPath+'/'+driverResource.entityPath
670
- : this.generateEntityRoute('viewPath', driverResource, idProperty, loadedEntity);
671
- let renderedEditProperties = {
672
- idValue,
673
- idProperty,
674
- idPropertyLabel: this.fetchTranslation(idProperty),
675
- templateTitle,
676
- entityViewRoute
677
- };
678
- let propertiesKeys = Object.keys(driverResource.options.properties);
679
- for(let propertyKey of propertiesKeys){
680
- let property = driverResource.options.properties[propertyKey];
681
- let fieldDisabled = -1 === driverResource.options.editProperties.indexOf(propertyKey);
682
- renderedEditProperties[propertyKey] = await this.render(
683
- this.adminFilesContents.fields.edit[this.propertyType(property, 'edit')],
684
- {
685
- fieldName: propertyKey,
686
- fieldValue: await this.generatePropertyEditRenderedValue(
687
- loadedEntity,
688
- propertyKey,
689
- property,
690
- fieldDisabled
691
- ),
692
- fieldDisabled: fieldDisabled ? ' disabled="disabled"' : '',
693
- required: (!property.isUpload || !loadedEntity) && property.isRequired ? ' required="required"' : '',
694
- multiple: property.isArray ? ' multiple="multiple"' : '',
695
- inputType: this.getInputType(property, fieldDisabled)
696
- }
697
- );
698
- }
699
- return await this.renderRoute(
700
- await this.render(this.adminContents.entities[entityPath].edit, renderedEditProperties),
701
- this.adminContents.sideBar
702
- );
703
- }
704
-
705
- getInputType(resourceProperty, fieldDisabled)
706
- {
707
- if('datetime' === resourceProperty.type && !fieldDisabled){
708
- return 'datetime-local';
709
- }
710
- if('number' === resourceProperty.type){
711
- return 'number';
712
- }
713
- return 'text';
714
- }
715
-
716
- async loadEntityById(driverResource, id)
717
- {
718
- let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
719
- if(!entityRepository){
720
- return false;
721
- }
722
- return await entityRepository.loadByIdWithRelations(id);
723
- }
724
-
725
- async generateViewRouteContent(req, driverResource, entityPath)
726
- {
727
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
728
- let id = (sc.get(req.query, idProperty, '')).toString();
729
- if('' === id){
730
- Logger.error('Missing ID on view route.', entityPath, id, idProperty);
731
- return '';
732
- }
733
- let loadedEntity = await this.loadEntityById(driverResource, id);
734
- let renderedViewProperties = {
735
- entityEditRoute: this.generateEntityRoute('editPath', driverResource, idProperty, loadedEntity),
736
- entityNewRoute: this.generateEntityRoute('editPath', driverResource, idProperty),
737
- id
738
- };
739
- let entitySerializedData = {};
740
- for(let propertyKey of driverResource.options.showProperties){
741
- let property = driverResource.options.properties[propertyKey];
742
- let {fieldValue, fieldName} = this.generatePropertyRenderedValueWithLabel(
743
- loadedEntity,
744
- propertyKey,
745
- property
746
- );
747
- entitySerializedData[fieldName] = fieldValue;
748
- renderedViewProperties[propertyKey] = await this.render(
749
- this.adminFilesContents.fields.view[this.propertyType(property)],
750
- {
751
- fieldName: propertyKey,
752
- fieldValue: await this.generatePropertyRenderedValue(
753
- fieldValue,
754
- fieldName,
755
- property,
756
- 'view'
757
- ),
758
- fieldOriginalValue: fieldValue,
759
- target: ' target="_blank"'
760
- }
761
- );
762
- }
763
- let extraDataEvent = {entitySerializedData, entityId: driverResource.id(), entity: loadedEntity};
764
- await this.events.emit('adminEntityExtraData', extraDataEvent);
765
- entitySerializedData = extraDataEvent.entitySerializedData;
766
- renderedViewProperties.entitySerializedData = JSON.stringify(entitySerializedData).replace(/"/g, '&quot;');
767
- return await this.renderRoute(
768
- await this.render(this.adminContents.entities[entityPath].view, renderedViewProperties),
769
- this.adminContents.sideBar
770
- );
771
- }
772
-
773
- propertyType(resourceProperty, templateType)
774
- {
775
- let propertyType = sc.get(resourceProperty, 'type', 'text');
776
- if('reference' === propertyType && 'edit' === templateType){
777
- return 'select';
778
- }
779
- if(resourceProperty.isUpload){
780
- if('edit' === templateType){
781
- return 'file';
782
- }
783
- if('view' === templateType){
784
- let multiple = resourceProperty.isArray ? 's' : '';
785
- if('image' === resourceProperty.allowedTypes){
786
- return resourceProperty.allowedTypes + multiple;
787
- }
788
- if('text' === resourceProperty.allowedTypes){
789
- return 'link'+multiple
790
- }
791
- return 'text';
792
- }
793
- }
794
- if('textarea' === propertyType){
795
- if('edit' === templateType){
796
- return 'textarea';
797
- }
798
- return 'text';
799
- }
800
- if(-1 !== ['reference', 'number', 'datetime'].indexOf(propertyType)){
801
- propertyType = 'text';
802
- }
803
- return propertyType;
804
- }
805
-
806
- async generateListRouteContent(req, driverResource, entityPath)
807
- {
808
- let page = Number(req?.query?.page || 1);
809
- let pageSize = Number(req?.query?.pageSize || 25);
810
- let filtersFromParams = req?.body?.filters || {};
811
- let filters = this.prepareFilters(filtersFromParams, driverResource);
812
- let mappedFiltersValues = driverResource.options.filterProperties.map((property) => {
813
- let filterValue = (filtersFromParams[property] || '').toString();
814
- return {[property]: '' === filterValue ? '' : 'value="'+filterValue+'"'};
815
- });
816
- let entitiesRows = await this.loadEntitiesForList(driverResource, pageSize, page, req, filters);
817
- let listRawContent = this.adminContents.entities[entityPath].list.toString();
818
- let totalEntities = await this.countTotalEntities(driverResource, filters);
819
- let totalPages = totalEntities <= pageSize ? 1 : Math.ceil(totalEntities / pageSize);
820
- let pages = PageRangeProvider.fetch(page, totalPages);
821
- let renderedPagination = '';
822
- for(let page of pages){
823
- renderedPagination += await this.render(
824
- this.adminFilesContents.fields.view['link'],
825
- {
826
- fieldName: page.label,
827
- fieldValue: this.rootPath+'/'+driverResource.entityPath+'?page='+ page.value,
828
- fieldOriginalValue: page.value,
829
- }
830
- );
831
- }
832
- let listVars = {
833
- deletePath: this.rootPath + '/' + driverResource.entityPath + this.deletePath,
834
- fieldsHeaders: driverResource.options.listProperties.map((property) => {
835
- let propertyTitle = this.fetchTranslation(property, driverResource.id());
836
- let alias = this.fetchTranslation(
837
- driverResource.options.properties[property]?.alias || '',
838
- driverResource.id()
839
- );
840
- let title = '' !== alias ? alias + ' ('+propertyTitle+')' : propertyTitle;
841
- return {name: property, value: title};
842
- }),
843
- rows: entitiesRows
844
- };
845
- let list = await this.render(this.adminFilesContents.listContent, listVars);
846
- let entitiesListView = await this.render(
847
- listRawContent,
848
- Object.assign({list, pagination: renderedPagination}, ...mappedFiltersValues)
849
- );
850
- return await this.renderRoute(entitiesListView, this.adminContents.sideBar);
851
- }
852
-
853
- fetchTranslation(snippet, group)
854
- {
855
- if('' === snippet){
856
- return snippet;
857
- }
858
- let translationGroup = sc.get(this.translations, group);
859
- if(translationGroup){
860
- let translationByGroup = sc.get(translationGroup, snippet, '');
861
- if('' !== translationByGroup){
862
- return translationByGroup;
863
- }
864
- }
865
- return sc.get(this.translations, snippet, snippet);
866
- }
867
-
868
- async countTotalEntities(driverResource, filters)
869
- {
870
- /** @type {BaseDriver|ObjectionJsDriver} entityRepository **/
871
- let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
872
- if(!entityRepository){
873
- return false;
874
- }
875
- return await entityRepository.count(filters);
876
- }
877
-
878
- async loadEntitiesForList(driverResource, pageSize, page, req, filters)
879
- {
880
- let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
881
- entityRepository.limit = pageSize;
882
- if(1 < page){
883
- entityRepository.offset = (page - 1) * pageSize;
884
- }
885
- entityRepository.sortBy = req?.body?.sortBy || false;
886
- entityRepository.sortDirection = req?.body?.sortDirection || false;
887
- let loadedEntities = await entityRepository.loadWithRelations(filters, []);
888
- entityRepository.limit = 0;
889
- entityRepository.offset = 0;
890
- entityRepository.sortBy = false;
891
- entityRepository.sortDirection = false;
892
- let entityRows = [];
893
- let deleteLink = this.rootPath + '/' + driverResource.entityPath + this.deletePath;
894
- for(let entity of loadedEntities){
895
- let entityRow = {fields: []};
896
- let resourceProperties = driverResource.options?.properties;
897
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
898
- let viewLink = '';
899
- let editLink = '';
900
- if('' !== idProperty){
901
- viewLink = this.generateEntityRoute('viewPath', driverResource, idProperty, entity);
902
- editLink = this.generateEntityRoute('editPath', driverResource, idProperty, entity);
903
- }
904
- for(let property of driverResource.options.listProperties){
905
- let {fieldValue, fieldName} = this.generatePropertyRenderedValueWithLabel(
906
- entity,
907
- property,
908
- resourceProperties[property]
909
- );
910
- let value = await this.generatePropertyRenderedValue(
911
- fieldValue,
912
- fieldName,
913
- resourceProperties[property]
914
- );
915
- entityRow.fields.push({
916
- name: property,
917
- value,
918
- viewLink
919
- });
920
- }
921
- entityRow.editLink = editLink;
922
- entityRow.deleteLink = deleteLink;
923
- entityRow.id = entity[idProperty];
924
- entityRows.push(entityRow);
925
- }
926
- return entityRows;
927
- }
928
-
929
- async generatePropertyRenderedValue(fieldValue, fieldName, resourceProperty, templateType)
930
- {
931
- let fieldOriginalValue = fieldValue;
932
- if('view' === templateType){
933
- if(resourceProperty.isArray){
934
- fieldValue = fieldValue.split(resourceProperty.isArray).map((value) => {
935
- let target = resourceProperty.isUpload ? ' target="_blank"' : '';
936
- let fieldValuePart = resourceProperty.isUpload && resourceProperty.bucketPath
937
- ? resourceProperty.bucketPath+value
938
- : value;
939
- return {fieldValuePart, fieldOriginalValuePart: value, target};
940
- });
941
- }
942
- if(!resourceProperty.isArray && resourceProperty.isUpload){
943
- fieldValue = resourceProperty.bucketPath+fieldValue;
944
- }
945
- }
946
- return await this.render(
947
- this.adminFilesContents.fields.view[this.propertyType(resourceProperty, templateType)],
948
- {fieldName, fieldValue, fieldOriginalValue, target: ' target="_blank"'}
949
- );
950
- }
951
-
952
- generatePropertyRenderedValueWithLabel(entity, propertyKey, resourceProperty)
226
+ fetchUploadProperties(driverResource)
953
227
  {
954
- let fieldValue = (0 === entity[propertyKey] ? '0' : entity[propertyKey] || '').toString();
955
- let fieldName = propertyKey;
956
- if('boolean' === resourceProperty.type){
957
- fieldValue = '1' === fieldValue || 'true' === fieldValue ? 'Yes' : 'No';
958
- }
959
- if('datetime' === resourceProperty.type){
960
- fieldValue = '' !== fieldValue ? sc.formatDate(new Date(fieldValue)) : '';
961
- }
962
- if('reference' === resourceProperty.type){
963
- let relationKey = resourceProperty.alias || resourceProperty.reference;
964
- let relationEntity = entity[relationKey];
965
- if(relationEntity){
966
- let relation = this.relations[resourceProperty.reference];
967
- if(relation){
968
- let relationTitleProperty = relation[relationKey];
969
- if(relationTitleProperty && '' !== String(relationEntity[relationTitleProperty] || '')){
970
- fieldName = relationTitleProperty;
971
- fieldValue = relationEntity[relationTitleProperty]+(' ('+fieldValue+')');
972
- }
228
+ if(!driverResource.options.uploadProperties){
229
+ driverResource.options.uploadProperties = {};
230
+ for(let propertyKey of Object.keys(driverResource.options.properties)){
231
+ let property = driverResource.options.properties[propertyKey];
232
+ if(property.isUpload){
233
+ driverResource.options.uploadProperties[propertyKey] = property;
973
234
  }
974
235
  }
975
236
  }
976
- if(resourceProperty.availableValues){
977
- let optionData = resourceProperty.availableValues.filter((availableValue) => {
978
- return String(availableValue.value) === String(fieldValue);
979
- }).shift();
980
- if(optionData){
981
- fieldValue = optionData.label + ' (' + fieldValue + ')';
982
- }
983
- }
984
- return {fieldValue, fieldName};
985
- }
986
-
987
- async generatePropertyEditRenderedValue(entity, propertyKey, resourceProperty, isFieldDisabled)
988
- {
989
- let entityPropertyValue = sc.get(entity, propertyKey, null);
990
- let fieldValue = (0 === entityPropertyValue ? '0' : entityPropertyValue || '').toString();
991
- if('boolean' === resourceProperty.type){
992
- fieldValue = '1' === fieldValue || 'true' === fieldValue ? ' checked="checked"' : '';
993
- }
994
- if('datetime' === resourceProperty.type){
995
- fieldValue = this.formatDateTimeForEdit(entityPropertyValue, isFieldDisabled);
996
- }
997
- if('reference' === resourceProperty.type){
998
- let relationDriverResource = this.resourcesByReference[resourceProperty.reference];
999
- let relation = this.relations[resourceProperty.reference];
1000
- let relationKey = resourceProperty.alias || resourceProperty.reference;
1001
- let idProperty = this.fetchEntityIdPropertyKey(relationDriverResource);
1002
- let relationTitleProperty = relation ? relation[relationKey] : idProperty;
1003
- let relationOptions = await this.fetchRelationOptions(relationDriverResource);
1004
- return relationOptions.map((option) => {
1005
- let value = option[idProperty];
1006
- let selected = entity && entity[propertyKey] === value ? ' selected="selected"' : '';
1007
- return {label: option[relationTitleProperty]+' (ID: '+value+')', value, selected}
1008
- });
1009
- }
1010
- return fieldValue;
1011
- }
1012
-
1013
- async fetchRelationOptions(relationDriverResource)
1014
- {
1015
- let relationEntityRepository = this.dataServer.getEntity(relationDriverResource.entityKey);
1016
- return await relationEntityRepository.loadAll();
237
+ return driverResource.options.uploadProperties;
1017
238
  }
1018
239
 
1019
240
  fetchEntityIdPropertyKey(driverResource)
@@ -1036,70 +257,19 @@ class AdminManager
1036
257
  return idProperty;
1037
258
  }
1038
259
 
1039
- generateEntityRoute(routeType, driverResource, idProperty, entity)
1040
- {
1041
- let idParam = '';
1042
- if(entity){
1043
- idParam = '?' + idProperty + '=' + entity[idProperty];
1044
- }
1045
- return this.rootPath + '/' + driverResource.entityPath + this[routeType] + idParam;
1046
- }
1047
-
1048
- isAuthenticated(req, res, next)
1049
- {
1050
- let allowContinue = {result: true, callback: null};
1051
- let event = {adminManager: this, req, res, next, allowContinue};
1052
- this.events.emit('reldens.adminIsAuthenticated', event);
1053
- let returnPath = this.rootPath+this.loginPath;
1054
- if(false === allowContinue.result){
1055
- return res.redirect(returnPath);
1056
- }
1057
- if(null !== allowContinue.callback){
1058
- return allowContinue.callback(event);
1059
- }
1060
- let user = req.session?.user;
1061
- if(!user){
1062
- return res.redirect(returnPath);
1063
- }
1064
- let userBlackList = this.blackList[user.role_id] || [];
1065
- if(-1 !== userBlackList.indexOf(req.path)){
1066
- let referrer = String(req.headers?.referer || '');
1067
- return res.redirect('' !== referrer ? referrer : returnPath);
1068
- }
1069
- return next();
1070
- }
1071
-
1072
- prepareFilters(filtersList, driverResource)
260
+ fetchTranslation(snippet, group)
1073
261
  {
1074
- let filtersKeys = Object.keys(filtersList);
1075
- if(0 === filtersKeys.length){
1076
- return {};
262
+ if('' === snippet){
263
+ return snippet;
1077
264
  }
1078
- let filters = {};
1079
- for(let i of filtersKeys){
1080
- let filter = filtersList[i];
1081
- if('' === filter){
1082
- continue;
1083
- }
1084
- let rawConfigFilterProperties = driverResource.options.properties[i];
1085
- if(!rawConfigFilterProperties){
1086
- Logger.critical('Could not found property by key.', i);
1087
- continue;
1088
- }
1089
- if(rawConfigFilterProperties.isUpload){
1090
- continue;
1091
- }
1092
- if('reference' === rawConfigFilterProperties.type){
1093
- filters[i] = filter;
1094
- continue;
1095
- }
1096
- if('boolean' === rawConfigFilterProperties.type){
1097
- filters[i] = ('true' === filter);
1098
- continue;
265
+ let translationGroup = sc.get(this.translations, group);
266
+ if(translationGroup){
267
+ let translationByGroup = sc.get(translationGroup, snippet, '');
268
+ if('' !== translationByGroup){
269
+ return translationByGroup;
1099
270
  }
1100
- filters[i] = {operator: 'like', value: '%'+filter+'%'};
1101
271
  }
1102
- return filters;
272
+ return sc.get(this.translations, snippet, snippet);
1103
273
  }
1104
274
 
1105
275
  }