@reldens/cms 0.1.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.
@@ -0,0 +1,1059 @@
1
+ /**
2
+ *
3
+ * Reldens - AdminManager
4
+ *
5
+ */
6
+
7
+ const { UploaderFactory, FileHandler } = require('@reldens/server-utils');
8
+ const { PageRangeProvider, ValidatorInterface, Logger, sc } = require('@reldens/utils');
9
+
10
+ class AdminManager
11
+ {
12
+
13
+ constructor(configData)
14
+ {
15
+ this.events = configData?.events;
16
+ this.renderCallback = configData?.renderCallback;
17
+ this.dataServer = configData?.dataServer;
18
+ this.authenticationCallback = configData?.authenticationCallback;
19
+ this.app = configData?.app;
20
+ this.applicationFramework = configData?.appServerFactory?.applicationFramework;
21
+ this.bodyParser = configData?.appServerFactory?.bodyParser;
22
+ this.session = configData?.appServerFactory?.session;
23
+ this.validator = configData?.validator;
24
+ this.buckets = sc.get(configData, 'buckets', {});
25
+ this.translations = sc.get(configData, 'translations', {});
26
+ this.adminFilesContents = sc.get(configData, 'adminFilesContents', false);
27
+ this.secret = sc.get(configData, 'secret', '');
28
+ this.rootPath = sc.get(configData, 'rootPath', '');
29
+ this.adminRoleId = sc.get(configData, 'adminRoleId', 0);
30
+ this.buildAdminCssOnActivation = sc.get(configData, 'buildAdminCssOnActivation', false);
31
+ this.buildAdminScriptsOnActivation = sc.get(configData, 'buildAdminScriptsOnActivation', false);
32
+ this.updateAdminAssetsDistOnActivation = sc.get(configData, 'updateAdminAssetsDistOnActivation', false);
33
+ this.stylesFilePath = sc.get(configData, 'stylesFilePath', '');
34
+ this.scriptsFilePath = sc.get(configData, 'scriptsFilePath', '');
35
+ this.autoSyncDistCallback = sc.get(configData, 'autoSyncDistCallback', false);
36
+ this.branding = sc.get(configData, 'branding', {});
37
+ this.entities = sc.get(configData, 'entities', {});
38
+ this.logoutPath = '/logout';
39
+ this.loginPath = '/login';
40
+ this.viewPath = '/view';
41
+ this.editPath = '/edit';
42
+ this.savePath = '/save';
43
+ this.deletePath = '/delete';
44
+ this.mimeTypes = sc.get(configData, 'mimeTypes', false);
45
+ this.allowedExtensions = sc.get(configData, 'allowedExtensions', false);
46
+ this.uploaderFactory = sc.get(configData, 'uploaderFactory', new UploaderFactory({
47
+ mimeTypes: this.mimeTypes,
48
+ allowedExtensions: this.allowedExtensions,
49
+ applySecureFileNames: sc.get(configData, 'applySecureFileNames', false)
50
+ }));
51
+ this.adminContents = {};
52
+ this.blackList = {};
53
+ }
54
+
55
+ async setupAdmin()
56
+ {
57
+ if (this.validator instanceof ValidatorInterface && !this.validator.validate(this)){
58
+ return false;
59
+ }
60
+ this.resourcesByReference = {};
61
+ this.resources = this.prepareResources(this.entities);
62
+ 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();
68
+ await this.events.emit('reldens.setupAdminRouter', {adminManager: this});
69
+ this.setupAdminRoutes();
70
+ await this.events.emit('reldens.setupAdminRoutes', {adminManager: this});
71
+ await this.setupEntitiesRoutes();
72
+ await this.events.emit('reldens.setupAdminManagers', {adminManager: this});
73
+ }
74
+
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
+ prepareResources(rawResources)
313
+ {
314
+ let rawResourcesKeys = Object.keys(rawResources);
315
+ if(!rawResources || 0 === rawResourcesKeys.length){
316
+ return [];
317
+ }
318
+ let registeredResources = [];
319
+ for(let i of rawResourcesKeys){
320
+ let rawResource = rawResources[i];
321
+ let tableName = rawResource.rawEntity.tableName();
322
+ // @TODO - BETA - Refactor to add the ID property and composed labels (id + label), in the resource.
323
+ let driverResource = {
324
+ id: () => {
325
+ return tableName;
326
+ },
327
+ entityKey: i,
328
+ entityPath: (tableName.replace(/_/g, '-')),
329
+ options: {
330
+ navigation: sc.hasOwn(rawResource.config, 'parentItemLabel') ? {
331
+ name: rawResource.config.parentItemLabel,
332
+ icon: rawResource.config.icon || 'List'
333
+ } : null,
334
+ listProperties: rawResource.config.listProperties || [],
335
+ showProperties: rawResource.config.showProperties || [],
336
+ filterProperties: rawResource.config.filterProperties || [],
337
+ editProperties: rawResource.config.editProperties || [],
338
+ properties: rawResource.config.properties || {},
339
+ titleProperty: sc.get(rawResource.config, 'titleProperty', null),
340
+ sort: sc.get(rawResource.config, 'sort', null),
341
+ navigationPosition: sc.get(rawResource.config, 'navigationPosition', 2000)
342
+ },
343
+ };
344
+ this.resourcesByReference[tableName] = driverResource;
345
+ registeredResources.push(driverResource);
346
+ }
347
+ registeredResources.sort((a, b) => a.options.navigationPosition - b.options.navigationPosition);
348
+ return registeredResources;
349
+ }
350
+
351
+ prepareRelations()
352
+ {
353
+ // @TODO - BETA - Refactor, include in resources generation at once.
354
+ let registeredRelations = {};
355
+ for(let resource of this.resources){
356
+ for(let propertyKey of Object.keys(resource.options.properties)){
357
+ let property = resource.options.properties[propertyKey];
358
+ if('reference' !== property.type){
359
+ continue;
360
+ }
361
+ let relationResource = this.resources.filter((resource) => {
362
+ return resource.id() === property.reference;
363
+ }).shift();
364
+ let relationKey = property.alias || property.reference;
365
+ let titleProperty = relationResource?.options?.titleProperty;
366
+ if(!titleProperty){
367
+ continue;
368
+ }
369
+ if(!registeredRelations[property.reference]){
370
+ registeredRelations[property.reference] = {};
371
+ }
372
+ registeredRelations[property.reference][relationKey] = titleProperty;
373
+ }
374
+ }
375
+ return registeredRelations;
376
+ }
377
+
378
+ setupAdminRouter()
379
+ {
380
+ this.adminRouter = this.applicationFramework.Router();
381
+ // apply session middleware only to /admin routes:
382
+ if(this.session){
383
+ this.adminRouter.use(this.session({secret: this.secret, resave: false, saveUninitialized: true}));
384
+ }
385
+ this.adminRouter.use(this.bodyParser.json());
386
+ }
387
+
388
+ setupAdminRoutes()
389
+ {
390
+ this.adminRouter.get(this.loginPath, async (req, res) => {
391
+ return res.send(this.adminContents.login);
392
+ });
393
+ // route for handling login:
394
+ this.adminRouter.post(this.loginPath, async (req, res) => {
395
+ let { email, password } = req.body;
396
+ let loginResult = await this.authenticationCallback(email, password, this.adminRoleId);
397
+ if(loginResult){
398
+ req.session.user = loginResult;
399
+ return res.redirect(this.rootPath);
400
+ }
401
+ return res.redirect(this.rootPath+this.loginPath+'?login-error=true');
402
+ });
403
+ // route for the admin panel dashboard:
404
+ this.adminRouter.get('/', this.isAuthenticated.bind(this), async (req, res) => {
405
+ return res.send(this.adminContents.dashboard);
406
+ });
407
+ // route for logout:
408
+ this.adminRouter.get(this.logoutPath, (req, res) => {
409
+ req.session.destroy();
410
+ res.redirect(this.rootPath+this.loginPath);
411
+ });
412
+ this.app.use(this.rootPath, this.adminRouter);
413
+ }
414
+
415
+ async setupEntitiesRoutes()
416
+ {
417
+ if(!this.resources || 0 === this.resources.length){
418
+ return;
419
+ }
420
+ for(let driverResource of this.resources){
421
+ let entityPath = driverResource.entityPath;
422
+ let entityRoute = '/'+entityPath;
423
+ this.adminRouter.get(entityRoute, this.isAuthenticated.bind(this), async (req, res) => {
424
+ let routeContents = await this.generateListRouteContent(req, driverResource, entityPath);
425
+ return res.send(routeContents);
426
+ });
427
+ this.adminRouter.post(entityRoute, this.isAuthenticated.bind(this), async (req, res) => {
428
+ let routeContents = await this.generateListRouteContent(req, driverResource, entityPath);
429
+ return res.send(routeContents);
430
+ });
431
+ this.adminRouter.get(entityRoute+this.viewPath, this.isAuthenticated.bind(this), async (req, res) => {
432
+ let routeContents = await this.generateViewRouteContent(req, driverResource, entityPath);
433
+ if('' === routeContents){
434
+ return res.redirect(this.rootPath+'/'+entityPath+'?result=errorView');
435
+ }
436
+ return res.send(routeContents);
437
+ });
438
+ this.adminRouter.get(entityRoute+this.editPath, this.isAuthenticated.bind(this), async (req, res) => {
439
+ let routeContents = await this.generateEditRouteContent(req, driverResource, entityPath);
440
+ if('' === routeContents){
441
+ return res.redirect(this.rootPath+'/'+entityPath+'?result=errorEdit');
442
+ }
443
+ return res.send(routeContents);
444
+ });
445
+ this.setupSavePath(entityRoute, driverResource, entityPath);
446
+ this.adminRouter.post(entityRoute+this.deletePath, this.isAuthenticated.bind(this), async (req, res) => {
447
+ let redirectResult = await this.processDeleteEntities(req, res, driverResource, entityPath);
448
+ return res.redirect(redirectResult);
449
+ });
450
+ await this.events.emit('reldens.setupEntitiesRoutes', {
451
+ adminManager: this,
452
+ entityPath,
453
+ entityRoute,
454
+ driverResource
455
+ });
456
+ }
457
+ }
458
+
459
+ setupSavePath(entityRoute, driverResource, entityPath)
460
+ {
461
+ let uploadProperties = this.fetchUploadProperties(driverResource);
462
+ let uploadPropertiesKeys = Object.keys(uploadProperties || {});
463
+ if(0 === uploadPropertiesKeys.length){
464
+ this.adminRouter.post(
465
+ entityRoute+this.savePath,
466
+ this.isAuthenticated.bind(this),
467
+ async (req, res) => {
468
+ let redirectResult = await this.processSaveEntity(req, res, driverResource, entityPath);
469
+ return res.redirect(redirectResult);
470
+ }
471
+ );
472
+ return;
473
+ }
474
+ let fields = [];
475
+ let allowedFileTypes = {};
476
+ for(let uploadPropertyKey of uploadPropertiesKeys){
477
+ let property = uploadProperties[uploadPropertyKey];
478
+ allowedFileTypes[uploadPropertyKey] = property.allowedTypes || false;
479
+ let field = {name: uploadPropertyKey};
480
+ if(!property.isArray){
481
+ field.maxCount = 1;
482
+ }
483
+ fields.push(field);
484
+ this.buckets[uploadPropertyKey] = property.bucket;
485
+ }
486
+ this.adminRouter.post(
487
+ entityRoute + this.savePath,
488
+ this.isAuthenticated.bind(this),
489
+ this.uploaderFactory.createUploader(fields, this.buckets, allowedFileTypes),
490
+ async (req, res) => {
491
+ let redirectResult = await this.processSaveEntity(req, res, driverResource, entityPath);
492
+ return res.redirect(redirectResult);
493
+ }
494
+ );
495
+ }
496
+
497
+ async processDeleteEntities(req, res, driverResource, entityPath)
498
+ {
499
+ let ids = req?.body?.ids;
500
+ if('string' === typeof ids){
501
+ ids = ids.split(',');
502
+ }
503
+ let redirectPath = this.rootPath+'/'+entityPath+'?result=';
504
+ let resultString = 'errorMissingId';
505
+ if(!ids || 0 === ids.length){
506
+ return redirectPath + resultString;
507
+ }
508
+ try {
509
+ let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
510
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
511
+ let idsFilter = {[idProperty]: {operator: 'IN', value: ids}};
512
+ let loadedEntities = await entityRepository.load(idsFilter);
513
+ await this.deleteEntitiesRelatedFiles(driverResource, loadedEntities);
514
+ let deleteResult = await entityRepository.delete(idsFilter);
515
+ resultString = deleteResult ? 'success' : 'errorStorageFailure';
516
+ } catch (error) {
517
+ resultString = 'errorDeleteFailure';
518
+ }
519
+ return redirectPath + resultString;
520
+ }
521
+
522
+ async deleteEntitiesRelatedFiles(driverResource, entities)
523
+ {
524
+ let resourcePropertiesKeys = Object.keys(driverResource.options.properties);
525
+ for(let propertyKey of resourcePropertiesKeys){
526
+ let property = driverResource.options.properties[propertyKey];
527
+ if(!property.isUpload){
528
+ continue;
529
+ }
530
+ for(let entity of entities){
531
+ if(!property.isArray){
532
+ FileHandler.remove([(property.bucket || ''), entity[propertyKey]]);
533
+ continue;
534
+ }
535
+ let entityFiles = entity[propertyKey].split(property.isArray);
536
+ for(let entityFile of entityFiles){
537
+ FileHandler.remove([(property.bucket || ''), entityFile]);
538
+ }
539
+ }
540
+ }
541
+ }
542
+
543
+ async processSaveEntity(req, res, driverResource, entityPath)
544
+ {
545
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
546
+ let id = (req?.body[idProperty] || '').toString();
547
+ let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
548
+ let resourceProperties = driverResource.options.properties;
549
+ let entityDataPatch = this.preparePatchData(driverResource, idProperty, req, resourceProperties, id);
550
+ if(!entityDataPatch){
551
+ Logger.error('Bad patch data.', entityDataPatch);
552
+ return this.rootPath+'/'+entityPath+'?result=saveBadPatchData';
553
+ }
554
+ let editRoute = this.generateEntityRoute('editPath', driverResource, idProperty);
555
+ try {
556
+ let saveResult = await this.saveEntity(id, entityRepository, entityDataPatch);
557
+ if(!saveResult){
558
+ Logger.error('Save result error.', saveResult, entityDataPatch);
559
+ return editRoute+'?result=saveEntityStorageError';
560
+ }
561
+ if(sc.isFunction(this.autoSyncDistCallback)){
562
+ let uploadProperties = this.fetchUploadProperties(driverResource);
563
+ if(0 < Object.keys(uploadProperties).length){
564
+ for(let uploadPropertyKey of Object.keys(uploadProperties)){
565
+ let property = uploadProperties[uploadPropertyKey];
566
+ await this.autoSyncDistCallback(
567
+ property.bucket,
568
+ saveResult[uploadPropertyKey],
569
+ property.distFolder
570
+ );
571
+ }
572
+ }
573
+ }
574
+ return this.generateEntityRoute('viewPath', driverResource, idProperty, saveResult) +'&result=success';
575
+ } catch (error) {
576
+ Logger.error('Save entity error.', error);
577
+ return this.rootPath+'/'+entityPath+'?result=saveEntityError';
578
+ }
579
+ }
580
+
581
+ async saveEntity(id, entityRepository, entityDataPatch)
582
+ {
583
+ if('' === id){
584
+ return entityRepository.create(entityDataPatch);
585
+ }
586
+ return entityRepository.updateById(id, entityDataPatch);
587
+ }
588
+
589
+ preparePatchData(driverResource, idProperty, req, resourceProperties, id)
590
+ {
591
+ let entityDataPatch = {};
592
+ for(let i of driverResource.options.editProperties){
593
+ if(i === idProperty){
594
+ continue;
595
+ }
596
+ let propertyUpdateValue = sc.get(req.body, i, null);
597
+ let property = resourceProperties[i];
598
+ let isNullValue = null === propertyUpdateValue;
599
+ let propertyType = property.type || 'string';
600
+ if(property.isUpload){
601
+ propertyType = 'upload';
602
+ propertyUpdateValue = this.prepareUploadPatchData(req, i, propertyUpdateValue, property);
603
+ }
604
+ if('number' === propertyType && !isNullValue){
605
+ propertyUpdateValue = Number(propertyUpdateValue);
606
+ }
607
+ if('string' === propertyType && !isNullValue){
608
+ propertyUpdateValue = String(propertyUpdateValue);
609
+ }
610
+ if('boolean' === propertyType){
611
+ propertyUpdateValue = Boolean(propertyUpdateValue);
612
+ }
613
+ let isUploadCreate = property.isUpload && !id;
614
+ if(property.isRequired && null === propertyUpdateValue && (!property.isUpload || isUploadCreate)){
615
+ // missing required fields would break the update:
616
+ Logger.critical('Bad patch data on update.', propertyUpdateValue, property);
617
+ return false;
618
+ }
619
+ if(!property.isUpload || (property.isUpload && null !== propertyUpdateValue)){
620
+ entityDataPatch[i] = propertyUpdateValue;
621
+ }
622
+ }
623
+ return entityDataPatch;
624
+ }
625
+
626
+ prepareUploadPatchData(req, i, propertyUpdateValue, property)
627
+ {
628
+ let filesData = sc.get(req.files, i, null);
629
+ if(null === filesData){
630
+ return null;
631
+ }
632
+ let fileNames = [];
633
+ for(let file of filesData){
634
+ fileNames.push(file.filename);
635
+ }
636
+ return fileNames.join(property.isArray);
637
+ }
638
+
639
+ async generateEditRouteContent(req, driverResource, entityPath)
640
+ {
641
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
642
+ let idValue = (req?.query[idProperty] || '').toString();
643
+ let templateTitle = ('' === idValue ? 'Create' : 'Edit')+' '+this.translations.labels[driverResource.id()];
644
+ let loadedEntity = '' === idValue ? null :await this.loadEntityById(driverResource, idValue);
645
+ let entityViewRoute = '' === idValue
646
+ ? this.rootPath+'/'+driverResource.entityPath
647
+ : this.generateEntityRoute('viewPath', driverResource, idProperty, loadedEntity);
648
+ let renderedEditProperties = {
649
+ idValue,
650
+ idProperty,
651
+ idPropertyLabel: this.fetchTranslation(idProperty),
652
+ templateTitle,
653
+ entityViewRoute
654
+ };
655
+ let propertiesKeys = Object.keys(driverResource.options.properties);
656
+ for(let propertyKey of propertiesKeys){
657
+ let resourceProperty = driverResource.options.properties[propertyKey];
658
+ let fieldDisabled = -1 === driverResource.options.editProperties.indexOf(propertyKey);
659
+ let isRequired = resourceProperty.isRequired ? ' required="required"' : '';
660
+ if(resourceProperty.isUpload && loadedEntity){
661
+ isRequired = '';
662
+ }
663
+ renderedEditProperties[propertyKey] = await this.render(
664
+ this.adminFilesContents.fields.edit[this.propertyType(resourceProperty, 'edit')],
665
+ {
666
+ fieldName: propertyKey,
667
+ fieldValue: await this.generatePropertyEditRenderedValue(
668
+ loadedEntity,
669
+ propertyKey,
670
+ resourceProperty
671
+ ),
672
+ fieldDisabled: fieldDisabled ? ' disabled="disabled"' : '',
673
+ required: isRequired,
674
+ multiple: resourceProperty.isArray ? ' multiple="multiple"' : ''
675
+ }
676
+ );
677
+ }
678
+ let renderedView = await this.render(this.adminContents.entities[entityPath].edit, renderedEditProperties);
679
+ return await this.renderRoute(renderedView, this.adminContents.sideBar);
680
+ }
681
+
682
+ async loadEntityById(driverResource, id)
683
+ {
684
+ let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
685
+ return await entityRepository.loadByIdWithRelations(id);
686
+ }
687
+
688
+ async generateViewRouteContent(req, driverResource, entityPath)
689
+ {
690
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
691
+ let id = (sc.get(req.query, idProperty, '')).toString();
692
+ if('' === id){
693
+ Logger.error('Missing ID on view route.', entityPath, id, idProperty);
694
+ return '';
695
+ }
696
+ let loadedEntity = await this.loadEntityById(driverResource, id);
697
+ let renderedViewProperties = {
698
+ entityEditRoute: this.generateEntityRoute('editPath', driverResource, idProperty, loadedEntity),
699
+ entityNewRoute: this.generateEntityRoute('editPath', driverResource, idProperty),
700
+ id
701
+ };
702
+ let entitySerializedData = {};
703
+ for(let propertyKey of driverResource.options.showProperties){
704
+ let resourceProperty = driverResource.options.properties[propertyKey];
705
+ let {fieldValue, fieldName} = this.generatePropertyRenderedValueWithLabel(
706
+ loadedEntity,
707
+ propertyKey,
708
+ resourceProperty
709
+ );
710
+ entitySerializedData[fieldName] = fieldValue;
711
+ let renderedFieldValue = await this.generatePropertyRenderedValue(
712
+ fieldValue,
713
+ fieldName,
714
+ resourceProperty,
715
+ 'view'
716
+ );
717
+ renderedViewProperties[propertyKey] = await this.render(
718
+ this.adminFilesContents.fields.view[this.propertyType(resourceProperty)],
719
+ {
720
+ fieldName: propertyKey,
721
+ fieldValue: renderedFieldValue,
722
+ fieldOriginalValue: fieldValue,
723
+ target: ' target="_blank"'
724
+ }
725
+ );
726
+ }
727
+ let extraDataEvent = {entitySerializedData, entityId: driverResource.id(), entity: loadedEntity};
728
+ await this.events.emit('adminEntityExtraData', extraDataEvent);
729
+ entitySerializedData = extraDataEvent.entitySerializedData;
730
+ renderedViewProperties.entitySerializedData = JSON.stringify(entitySerializedData).replace(/"/g, '&quot;');
731
+ let renderedView = await this.render(this.adminContents.entities[entityPath].view, renderedViewProperties);
732
+ return await this.renderRoute(renderedView, this.adminContents.sideBar);
733
+ }
734
+
735
+ propertyType(resourceProperty, templateType)
736
+ {
737
+ let propertyType = resourceProperty.type || 'text';
738
+ if('reference' === propertyType && 'edit' === templateType){
739
+ return 'select';
740
+ }
741
+ if(resourceProperty.isUpload){
742
+ if('edit' === templateType){
743
+ return 'file';
744
+ }
745
+ if('view' === templateType){
746
+ let multiple = resourceProperty.isArray ? 's' : '';
747
+ if('image' === resourceProperty.allowedTypes){
748
+ return resourceProperty.allowedTypes + multiple;
749
+ }
750
+ if('text' === resourceProperty.allowedTypes){
751
+ return 'link'+multiple
752
+ }
753
+ return 'text';
754
+ }
755
+ }
756
+ if(-1 !== ['reference', 'number', 'datetime'].indexOf(propertyType)){
757
+ propertyType = 'text';
758
+ }
759
+ return propertyType;
760
+ }
761
+
762
+ async generateListRouteContent(req, driverResource, entityPath)
763
+ {
764
+ let page = Number(req?.query?.page || 1);
765
+ let pageSize = Number(req?.query?.pageSize || 25);
766
+ let filtersFromParams = req?.body?.filters || {};
767
+ let filters = this.prepareFilters(filtersFromParams, driverResource);
768
+ let mappedFiltersValues = driverResource.options.filterProperties.map((property) => {
769
+ let filterValue = (filtersFromParams[property] || '').toString();
770
+ return {[property]: '' === filterValue ? '' : 'value="'+filterValue+'"'};
771
+ });
772
+ let entitiesRows = await this.loadEntitiesForList(driverResource, pageSize, page, req, filters);
773
+ let listRawContent = this.adminContents.entities[entityPath].list.toString();
774
+ let totalPages = Math.ceil(await this.countTotalEntities(driverResource, filters) / pageSize);
775
+ let pages = PageRangeProvider.fetch(page, totalPages);
776
+ let renderedPagination = '';
777
+ for(let page of pages){
778
+ renderedPagination += await this.render(
779
+ this.adminFilesContents.fields.view['link'],
780
+ {
781
+ fieldName: page.label,
782
+ fieldValue: this.rootPath+'/'+driverResource.entityPath+'?page='+ page.value,
783
+ fieldOriginalValue: page.value,
784
+ }
785
+ );
786
+ }
787
+ let listVars = {
788
+ deletePath: this.rootPath + '/' + driverResource.entityPath + this.deletePath,
789
+ fieldsHeaders: driverResource.options.listProperties.map((property) => {
790
+ let propertyTitle = this.fetchTranslation(property, driverResource.id());
791
+ let alias = this.fetchTranslation(
792
+ driverResource.options.properties[property]?.alias || '',
793
+ driverResource.id()
794
+ );
795
+ let title = '' !== alias ? alias + ' ('+propertyTitle+')' : propertyTitle;
796
+ return {name: property, value: title};
797
+ }),
798
+ rows: entitiesRows
799
+ };
800
+ let list = await this.render(this.adminFilesContents.listContent, listVars);
801
+ let entitiesListView = await this.render(
802
+ listRawContent,
803
+ Object.assign({list, pagination: renderedPagination}, ...mappedFiltersValues)
804
+ );
805
+ return await this.renderRoute(entitiesListView, this.adminContents.sideBar);
806
+ }
807
+
808
+ fetchTranslation(snippet, group)
809
+ {
810
+ if('' === snippet){
811
+ return snippet;
812
+ }
813
+ let translationGroup = sc.get(this.translations, group);
814
+ if(translationGroup){
815
+ let translationByGroup = sc.get(translationGroup, snippet, '');
816
+ if('' !== translationByGroup){
817
+ return translationByGroup;
818
+ }
819
+ }
820
+ return sc.get(this.translations, snippet, snippet);
821
+ }
822
+
823
+ async countTotalEntities(driverResource, filters)
824
+ {
825
+ /** @type {BaseDriver|ObjectionJsDriver} entityRepository **/
826
+ let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
827
+ return await entityRepository.count(filters);
828
+ }
829
+
830
+ async loadEntitiesForList(driverResource, pageSize, page, req, filters)
831
+ {
832
+ let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
833
+ entityRepository.limit = pageSize;
834
+ if(1 < page){
835
+ entityRepository.offset = (page - 1) * pageSize;
836
+ }
837
+ entityRepository.sortBy = req?.body?.sortBy || false;
838
+ entityRepository.sortDirection = req?.body?.sortDirection || false;
839
+ let loadedEntities = await entityRepository.loadWithRelations(filters, []);
840
+ entityRepository.limit = 0;
841
+ entityRepository.offset = 0;
842
+ entityRepository.sortBy = false;
843
+ entityRepository.sortDirection = false;
844
+ let entityRows = [];
845
+ let deleteLink = this.rootPath + '/' + driverResource.entityPath + this.deletePath;
846
+ for(let entity of loadedEntities){
847
+ let entityRow = {fields: []};
848
+ let resourceProperties = driverResource.options?.properties;
849
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
850
+ let viewLink = '';
851
+ let editLink = '';
852
+ if('' !== idProperty){
853
+ viewLink = this.generateEntityRoute('viewPath', driverResource, idProperty, entity);
854
+ editLink = this.generateEntityRoute('editPath', driverResource, idProperty, entity);
855
+ }
856
+ for(let property of driverResource.options.listProperties){
857
+ let {fieldValue, fieldName} = this.generatePropertyRenderedValueWithLabel(
858
+ entity,
859
+ property,
860
+ resourceProperties[property]
861
+ );
862
+ let value = await this.generatePropertyRenderedValue(
863
+ fieldValue,
864
+ fieldName,
865
+ resourceProperties[property]
866
+ );
867
+ entityRow.fields.push({
868
+ name: property,
869
+ value,
870
+ viewLink
871
+ });
872
+ }
873
+ entityRow.editLink = editLink;
874
+ entityRow.deleteLink = deleteLink;
875
+ entityRow.id = entity[idProperty];
876
+ entityRows.push(entityRow);
877
+ }
878
+ return entityRows;
879
+ }
880
+
881
+ async generatePropertyRenderedValue(fieldValue, fieldName, resourceProperty, templateType)
882
+ {
883
+ let fieldOriginalValue = fieldValue;
884
+ if('view' === templateType){
885
+ if(resourceProperty.isArray){
886
+ fieldValue = fieldValue.split(resourceProperty.isArray).map((value) => {
887
+ let target = resourceProperty.isUpload ? ' target="_blank"' : '';
888
+ let fieldValuePart = resourceProperty.isUpload && resourceProperty.bucketPath
889
+ ? resourceProperty.bucketPath+value
890
+ : value;
891
+ return {fieldValuePart, fieldOriginalValuePart: value, target};
892
+ });
893
+ }
894
+ if(!resourceProperty.isArray && resourceProperty.isUpload){
895
+ fieldValue = resourceProperty.bucketPath+fieldValue;
896
+ }
897
+ }
898
+ return await this.render(
899
+ this.adminFilesContents.fields.view[this.propertyType(resourceProperty, templateType)],
900
+ {fieldName, fieldValue, fieldOriginalValue, target: ' target="_blank"'}
901
+ );
902
+ }
903
+
904
+ generatePropertyRenderedValueWithLabel(entity, propertyKey, resourceProperty)
905
+ {
906
+ let fieldValue = (0 === entity[propertyKey] ? '0' : entity[propertyKey] || '').toString();
907
+ let fieldName = propertyKey;
908
+ if('boolean' === resourceProperty.type){
909
+ fieldValue = '1' === fieldValue || 'true' === fieldValue ? 'Yes' : 'No';
910
+ }
911
+ if('datetime' === resourceProperty.type){
912
+ fieldValue = '' !== fieldValue ? sc.formatDate(new Date(fieldValue)) : '';
913
+ }
914
+ if('reference' === resourceProperty.type){
915
+ let relationKey = resourceProperty.alias || resourceProperty.reference;
916
+ let relationEntity = entity[relationKey];
917
+ if(relationEntity){
918
+ let relation = this.relations[resourceProperty.reference];
919
+ if(relation){
920
+ let relationTitleProperty = relation[relationKey];
921
+ if(relationTitleProperty && '' !== String(relationEntity[relationTitleProperty] || '')){
922
+ fieldName = relationTitleProperty;
923
+ fieldValue = relationEntity[relationTitleProperty]+(' ('+fieldValue+')');
924
+ }
925
+ }
926
+ }
927
+ }
928
+ if(resourceProperty.availableValues){
929
+ let optionData = resourceProperty.availableValues.filter((availableValue) => {
930
+ return String(availableValue.value) === String(fieldValue);
931
+ }).shift();
932
+ if(optionData){
933
+ fieldValue = optionData.label + ' (' + fieldValue + ')';
934
+ }
935
+ }
936
+ return {fieldValue, fieldName};
937
+ }
938
+
939
+ async generatePropertyEditRenderedValue(entity, propertyKey, resourceProperty)
940
+ {
941
+ let entityPropertyValue = sc.get(entity, propertyKey, null);
942
+ let fieldValue = (0 === entityPropertyValue ? '0' : entityPropertyValue || '').toString();
943
+ if('boolean' === resourceProperty.type){
944
+ fieldValue = '1' === fieldValue || 'true' === fieldValue ? ' checked="checked"' : '';
945
+ }
946
+ if('reference' === resourceProperty.type){
947
+ let relationDriverResource = this.resourcesByReference[resourceProperty.reference];
948
+ let relation = this.relations[resourceProperty.reference];
949
+ let relationKey = resourceProperty.alias || resourceProperty.reference;
950
+ let idProperty = this.fetchEntityIdPropertyKey(relationDriverResource);
951
+ let relationTitleProperty = relation ? relation[relationKey] : idProperty;
952
+ let relationOptions = await this.fetchRelationOptions(relationDriverResource);
953
+ return relationOptions.map((option) => {
954
+ let value = option[idProperty];
955
+ let selected = entity && entity[propertyKey] === value ? ' selected="selected"' : '';
956
+ return {label: option[relationTitleProperty]+' (ID: '+value+')', value, selected}
957
+ });
958
+ }
959
+ return await this.render(
960
+ this.adminFilesContents.fields.view[this.propertyType(resourceProperty)],
961
+ {fieldName: propertyKey, fieldValue}
962
+ );
963
+ }
964
+
965
+ async fetchRelationOptions(relationDriverResource)
966
+ {
967
+ let relationEntityRepository = this.dataServer.getEntity(relationDriverResource.entityKey);
968
+ return await relationEntityRepository.loadAll();
969
+ }
970
+
971
+ fetchEntityIdPropertyKey(driverResource)
972
+ {
973
+ let resourceProperties = driverResource.options?.properties;
974
+ if(!resourceProperties){
975
+ Logger.error('Property "ID" not found.', resourceProperties);
976
+ return '';
977
+ }
978
+ if(resourceProperties['id']){
979
+ return 'id';
980
+ }
981
+ let idProperty = '';
982
+ let idProperties = Object.keys(resourceProperties).filter((propertyKey) => {
983
+ return resourceProperties[propertyKey].isId;
984
+ });
985
+ if(0 < idProperties.length){
986
+ idProperty = idProperties.shift();
987
+ }
988
+ return idProperty;
989
+ }
990
+
991
+ generateEntityRoute(routeType, driverResource, idProperty, entity)
992
+ {
993
+ let idParam = '';
994
+ if(entity){
995
+ idParam = '?' + idProperty + '=' + entity[idProperty];
996
+ }
997
+ return this.rootPath + '/' + driverResource.entityPath + this[routeType] + idParam;
998
+ }
999
+
1000
+ isAuthenticated(req, res, next)
1001
+ {
1002
+ let allowContinue = {result: true, callback: null};
1003
+ let event = {adminManager: this, req, res, next, allowContinue};
1004
+ this.events.emit('reldens.adminIsAuthenticated', event);
1005
+ let returnPath = this.rootPath+this.loginPath;
1006
+ if(false === allowContinue.result){
1007
+ return res.redirect(returnPath);
1008
+ }
1009
+ if(null !== allowContinue.callback){
1010
+ return allowContinue.callback(event);
1011
+ }
1012
+ let user = req.session?.user;
1013
+ if(!user){
1014
+ return res.redirect(returnPath);
1015
+ }
1016
+ let userBlackList = this.blackList[user.role_id] || [];
1017
+ if(-1 !== userBlackList.indexOf(req.path)){
1018
+ let referrer = String(req.headers?.referer || '');
1019
+ return res.redirect('' !== referrer ? referrer : returnPath);
1020
+ }
1021
+ return next();
1022
+ }
1023
+
1024
+ prepareFilters(filtersList, driverResource)
1025
+ {
1026
+ let filtersKeys = Object.keys(filtersList);
1027
+ if(0 === filtersKeys.length){
1028
+ return {};
1029
+ }
1030
+ let filters = {};
1031
+ for(let i of filtersKeys){
1032
+ let filter = filtersList[i];
1033
+ if('' === filter){
1034
+ continue;
1035
+ }
1036
+ let rawConfigFilterProperties = driverResource.options.properties[i];
1037
+ if(!rawConfigFilterProperties){
1038
+ Logger.critical('Could not found property by key.', i);
1039
+ continue;
1040
+ }
1041
+ if(rawConfigFilterProperties.isUpload){
1042
+ continue;
1043
+ }
1044
+ if('reference' === rawConfigFilterProperties.type){
1045
+ filters[i] = filter;
1046
+ continue;
1047
+ }
1048
+ if('boolean' === rawConfigFilterProperties.type){
1049
+ filters[i] = ('true' === filter);
1050
+ continue;
1051
+ }
1052
+ filters[i] = {operator: 'like', value: '%'+filter+'%'};
1053
+ }
1054
+ return filters;
1055
+ }
1056
+
1057
+ }
1058
+
1059
+ module.exports.AdminManager = AdminManager;