@reldens/cms 0.85.0 → 0.86.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.
@@ -1,854 +1,854 @@
1
- /**
2
- *
3
- * Reldens - RouterContents
4
- *
5
- */
6
-
7
- const { PageRangeProvider, Logger, sc } = require('@reldens/utils');
8
- const { FileHandler } = require('@reldens/server-utils');
9
- const { AdminFiltersManager } = require('./admin-filters-manager');
10
-
11
- class RouterContents
12
- {
13
-
14
- constructor(props)
15
- {
16
- this.dataServer = props.dataServer;
17
- this.translations = props.translations;
18
- this.rootPath = props.rootPath;
19
- this.relations = props.relations;
20
- this.resourcesByReference = props.resourcesByReference;
21
- this.adminFilesContents = props.adminFilesContents;
22
- this.autoSyncDistCallback = props.autoSyncDistCallback;
23
- this.viewPath = props.viewPath;
24
- this.editPath = props.editPath;
25
- this.deletePath = props.deletePath;
26
- this.emitEvent = props.emitEvent;
27
- this.adminContentsRender = props.adminContentsRender;
28
- this.adminContentsRenderRoute = props.adminContentsRenderRoute;
29
- this.adminContentsEntities = props.adminContentsEntities;
30
- this.adminContentsSideBar = props.adminContentsSideBar;
31
- this.fetchTranslation = props.fetchTranslation;
32
- this.fetchEntityIdPropertyKey = props.fetchEntityIdPropertyKey;
33
- this.fetchUploadProperties = props.fetchUploadProperties;
34
- this.uploaderFactory = props.uploaderFactory;
35
- this.filtersManager = new AdminFiltersManager();
36
- this.passwordFieldNames = props.passwordFieldNames || ['password'];
37
- }
38
-
39
- async generateListRouteContent(req, driverResource, entityPath)
40
- {
41
- let currentPage = Number(req?.query?.page || 1);
42
- let pageSize = Number(req?.query?.pageSize || 25);
43
- let shouldClearFilters = 'true' === req?.query?.clearFilters;
44
- let sessionFilters = this.filtersManager.getFiltersFromSession(req, entityPath);
45
- let sessionSorting = this.filtersManager.getSortingFromSession(req, entityPath);
46
- let filtersFromParams = shouldClearFilters ? {} : req?.body?.filters || req?.query?.filters || {};
47
- let entityFilterTerm = shouldClearFilters ? '' : sc.get(req?.body, 'entityFilterTerm', '');
48
- let sortByFromParams = shouldClearFilters ? '' : sc.get(req?.body, 'sortBy', sc.get(req?.query, 'sortBy', ''));
49
- let sortDirectionFromParams = shouldClearFilters ? '' : sc.get(req?.body, 'sortDirection', sc.get(req?.query, 'sortDirection', ''));
50
- if(shouldClearFilters){
51
- this.filtersManager.clearFiltersFromSession(req, entityPath);
52
- this.filtersManager.clearSortingFromSession(req, entityPath);
53
- }
54
- let hasNewEntityFilterTerm = entityFilterTerm && '' !== entityFilterTerm;
55
- let hasNewToggleFilters = Object.keys(filtersFromParams).length > 0;
56
- if(hasNewEntityFilterTerm && hasNewToggleFilters){
57
- filtersFromParams = {};
58
- hasNewToggleFilters = false;
59
- }
60
- if(hasNewEntityFilterTerm || hasNewToggleFilters){
61
- let filtersToSave = {
62
- regular: hasNewToggleFilters ? filtersFromParams : {},
63
- entityFilterTerm: hasNewEntityFilterTerm ? entityFilterTerm : ''
64
- };
65
- this.filtersManager.saveFiltersToSession(req, entityPath, filtersToSave);
66
- sessionFilters = filtersToSave;
67
- }
68
- if(sortByFromParams && sortDirectionFromParams){
69
- this.filtersManager.saveSortingToSession(req, entityPath, sortByFromParams, sortDirectionFromParams);
70
- sessionSorting = {sortBy: sortByFromParams, sortDirection: sortDirectionFromParams};
71
- }
72
- let mergedFilters = shouldClearFilters ? {} : Object.assign({}, sessionFilters.regular || {});
73
- let finalEntityFilterTerm = shouldClearFilters ? '' : sessionFilters.entityFilterTerm || '';
74
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
75
- let finalSortBy = sessionSorting.sortBy || idProperty;
76
- let finalSortDirection = sessionSorting.sortDirection || 'ASC';
77
- let filters = this.filtersManager.prepareFilters(mergedFilters, driverResource);
78
- let textFilters = this.filtersManager.prepareTextFilters(finalEntityFilterTerm, driverResource);
79
- let combinedFilters = this.filtersManager.combineFilters(filters, textFilters);
80
- let totalEntities = await this.countTotalEntities(driverResource, combinedFilters);
81
- let totalPages = totalEntities <= pageSize ? 1 : Math.ceil(totalEntities / pageSize);
82
- let renderedPagination = '';
83
- for(let paginationItem of PageRangeProvider.fetch(currentPage, totalPages)){
84
- let paginationUrl = this.rootPath+'/'+driverResource.entityPath+'?page='+ paginationItem.value;
85
- if(finalEntityFilterTerm){
86
- paginationUrl += '&entityFilterTerm='+encodeURIComponent(finalEntityFilterTerm);
87
- }
88
- if(finalSortBy){
89
- paginationUrl += '&sortBy='+encodeURIComponent(finalSortBy);
90
- }
91
- if(finalSortDirection){
92
- paginationUrl += '&sortDirection='+encodeURIComponent(finalSortDirection);
93
- }
94
- for(let filterKey of Object.keys(mergedFilters)){
95
- if(mergedFilters[filterKey]){
96
- paginationUrl += '&filters['+filterKey+']='+encodeURIComponent(mergedFilters[filterKey]);
97
- }
98
- }
99
- renderedPagination += await this.adminContentsRender(
100
- this.adminFilesContents.fields.view['link'],
101
- {
102
- fieldName: paginationItem.label,
103
- fieldValue: paginationUrl,
104
- fieldOriginalValue: paginationItem.value,
105
- }
106
- );
107
- }
108
- let listProperties = {
109
- entityNewRoute: this.rootPath+'/'+driverResource.entityPath+this.editPath,
110
- entityFilterTermValue: finalEntityFilterTerm || ''
111
- };
112
- await this.emitEvent('reldens.adminListPropertiesPopulation', {
113
- req,
114
- driverResource,
115
- listProperties
116
- });
117
- return await this.adminContentsRenderRoute(
118
- await this.adminContentsRender(
119
- this.adminContentsEntities()[entityPath].list,
120
- Object.assign({
121
- list: await this.adminContentsRender(this.adminFilesContents.listContent, {
122
- deletePath: this.rootPath + '/' + driverResource.entityPath + this.deletePath,
123
- fieldsHeaders: driverResource.options.listProperties.map((property) => {
124
- let propertyTitle = this.fetchTranslation(property, driverResource.id());
125
- let alias = this.fetchTranslation(
126
- driverResource.options.properties[property]?.alias || '',
127
- driverResource.id()
128
- );
129
- let isSorted = finalSortBy === property;
130
- let sortDirection = isSorted ? finalSortDirection.toLowerCase() : '';
131
- return {
132
- name: property,
133
- value: '' !== alias ? alias + ' ('+propertyTitle+')' : propertyTitle,
134
- isSorted,
135
- sortDirection,
136
- isSortedAsc: isSorted && 'asc' === sortDirection,
137
- isSortedDesc: isSorted && 'desc' === sortDirection
138
- };
139
- }),
140
- rows: await this.loadEntitiesForList(
141
- driverResource,
142
- pageSize,
143
- currentPage,
144
- req,
145
- combinedFilters,
146
- finalSortBy,
147
- finalSortDirection
148
- )
149
- }),
150
- pagination: renderedPagination,
151
- extraContentForList: sc.get(listProperties, 'extraContentForList', ''),
152
- entityFilterTermValue: listProperties.entityFilterTermValue
153
- }, ...driverResource.options.filterProperties.map((property) => {
154
- let filterValue = (mergedFilters[property] || '');
155
- return {[property]: '' === filterValue ? '' : 'value="'+filterValue+'"'};
156
- }))
157
- ),
158
- this.adminContentsSideBar()
159
- );
160
- }
161
-
162
- async generateViewRouteContent(req, driverResource, entityPath)
163
- {
164
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
165
- let id = (sc.get(req.query, idProperty, ''));
166
- if('' === id){
167
- Logger.error('Missing ID on view route.', entityPath, id, idProperty);
168
- return '';
169
- }
170
- let loadedEntity = await this.loadEntityById(driverResource, id);
171
- if(!loadedEntity){
172
- Logger.critical('Entity not found on view route.', entityPath, id, idProperty);
173
- return '';
174
- }
175
- let renderedViewProperties = {
176
- entityEditRoute: this.generateEntityRoute('editPath', driverResource, idProperty, loadedEntity),
177
- entityNewRoute: this.rootPath+'/'+driverResource.entityPath+this.editPath,
178
- id
179
- };
180
- let entitySerializedData = {};
181
- for(let propertyKey of driverResource.options.showProperties){
182
- let property = driverResource.options.properties[propertyKey];
183
- let {fieldValue, fieldName} = this.generatePropertyRenderedValueWithLabel(
184
- loadedEntity,
185
- propertyKey,
186
- property
187
- );
188
- entitySerializedData[fieldName] = fieldValue;
189
- renderedViewProperties[propertyKey] = await this.generatePropertyRenderedValue(
190
- fieldValue,
191
- fieldName,
192
- property,
193
- 'view'
194
- );
195
- }
196
- let extraDataEvent = {entitySerializedData, entityId: driverResource.id(), entity: loadedEntity};
197
- await this.emitEvent('adminEntityExtraData', extraDataEvent);
198
- renderedViewProperties.entitySerializedData = JSON.stringify(extraDataEvent.entitySerializedData)
199
- .replace(/"/g, '&quot;');
200
- renderedViewProperties.extraContentForViewTop = '';
201
- renderedViewProperties.extraContentForViewBottom = '';
202
- await this.emitEvent('reldens.adminViewPropertiesPopulation', {
203
- idProperty,
204
- req,
205
- driverResource,
206
- loadedEntity,
207
- renderedViewProperties
208
- });
209
- return await this.adminContentsRenderRoute(
210
- await this.adminContentsRender(this.adminContentsEntities()[entityPath].view, renderedViewProperties),
211
- this.adminContentsSideBar()
212
- );
213
- }
214
-
215
- async generateEditRouteContent(req, driverResource, entityPath)
216
- {
217
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
218
- let idValue = String(sc.get(req?.query, idProperty, ''));
219
- let loadedEntity = !idValue ? null : await this.loadEntityById(driverResource, idValue);
220
- let renderedEditProperties = {
221
- idValue,
222
- idProperty,
223
- idPropertyLabel: this.fetchTranslation(idProperty, driverResource.id()),
224
- templateTitle: (!idValue ? 'Create' : 'Edit')+' '+this.translations.labels[driverResource.id()],
225
- entityViewRoute: !idValue
226
- ? this.rootPath+'/'+driverResource.entityPath
227
- : this.generateEntityRoute('viewPath', driverResource, idProperty, loadedEntity)
228
- };
229
- await this.emitEvent('reldens.adminEditPropertiesPopulation', {
230
- req,
231
- driverResource,
232
- renderedEditProperties,
233
- loadedEntity,
234
- entityId: driverResource.id(),
235
- entityData: loadedEntity
236
- });
237
- for(let propertyKey of Object.keys(driverResource.options.properties)){
238
- let property = driverResource.options.properties[propertyKey];
239
- let fieldDisabled = -1 === driverResource.options.editProperties.indexOf(propertyKey);
240
- let fieldValue = await this.generatePropertyEditRenderedValue(
241
- loadedEntity,
242
- propertyKey,
243
- property,
244
- fieldDisabled
245
- );
246
- let templateData = {
247
- fieldName: propertyKey,
248
- fieldValue,
249
- fieldDisabled: fieldDisabled ? ' disabled="disabled"' : '',
250
- required: (!property.isUpload || !loadedEntity) && property.isRequired ? ' required="required"' : '',
251
- multiple: property.isArray ? ' multiple="multiple"' : '',
252
- inputType: this.getInputType(property, propertyKey, fieldDisabled),
253
- isArray: property.isArray || '',
254
- arraySeparator: property.isArray || '',
255
- isRequired: property.isRequired || false
256
- };
257
- if(property.isUpload && fieldValue && '' !== fieldValue){
258
- let filesArray = property.isArray ? fieldValue.split(property.isArray) : [fieldValue];
259
- templateData.filesArray = filesArray.map((filename) => {
260
- return {
261
- filename,
262
- isRemovable: !property.isRequired || filesArray.length > 1
263
- };
264
- });
265
- }
266
- await this.emitEvent('reldens.adminBeforeFieldRender', {
267
- req,
268
- driverResource,
269
- propertyKey,
270
- property,
271
- templateData,
272
- loadedEntity,
273
- renderedEditProperties,
274
- adminContentsRender: this.adminContentsRender.bind(this),
275
- adminFilesContents: this.adminFilesContents
276
- });
277
- renderedEditProperties[propertyKey] = await this.adminContentsRender(
278
- this.adminFilesContents.fields.edit[this.propertyType(property, 'edit')],
279
- templateData
280
- );
281
- }
282
- return await this.adminContentsRenderRoute(
283
- await this.adminContentsRender(this.adminContentsEntities()[entityPath].edit, renderedEditProperties),
284
- this.adminContentsSideBar()
285
- );
286
- }
287
-
288
- async processDeleteEntities(req, res, driverResource, entityPath)
289
- {
290
- let ids = req?.body?.ids;
291
- if('string' === typeof ids){
292
- ids = ids.split(',');
293
- }
294
- let redirectPath = this.rootPath+'/'+entityPath+'?result=';
295
- if(!ids || 0 === ids.length){
296
- return redirectPath + 'errorMissingId';
297
- }
298
- try {
299
- let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
300
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
301
- let idsFilter = {[idProperty]: {operator: 'IN', value: ids}};
302
- let loadedEntities = await entityRepository.load(idsFilter);
303
- let deleteControl = {prevented: false, result: ''};
304
- await this.emitEvent('reldens.adminBeforeEntityDelete', {
305
- driverResource,
306
- entityPath,
307
- idProperty,
308
- ids,
309
- loadedEntities,
310
- deleteControl
311
- });
312
- if(deleteControl.prevented){
313
- return redirectPath + (deleteControl.result || 'errorDeletePrevented');
314
- }
315
- await this.deleteEntitiesRelatedFiles(driverResource, loadedEntities);
316
- let deleteResult = await entityRepository.delete(idsFilter);
317
- await this.emitEvent('reldens.adminAfterEntityDelete', {driverResource, idProperty, ids});
318
- return redirectPath + (deleteResult ? 'success' : 'errorStorageFailure');
319
- } catch (error) {
320
- return redirectPath + 'errorDeleteFailure';
321
- }
322
- }
323
-
324
- async processSaveEntity(req, res, driverResource, entityPath)
325
- {
326
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
327
- let id = (req?.body[idProperty] || '');
328
- let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
329
- let loadedEntity = id ? await this.loadEntityById(driverResource, id) : null;
330
- let uploadProperties = this.fetchUploadProperties(driverResource);
331
- if(0 < Object.keys(uploadProperties).length){
332
- if(this.uploaderFactory?.error?.message){
333
- Logger.error('Upload factory error detected', this.uploaderFactory.error);
334
- return this.rootPath+'/'+entityPath+'?result=uploadError&error='
335
- +encodeURIComponent(this.uploaderFactory.error.message);
336
- }
337
- let uploadValidationResult = this.validateUploadedFiles(req, uploadProperties, loadedEntity);
338
- if(!uploadValidationResult.success){
339
- Logger.error('Upload validation failed', uploadValidationResult.error);
340
- return this.rootPath+'/'+entityPath+'?result=uploadValidationError&error='
341
- +encodeURIComponent(uploadValidationResult.error.message);
342
- }
343
- }
344
- let entityDataPatch = this.preparePatchData(
345
- driverResource,
346
- idProperty,
347
- req,
348
- driverResource.options.properties,
349
- id,
350
- loadedEntity
351
- );
352
- if(!entityDataPatch){
353
- Logger.error('Bad patch data.', entityDataPatch);
354
- return this.rootPath+'/'+entityPath+'?result=saveBadPatchData';
355
- }
356
- try {
357
- let saveResult = await this.saveEntity(id, entityRepository, entityDataPatch);
358
- if(!saveResult){
359
- Logger.error('Save result error.', saveResult, entityDataPatch);
360
- return this.appendResultParam(
361
- this.generateEntityRoute('editPath', driverResource, idProperty, null, id),
362
- 'saveEntityStorageError'
363
- );
364
- }
365
- await this.emitEvent('reldens.adminAfterEntitySave', {
366
- req,
367
- res,
368
- driverResource,
369
- entityPath,
370
- entityData: saveResult
371
- });
372
- if(sc.isFunction(this.autoSyncDistCallback)){
373
- let uploadProperties = this.fetchUploadProperties(driverResource);
374
- if(0 < Object.keys(uploadProperties).length){
375
- for(let uploadPropertyKey of Object.keys(uploadProperties)){
376
- let property = uploadProperties[uploadPropertyKey];
377
- await this.autoSyncDistCallback(
378
- property.bucket,
379
- saveResult[uploadPropertyKey],
380
- property.distFolder
381
- );
382
- }
383
- }
384
- }
385
- let saveAction = sc.get(req.body, 'saveAction', 'save');
386
- if('saveAndContinue' === saveAction){
387
- return this.appendResultParam(
388
- this.generateEntityRoute('editPath', driverResource, idProperty, saveResult),
389
- 'success'
390
- );
391
- }
392
- if('saveAndGoBack' === saveAction){
393
- return this.rootPath+'/'+entityPath+'?result=success';
394
- }
395
- return this.generateEntityRoute('viewPath', driverResource, idProperty, saveResult) +'&result=success';
396
- } catch (error) {
397
- Logger.error('Save entity error.', error);
398
- return this.rootPath+'/'+entityPath+'?result=saveEntityError';
399
- }
400
- }
401
-
402
- validateUploadedFiles(req, uploadProperties, loadedEntity)
403
- {
404
- for(let uploadPropertyKey of Object.keys(uploadProperties)){
405
- let property = uploadProperties[uploadPropertyKey];
406
- let uploadedFiles = req.files?.[uploadPropertyKey];
407
- if(!uploadedFiles || 0 === uploadedFiles.length){
408
- if(property.isRequired){
409
- let isExplicitClear = '1' === sc.get(req.body, 'clear_'+uploadPropertyKey, false);
410
- if(isExplicitClear){
411
- return {success: false, error: {message: 'Required file upload missing: '+uploadPropertyKey}};
412
- }
413
- let existingValue = loadedEntity ? sc.get(loadedEntity, uploadPropertyKey, '') : '';
414
- if(!existingValue || '' === existingValue){
415
- return {success: false, error: {message: 'Required file upload missing: '+uploadPropertyKey}};
416
- }
417
- }
418
- continue;
419
- }
420
- for(let file of uploadedFiles){
421
- let filePath = file.path;
422
- if(!FileHandler.exists(filePath)){
423
- return {
424
- success: false,
425
- error: {
426
- message: 'Uploaded file not found in bucket: '+file.filename,
427
- filePath,
428
- bucket: property.bucket
429
- }
430
- };
431
- }
432
- if(!FileHandler.isFile(filePath)){
433
- return {
434
- success: false,
435
- error: {message: 'Uploaded file is not a valid file: '+file.filename, filePath}
436
- };
437
- }
438
- }
439
- }
440
- return {success: true};
441
- }
442
-
443
- async countTotalEntities(driverResource, filters)
444
- {
445
- let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
446
- if(!entityRepository){
447
- return false;
448
- }
449
- return await entityRepository.count(filters);
450
- }
451
-
452
- async loadEntitiesForList(driverResource, pageSize, currentPage, req, filters, sortBy, sortDirection)
453
- {
454
- let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
455
- entityRepository.limit = pageSize;
456
- if(1 < currentPage){
457
- entityRepository.offset = (currentPage - 1) * pageSize;
458
- }
459
- entityRepository.sortBy = sortBy || false;
460
- entityRepository.sortDirection = sortDirection || false;
461
- let loadedEntities = await entityRepository.loadWithRelations(filters, []);
462
- entityRepository.limit = 0;
463
- entityRepository.offset = 0;
464
- entityRepository.sortBy = false;
465
- entityRepository.sortDirection = false;
466
- let entityRows = [];
467
- let resourceProperties = driverResource.options?.properties;
468
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
469
- for(let entity of loadedEntities){
470
- let fields = [];
471
- for(let property of driverResource.options.listProperties){
472
- let {fieldValue, fieldName} = this.generatePropertyRenderedValueWithLabel(
473
- entity,
474
- property,
475
- resourceProperties[property]
476
- );
477
- fields.push({
478
- name: property,
479
- value: await this.generatePropertyRenderedValue(
480
- fieldValue,
481
- fieldName,
482
- resourceProperties[property]
483
- ),
484
- viewLink: '' !== idProperty
485
- ? this.generateEntityRoute('viewPath', driverResource, idProperty, entity)
486
- : ''
487
- });
488
- }
489
- entityRows.push({
490
- fields,
491
- editLink: '' !== idProperty
492
- ? this.generateEntityRoute('editPath', driverResource, idProperty, entity)
493
- : '',
494
- deleteLink: this.rootPath + '/' + driverResource.entityPath + this.deletePath,
495
- id: entity[idProperty]
496
- });
497
- }
498
- return entityRows;
499
- }
500
-
501
- async loadEntityById(driverResource, id)
502
- {
503
- let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
504
- if(!entityRepository){
505
- return false;
506
- }
507
- await this.emitEvent('reldens.adminBeforeEntityLoad', {
508
- driverResource,
509
- entityId: id
510
- });
511
- let idProperty = this.fetchEntityIdPropertyKey(driverResource);
512
- if(!idProperty){
513
- return false;
514
- }
515
- return await entityRepository.loadOneByWithRelations(idProperty, id);
516
- }
517
-
518
- async saveEntity(id, entityRepository, entityDataPatch)
519
- {
520
- if('' === id){
521
- return entityRepository.create(entityDataPatch);
522
- }
523
- return entityRepository.updateById(id, entityDataPatch);
524
- }
525
-
526
- async deleteEntitiesRelatedFiles(driverResource, entities)
527
- {
528
- for(let propertyKey of Object.keys(driverResource.options.properties)){
529
- let property = driverResource.options.properties[propertyKey];
530
- if(!property.isUpload){
531
- continue;
532
- }
533
- for(let entity of entities){
534
- let bucket = sc.get(property, 'bucket', '');
535
- if(!property.isArray){
536
- FileHandler.remove([bucket, entity[propertyKey]]);
537
- continue;
538
- }
539
- for(let entityFile of entity[propertyKey].split(property.isArray)){
540
- FileHandler.remove([bucket, entityFile]);
541
- }
542
- }
543
- }
544
- }
545
-
546
- preparePatchData(driverResource, idProperty, req, resourceProperties, id, loadedEntity)
547
- {
548
- let entityDataPatch = {};
549
- for(let i of Object.keys(resourceProperties)){
550
- if(i === idProperty){
551
- continue;
552
- }
553
- let property = resourceProperties[i];
554
- let shouldProcess = driverResource.options.editProperties.includes(i);
555
- if(!id && !shouldProcess && property.isRequired && 'reference' === property.type){
556
- shouldProcess = true;
557
- }
558
- if(!shouldProcess){
559
- continue;
560
- }
561
- let clearFieldParam = sc.get(req.body, 'clear_'+i, false);
562
- let isExplicitClear = '1' === clearFieldParam;
563
- let propertyUpdateValue = sc.get(req.body, i, null);
564
- if('null' === propertyUpdateValue){
565
- propertyUpdateValue = null;
566
- }
567
- let propertyType = sc.get(property, 'type', 'string');
568
- if(property.isUpload){
569
- propertyType = 'upload';
570
- if(isExplicitClear){
571
- propertyUpdateValue = null;
572
- }
573
- if(!isExplicitClear){
574
- let currentValue = loadedEntity ? sc.get(loadedEntity, i, '') : '';
575
- propertyUpdateValue = this.prepareUploadPatchData(req, i, currentValue, property);
576
- }
577
- }
578
- if('boolean' === propertyType){
579
- propertyUpdateValue = '1' === propertyUpdateValue || 'on' === propertyUpdateValue;
580
- }
581
- let isEmpty = '' === propertyUpdateValue;
582
- let isNull = null === propertyUpdateValue;
583
- if('number' === propertyType && !isNull && !isEmpty){
584
- let numValue = Number(propertyUpdateValue);
585
- if(isNaN(numValue)){
586
- Logger.critical(
587
- 'Invalid number value for property.',
588
- {propertyKey: i, value: propertyUpdateValue, entity: driverResource.entityKey}
589
- );
590
- return false;
591
- }
592
- propertyUpdateValue = numValue;
593
- }
594
- if('string' === propertyType && !isNull && !isEmpty){
595
- propertyUpdateValue = String(propertyUpdateValue);
596
- }
597
- if(isEmpty && !property.isRequired){
598
- propertyUpdateValue = null;
599
- }
600
- let isUploadCreate = property.isUpload && !id;
601
- if(property.isRequired && (isNull || isEmpty) && (!property.isUpload || isUploadCreate)){
602
- Logger.critical(
603
- 'Missing property on prepare patch data.',
604
- {propertyKey: i, config: property, propertyUpdateValue, entity: driverResource.entityKey}
605
- );
606
- return false;
607
- }
608
- if(-1 !== this.passwordFieldNames.indexOf(i)){
609
- if(!propertyUpdateValue || '' === propertyUpdateValue){
610
- continue;
611
- }
612
- }
613
- if(!property.isUpload || (property.isUpload && (!isNull || isExplicitClear))){
614
- entityDataPatch[i] = propertyUpdateValue;
615
- }
616
- }
617
- return entityDataPatch;
618
- }
619
-
620
-
621
- prepareUploadPatchData(req, i, propertyUpdateValue, property)
622
- {
623
- let filesData = sc.get(req.files, i, null);
624
- let removedFiles = sc.get(req.body, 'removed_'+i, '');
625
- let existingFiles = propertyUpdateValue || '';
626
- if(property.isArray && removedFiles && '' !== removedFiles){
627
- let removedFilesArray = removedFiles.split(',');
628
- let existingFilesArray = existingFiles ? existingFiles.split(property.isArray) : [];
629
- existingFilesArray = existingFilesArray.filter(file => {
630
- return -1 === removedFilesArray.indexOf(file);
631
- });
632
- existingFiles = existingFilesArray.join(property.isArray);
633
- }
634
- if(!filesData || 0 === filesData.length){
635
- if(removedFiles && '' !== removedFiles){
636
- return existingFiles;
637
- }
638
- return null;
639
- }
640
- let newFileNames = [];
641
- for(let file of filesData){
642
- newFileNames.push(file.filename);
643
- }
644
- if(property.isArray && existingFiles && '' !== existingFiles){
645
- let existingFilesArray = existingFiles.split(property.isArray);
646
- let allFiles = existingFilesArray.concat(newFileNames);
647
- return allFiles.join(property.isArray);
648
- }
649
- return newFileNames.join(property.isArray);
650
- }
651
-
652
- async generatePropertyRenderedValue(fieldValue, fieldName, resourceProperty, templateType)
653
- {
654
- let fieldOriginalValue = fieldValue;
655
- if('view' === templateType){
656
- if(resourceProperty.isArray){
657
- fieldValue = fieldValue.split(resourceProperty.isArray).map((value) => {
658
- let target = resourceProperty.isUpload ? ' target="_blank"' : '';
659
- let fieldValuePart = resourceProperty.isUpload && resourceProperty.bucketPath && value
660
- ? resourceProperty.bucketPath+value
661
- : value;
662
- return {fieldValuePart, fieldOriginalValuePart: value, target};
663
- });
664
- }
665
- if(!resourceProperty.isArray && resourceProperty.isUpload && fieldValue){
666
- fieldValue = resourceProperty.bucketPath+fieldValue;
667
- }
668
- }
669
- return await this.adminContentsRender(
670
- this.adminFilesContents.fields.view[this.propertyType(resourceProperty, templateType, fieldValue)],
671
- {fieldName, fieldValue, fieldOriginalValue, target: ' target="_blank"'}
672
- );
673
- }
674
-
675
- generatePropertyRenderedValueWithLabel(entity, propertyKey, resourceProperty)
676
- {
677
- let fieldValue = (0 === entity[propertyKey] ? '0' : entity[propertyKey] || '');
678
- if((sc.isObject(fieldValue) || sc.isArray(fieldValue)) && 'json' === resourceProperty.dbType){
679
- fieldValue = sc.toJsonString(fieldValue);
680
- }
681
- let fieldName = propertyKey;
682
- if('boolean' === resourceProperty.type){
683
- fieldValue = 1 === entity[propertyKey]
684
- || '1' === entity[propertyKey]
685
- || true === entity[propertyKey] ? 'Yes' : 'No';
686
- }
687
- if('datetime' === resourceProperty.type){
688
- fieldValue = '' !== fieldValue ? sc.formatDate(new Date(fieldValue)) : '';
689
- }
690
- if('reference' === resourceProperty.type){
691
- let relationEntity = entity[resourceProperty.alias || 'related_'+resourceProperty.reference];
692
- if(relationEntity){
693
- let relation = this.relations()[resourceProperty.reference];
694
- if(relation){
695
- let relationTitleProperty = relation[resourceProperty.alias || resourceProperty.reference];
696
- if(relationTitleProperty && '' !== String(relationEntity[relationTitleProperty] || '')){
697
- fieldName = relationTitleProperty;
698
- fieldValue = relationEntity[relationTitleProperty]+(' ('+fieldValue+')');
699
- }
700
- }
701
- }
702
- }
703
- if(resourceProperty.availableValues){
704
- let optionData = resourceProperty.availableValues.filter((availableValue) => {
705
- return String(availableValue.value) === String(fieldValue);
706
- }).shift();
707
- if(optionData){
708
- fieldValue = optionData.label + ' (' + fieldValue + ')';
709
- }
710
- }
711
- return {fieldValue, fieldName};
712
- }
713
-
714
- async generatePropertyEditRenderedValue(entity, propertyKey, resourceProperty)
715
- {
716
- if(-1 !== this.passwordFieldNames.indexOf(propertyKey)){
717
- return '';
718
- }
719
- let entityPropertyValue = sc.get(entity, propertyKey, null);
720
- let fieldValue = (0 === entityPropertyValue ? '0' : entityPropertyValue || '');
721
- if('null' === fieldValue){
722
- fieldValue = '';
723
- }
724
- if((sc.isObject(fieldValue) || sc.isArray(fieldValue)) && 'json' === resourceProperty.dbType){
725
- fieldValue = sc.toJsonString(fieldValue);
726
- }
727
- if('boolean' === resourceProperty.type){
728
- fieldValue = 1 === entityPropertyValue
729
- || '1' === entityPropertyValue
730
- || true === entityPropertyValue ? ' checked="checked"' : '';
731
- }
732
- if('datetime' === resourceProperty.type){
733
- fieldValue = !entityPropertyValue || '' === entityPropertyValue
734
- ? ''
735
- : sc.formatDate(new Date(entityPropertyValue), 'Y-m-d H:i:s');
736
- }
737
- if('reference' === resourceProperty.type){
738
- let relationDriverResource = this.resourcesByReference()[resourceProperty.reference];
739
- if(!relationDriverResource){
740
- Logger.critical('Driver resource not found by reference.', resourceProperty);
741
- return fieldValue;
742
- }
743
- let relation = this.relations()[resourceProperty.reference];
744
- let relationKey = resourceProperty.alias || resourceProperty.reference;
745
- let relationTitleProperty = relation
746
- ? relation[relationKey]
747
- : this.fetchEntityIdPropertyKey(relationDriverResource);
748
- let options = (await this.fetchRelationOptions(relationDriverResource)).map((option) => {
749
- let value = option[this.fetchEntityIdPropertyKey(relationDriverResource)];
750
- return {
751
- label: option[relationTitleProperty]+' (ID: '+value+')',
752
- value,
753
- selected: entity && entity[propertyKey] === value ? ' selected="selected"' : ''
754
- };
755
- });
756
- if(!resourceProperty.isRequired){
757
- let isSelected = !entity || null === entity[propertyKey] || '' === entity[propertyKey];
758
- options.unshift({
759
- label: '-- Select --',
760
- value: '',
761
- selected: isSelected ? ' selected="selected"' : ''
762
- });
763
- }
764
- return options;
765
- }
766
- return fieldValue;
767
- }
768
-
769
- async fetchRelationOptions(relationDriverResource)
770
- {
771
- let relationEntityRepository = this.dataServer.getEntity(relationDriverResource.entityKey);
772
- if(!relationEntityRepository){
773
- return [];
774
- }
775
- return await relationEntityRepository.loadAll();
776
- }
777
-
778
- propertyType(resourceProperty, templateType, fieldValue)
779
- {
780
- let propertyType = sc.get(resourceProperty, 'type', 'text');
781
- if('reference' === propertyType && 'edit' === templateType){
782
- return 'select';
783
- }
784
- if(resourceProperty.isUpload){
785
- if('edit' === templateType){
786
- return 'file';
787
- }
788
- if('view' === templateType){
789
- if(!fieldValue || '' === fieldValue){
790
- return 'text';
791
- }
792
- let multiple = resourceProperty.isArray ? 's' : '';
793
- let allowedTypes = sc.get(resourceProperty, 'allowedTypes', '');
794
- if('text' === allowedTypes){
795
- return 'link'+multiple;
796
- }
797
- if('' !== allowedTypes){
798
- let templateName = allowedTypes + multiple;
799
- if(sc.hasOwn(this.adminFilesContents.fields.view, templateName)){
800
- return templateName;
801
- }
802
- }
803
- return 'text';
804
- }
805
- }
806
- if('textarea' === resourceProperty.type){
807
- return 'textarea';
808
- }
809
- if(-1 !== ['reference', 'number', 'datetime'].indexOf(propertyType)){
810
- propertyType = 'text';
811
- }
812
- return propertyType;
813
- }
814
-
815
- getInputType(resourceProperty, propertyKey, fieldDisabled)
816
- {
817
- if(-1 !== this.passwordFieldNames.indexOf(propertyKey) && !fieldDisabled){
818
- return 'password';
819
- }
820
- if('datetime' === resourceProperty.type && !fieldDisabled){
821
- return 'datetime-local';
822
- }
823
- if('number' === resourceProperty.type){
824
- return 'number';
825
- }
826
- return 'text';
827
- }
828
-
829
- generateEntityRoute(routeType, driverResource, idProperty, entity, entityId)
830
- {
831
- if(!idProperty || (!entity && !entityId)){
832
- return this.rootPath + '/' + driverResource.entityPath;
833
- }
834
- let idParam = '?' + idProperty + '=';
835
- if(entity){
836
- idParam = idParam + entity[idProperty];
837
- }
838
- if(entityId){
839
- idParam = idParam + entityId;
840
- }
841
- return this.rootPath + '/' + driverResource.entityPath + this[routeType] + idParam;
842
- }
843
-
844
- appendResultParam(route, result)
845
- {
846
- if(route.includes('?')){
847
- return route+'&result='+result;
848
- }
849
- return route+'?result='+result;
850
- }
851
-
852
- }
853
-
854
- module.exports.RouterContents = RouterContents;
1
+ /**
2
+ *
3
+ * Reldens - RouterContents
4
+ *
5
+ */
6
+
7
+ const { PageRangeProvider, Logger, sc } = require('@reldens/utils');
8
+ const { FileHandler } = require('@reldens/server-utils');
9
+ const { AdminFiltersManager } = require('./admin-filters-manager');
10
+
11
+ class RouterContents
12
+ {
13
+
14
+ constructor(props)
15
+ {
16
+ this.dataServer = props.dataServer;
17
+ this.translations = props.translations;
18
+ this.rootPath = props.rootPath;
19
+ this.relations = props.relations;
20
+ this.resourcesByReference = props.resourcesByReference;
21
+ this.adminFilesContents = props.adminFilesContents;
22
+ this.autoSyncDistCallback = props.autoSyncDistCallback;
23
+ this.viewPath = props.viewPath;
24
+ this.editPath = props.editPath;
25
+ this.deletePath = props.deletePath;
26
+ this.emitEvent = props.emitEvent;
27
+ this.adminContentsRender = props.adminContentsRender;
28
+ this.adminContentsRenderRoute = props.adminContentsRenderRoute;
29
+ this.adminContentsEntities = props.adminContentsEntities;
30
+ this.adminContentsSideBar = props.adminContentsSideBar;
31
+ this.fetchTranslation = props.fetchTranslation;
32
+ this.fetchEntityIdPropertyKey = props.fetchEntityIdPropertyKey;
33
+ this.fetchUploadProperties = props.fetchUploadProperties;
34
+ this.uploaderFactory = props.uploaderFactory;
35
+ this.filtersManager = new AdminFiltersManager();
36
+ this.passwordFieldNames = props.passwordFieldNames || ['password'];
37
+ }
38
+
39
+ async generateListRouteContent(req, driverResource, entityPath)
40
+ {
41
+ let currentPage = Number(req?.query?.page || 1);
42
+ let pageSize = Number(req?.query?.pageSize || 25);
43
+ let shouldClearFilters = 'true' === req?.query?.clearFilters;
44
+ let sessionFilters = this.filtersManager.getFiltersFromSession(req, entityPath);
45
+ let sessionSorting = this.filtersManager.getSortingFromSession(req, entityPath);
46
+ let filtersFromParams = shouldClearFilters ? {} : req?.body?.filters || req?.query?.filters || {};
47
+ let entityFilterTerm = shouldClearFilters ? '' : sc.get(req?.body, 'entityFilterTerm', '');
48
+ let sortByFromParams = shouldClearFilters ? '' : sc.get(req?.body, 'sortBy', sc.get(req?.query, 'sortBy', ''));
49
+ let sortDirectionFromParams = shouldClearFilters ? '' : sc.get(req?.body, 'sortDirection', sc.get(req?.query, 'sortDirection', ''));
50
+ if(shouldClearFilters){
51
+ this.filtersManager.clearFiltersFromSession(req, entityPath);
52
+ this.filtersManager.clearSortingFromSession(req, entityPath);
53
+ }
54
+ let hasNewEntityFilterTerm = entityFilterTerm && '' !== entityFilterTerm;
55
+ let hasNewToggleFilters = Object.keys(filtersFromParams).length > 0;
56
+ if(hasNewEntityFilterTerm && hasNewToggleFilters){
57
+ filtersFromParams = {};
58
+ hasNewToggleFilters = false;
59
+ }
60
+ if(hasNewEntityFilterTerm || hasNewToggleFilters){
61
+ let filtersToSave = {
62
+ regular: hasNewToggleFilters ? filtersFromParams : {},
63
+ entityFilterTerm: hasNewEntityFilterTerm ? entityFilterTerm : ''
64
+ };
65
+ this.filtersManager.saveFiltersToSession(req, entityPath, filtersToSave);
66
+ sessionFilters = filtersToSave;
67
+ }
68
+ if(sortByFromParams && sortDirectionFromParams){
69
+ this.filtersManager.saveSortingToSession(req, entityPath, sortByFromParams, sortDirectionFromParams);
70
+ sessionSorting = {sortBy: sortByFromParams, sortDirection: sortDirectionFromParams};
71
+ }
72
+ let mergedFilters = shouldClearFilters ? {} : Object.assign({}, sessionFilters.regular || {});
73
+ let finalEntityFilterTerm = shouldClearFilters ? '' : sessionFilters.entityFilterTerm || '';
74
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
75
+ let finalSortBy = sessionSorting.sortBy || idProperty;
76
+ let finalSortDirection = sessionSorting.sortDirection || 'ASC';
77
+ let filters = this.filtersManager.prepareFilters(mergedFilters, driverResource);
78
+ let textFilters = this.filtersManager.prepareTextFilters(finalEntityFilterTerm, driverResource);
79
+ let combinedFilters = this.filtersManager.combineFilters(filters, textFilters);
80
+ let totalEntities = await this.countTotalEntities(driverResource, combinedFilters);
81
+ let totalPages = totalEntities <= pageSize ? 1 : Math.ceil(totalEntities / pageSize);
82
+ let renderedPagination = '';
83
+ for(let paginationItem of PageRangeProvider.fetch(currentPage, totalPages)){
84
+ let paginationUrl = this.rootPath+'/'+driverResource.entityPath+'?page='+ paginationItem.value;
85
+ if(finalEntityFilterTerm){
86
+ paginationUrl += '&entityFilterTerm='+encodeURIComponent(finalEntityFilterTerm);
87
+ }
88
+ if(finalSortBy){
89
+ paginationUrl += '&sortBy='+encodeURIComponent(finalSortBy);
90
+ }
91
+ if(finalSortDirection){
92
+ paginationUrl += '&sortDirection='+encodeURIComponent(finalSortDirection);
93
+ }
94
+ for(let filterKey of Object.keys(mergedFilters)){
95
+ if(mergedFilters[filterKey]){
96
+ paginationUrl += '&filters['+filterKey+']='+encodeURIComponent(mergedFilters[filterKey]);
97
+ }
98
+ }
99
+ renderedPagination += await this.adminContentsRender(
100
+ this.adminFilesContents.fields.view['link'],
101
+ {
102
+ fieldName: paginationItem.label,
103
+ fieldValue: paginationUrl,
104
+ fieldOriginalValue: paginationItem.value,
105
+ }
106
+ );
107
+ }
108
+ let listProperties = {
109
+ entityNewRoute: this.rootPath+'/'+driverResource.entityPath+this.editPath,
110
+ entityFilterTermValue: finalEntityFilterTerm || ''
111
+ };
112
+ await this.emitEvent('reldens.adminListPropertiesPopulation', {
113
+ req,
114
+ driverResource,
115
+ listProperties
116
+ });
117
+ return await this.adminContentsRenderRoute(
118
+ await this.adminContentsRender(
119
+ this.adminContentsEntities()[entityPath].list,
120
+ Object.assign({
121
+ list: await this.adminContentsRender(this.adminFilesContents.listContent, {
122
+ deletePath: this.rootPath + '/' + driverResource.entityPath + this.deletePath,
123
+ fieldsHeaders: driverResource.options.listProperties.map((property) => {
124
+ let propertyTitle = this.fetchTranslation(property, driverResource.id());
125
+ let alias = this.fetchTranslation(
126
+ driverResource.options.properties[property]?.alias || '',
127
+ driverResource.id()
128
+ );
129
+ let isSorted = finalSortBy === property;
130
+ let sortDirection = isSorted ? finalSortDirection.toLowerCase() : '';
131
+ return {
132
+ name: property,
133
+ value: '' !== alias ? alias + ' ('+propertyTitle+')' : propertyTitle,
134
+ isSorted,
135
+ sortDirection,
136
+ isSortedAsc: isSorted && 'asc' === sortDirection,
137
+ isSortedDesc: isSorted && 'desc' === sortDirection
138
+ };
139
+ }),
140
+ rows: await this.loadEntitiesForList(
141
+ driverResource,
142
+ pageSize,
143
+ currentPage,
144
+ req,
145
+ combinedFilters,
146
+ finalSortBy,
147
+ finalSortDirection
148
+ )
149
+ }),
150
+ pagination: renderedPagination,
151
+ extraContentForList: sc.get(listProperties, 'extraContentForList', ''),
152
+ entityFilterTermValue: listProperties.entityFilterTermValue
153
+ }, ...driverResource.options.filterProperties.map((property) => {
154
+ let filterValue = (mergedFilters[property] || '');
155
+ return {[property]: '' === filterValue ? '' : 'value="'+filterValue+'"'};
156
+ }))
157
+ ),
158
+ this.adminContentsSideBar()
159
+ );
160
+ }
161
+
162
+ async generateViewRouteContent(req, driverResource, entityPath)
163
+ {
164
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
165
+ let id = (sc.get(req.query, idProperty, ''));
166
+ if('' === id){
167
+ Logger.error('Missing ID on view route.', entityPath, id, idProperty);
168
+ return '';
169
+ }
170
+ let loadedEntity = await this.loadEntityById(driverResource, id);
171
+ if(!loadedEntity){
172
+ Logger.critical('Entity not found on view route.', entityPath, id, idProperty);
173
+ return '';
174
+ }
175
+ let renderedViewProperties = {
176
+ entityEditRoute: this.generateEntityRoute('editPath', driverResource, idProperty, loadedEntity),
177
+ entityNewRoute: this.rootPath+'/'+driverResource.entityPath+this.editPath,
178
+ id
179
+ };
180
+ let entitySerializedData = {};
181
+ for(let propertyKey of driverResource.options.showProperties){
182
+ let property = driverResource.options.properties[propertyKey];
183
+ let {fieldValue, fieldName} = this.generatePropertyRenderedValueWithLabel(
184
+ loadedEntity,
185
+ propertyKey,
186
+ property
187
+ );
188
+ entitySerializedData[fieldName] = fieldValue;
189
+ renderedViewProperties[propertyKey] = await this.generatePropertyRenderedValue(
190
+ fieldValue,
191
+ fieldName,
192
+ property,
193
+ 'view'
194
+ );
195
+ }
196
+ let extraDataEvent = {entitySerializedData, entityId: driverResource.id(), entity: loadedEntity};
197
+ await this.emitEvent('adminEntityExtraData', extraDataEvent);
198
+ renderedViewProperties.entitySerializedData = JSON.stringify(extraDataEvent.entitySerializedData)
199
+ .replace(/"/g, '&quot;');
200
+ renderedViewProperties.extraContentForViewTop = '';
201
+ renderedViewProperties.extraContentForViewBottom = '';
202
+ await this.emitEvent('reldens.adminViewPropertiesPopulation', {
203
+ idProperty,
204
+ req,
205
+ driverResource,
206
+ loadedEntity,
207
+ renderedViewProperties
208
+ });
209
+ return await this.adminContentsRenderRoute(
210
+ await this.adminContentsRender(this.adminContentsEntities()[entityPath].view, renderedViewProperties),
211
+ this.adminContentsSideBar()
212
+ );
213
+ }
214
+
215
+ async generateEditRouteContent(req, driverResource, entityPath)
216
+ {
217
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
218
+ let idValue = String(sc.get(req?.query, idProperty, ''));
219
+ let loadedEntity = !idValue ? null : await this.loadEntityById(driverResource, idValue);
220
+ let renderedEditProperties = {
221
+ idValue,
222
+ idProperty,
223
+ idPropertyLabel: this.fetchTranslation(idProperty, driverResource.id()),
224
+ templateTitle: (!idValue ? 'Create' : 'Edit')+' '+this.translations.labels[driverResource.id()],
225
+ entityViewRoute: !idValue
226
+ ? this.rootPath+'/'+driverResource.entityPath
227
+ : this.generateEntityRoute('viewPath', driverResource, idProperty, loadedEntity)
228
+ };
229
+ await this.emitEvent('reldens.adminEditPropertiesPopulation', {
230
+ req,
231
+ driverResource,
232
+ renderedEditProperties,
233
+ loadedEntity,
234
+ entityId: driverResource.id(),
235
+ entityData: loadedEntity
236
+ });
237
+ for(let propertyKey of Object.keys(driverResource.options.properties)){
238
+ let property = driverResource.options.properties[propertyKey];
239
+ let fieldDisabled = -1 === driverResource.options.editProperties.indexOf(propertyKey);
240
+ let fieldValue = await this.generatePropertyEditRenderedValue(
241
+ loadedEntity,
242
+ propertyKey,
243
+ property,
244
+ fieldDisabled
245
+ );
246
+ let templateData = {
247
+ fieldName: propertyKey,
248
+ fieldValue,
249
+ fieldDisabled: fieldDisabled ? ' disabled="disabled"' : '',
250
+ required: (!property.isUpload || !loadedEntity) && property.isRequired ? ' required="required"' : '',
251
+ multiple: property.isArray ? ' multiple="multiple"' : '',
252
+ inputType: this.getInputType(property, propertyKey, fieldDisabled),
253
+ isArray: property.isArray || '',
254
+ arraySeparator: property.isArray || '',
255
+ isRequired: property.isRequired || false
256
+ };
257
+ if(property.isUpload && fieldValue && '' !== fieldValue){
258
+ let filesArray = property.isArray ? fieldValue.split(property.isArray) : [fieldValue];
259
+ templateData.filesArray = filesArray.map((filename) => {
260
+ return {
261
+ filename,
262
+ isRemovable: !property.isRequired || filesArray.length > 1
263
+ };
264
+ });
265
+ }
266
+ await this.emitEvent('reldens.adminBeforeFieldRender', {
267
+ req,
268
+ driverResource,
269
+ propertyKey,
270
+ property,
271
+ templateData,
272
+ loadedEntity,
273
+ renderedEditProperties,
274
+ adminContentsRender: this.adminContentsRender.bind(this),
275
+ adminFilesContents: this.adminFilesContents
276
+ });
277
+ renderedEditProperties[propertyKey] = await this.adminContentsRender(
278
+ this.adminFilesContents.fields.edit[this.propertyType(property, 'edit')],
279
+ templateData
280
+ );
281
+ }
282
+ return await this.adminContentsRenderRoute(
283
+ await this.adminContentsRender(this.adminContentsEntities()[entityPath].edit, renderedEditProperties),
284
+ this.adminContentsSideBar()
285
+ );
286
+ }
287
+
288
+ async processDeleteEntities(req, res, driverResource, entityPath)
289
+ {
290
+ let ids = req?.body?.ids;
291
+ if('string' === typeof ids){
292
+ ids = ids.split(',');
293
+ }
294
+ let redirectPath = this.rootPath+'/'+entityPath+'?result=';
295
+ if(!ids || 0 === ids.length){
296
+ return redirectPath + 'errorMissingId';
297
+ }
298
+ try {
299
+ let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
300
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
301
+ let idsFilter = {[idProperty]: {operator: 'IN', value: ids}};
302
+ let loadedEntities = await entityRepository.load(idsFilter);
303
+ let deleteControl = {prevented: false, result: ''};
304
+ await this.emitEvent('reldens.adminBeforeEntityDelete', {
305
+ driverResource,
306
+ entityPath,
307
+ idProperty,
308
+ ids,
309
+ loadedEntities,
310
+ deleteControl
311
+ });
312
+ if(deleteControl.prevented){
313
+ return redirectPath + (deleteControl.result || 'errorDeletePrevented');
314
+ }
315
+ await this.deleteEntitiesRelatedFiles(driverResource, loadedEntities);
316
+ let deleteResult = await entityRepository.delete(idsFilter);
317
+ await this.emitEvent('reldens.adminAfterEntityDelete', {driverResource, idProperty, ids});
318
+ return redirectPath + (deleteResult ? 'success' : 'errorStorageFailure');
319
+ } catch (error) {
320
+ return redirectPath + 'errorDeleteFailure';
321
+ }
322
+ }
323
+
324
+ async processSaveEntity(req, res, driverResource, entityPath)
325
+ {
326
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
327
+ let id = (req?.body[idProperty] || '');
328
+ let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
329
+ let loadedEntity = id ? await this.loadEntityById(driverResource, id) : null;
330
+ let uploadProperties = this.fetchUploadProperties(driverResource);
331
+ if(0 < Object.keys(uploadProperties).length){
332
+ if(this.uploaderFactory?.error?.message){
333
+ Logger.error('Upload factory error detected', this.uploaderFactory.error);
334
+ return this.rootPath+'/'+entityPath+'?result=uploadError&error='
335
+ +encodeURIComponent(this.uploaderFactory.error.message);
336
+ }
337
+ let uploadValidationResult = this.validateUploadedFiles(req, uploadProperties, loadedEntity);
338
+ if(!uploadValidationResult.success){
339
+ Logger.error('Upload validation failed', uploadValidationResult.error);
340
+ return this.rootPath+'/'+entityPath+'?result=uploadValidationError&error='
341
+ +encodeURIComponent(uploadValidationResult.error.message);
342
+ }
343
+ }
344
+ let entityDataPatch = this.preparePatchData(
345
+ driverResource,
346
+ idProperty,
347
+ req,
348
+ driverResource.options.properties,
349
+ id,
350
+ loadedEntity
351
+ );
352
+ if(!entityDataPatch){
353
+ Logger.error('Bad patch data.', entityDataPatch);
354
+ return this.rootPath+'/'+entityPath+'?result=saveBadPatchData';
355
+ }
356
+ try {
357
+ let saveResult = await this.saveEntity(id, entityRepository, entityDataPatch);
358
+ if(!saveResult){
359
+ Logger.error('Save result error.', saveResult, entityDataPatch);
360
+ return this.appendResultParam(
361
+ this.generateEntityRoute('editPath', driverResource, idProperty, null, id),
362
+ 'saveEntityStorageError'
363
+ );
364
+ }
365
+ await this.emitEvent('reldens.adminAfterEntitySave', {
366
+ req,
367
+ res,
368
+ driverResource,
369
+ entityPath,
370
+ entityData: saveResult
371
+ });
372
+ if(sc.isFunction(this.autoSyncDistCallback)){
373
+ let uploadProperties = this.fetchUploadProperties(driverResource);
374
+ if(0 < Object.keys(uploadProperties).length){
375
+ for(let uploadPropertyKey of Object.keys(uploadProperties)){
376
+ let property = uploadProperties[uploadPropertyKey];
377
+ await this.autoSyncDistCallback(
378
+ property.bucket,
379
+ saveResult[uploadPropertyKey],
380
+ property.distFolder
381
+ );
382
+ }
383
+ }
384
+ }
385
+ let saveAction = sc.get(req.body, 'saveAction', 'save');
386
+ if('saveAndContinue' === saveAction){
387
+ return this.appendResultParam(
388
+ this.generateEntityRoute('editPath', driverResource, idProperty, saveResult),
389
+ 'success'
390
+ );
391
+ }
392
+ if('saveAndGoBack' === saveAction){
393
+ return this.rootPath+'/'+entityPath+'?result=success';
394
+ }
395
+ return this.generateEntityRoute('viewPath', driverResource, idProperty, saveResult) +'&result=success';
396
+ } catch (error) {
397
+ Logger.error('Save entity error.', error);
398
+ return this.rootPath+'/'+entityPath+'?result=saveEntityError';
399
+ }
400
+ }
401
+
402
+ validateUploadedFiles(req, uploadProperties, loadedEntity)
403
+ {
404
+ for(let uploadPropertyKey of Object.keys(uploadProperties)){
405
+ let property = uploadProperties[uploadPropertyKey];
406
+ let uploadedFiles = req.files?.[uploadPropertyKey];
407
+ if(!uploadedFiles || 0 === uploadedFiles.length){
408
+ if(property.isRequired){
409
+ let isExplicitClear = '1' === sc.get(req.body, 'clear_'+uploadPropertyKey, false);
410
+ if(isExplicitClear){
411
+ return {success: false, error: {message: 'Required file upload missing: '+uploadPropertyKey}};
412
+ }
413
+ let existingValue = loadedEntity ? sc.get(loadedEntity, uploadPropertyKey, '') : '';
414
+ if(!existingValue || '' === existingValue){
415
+ return {success: false, error: {message: 'Required file upload missing: '+uploadPropertyKey}};
416
+ }
417
+ }
418
+ continue;
419
+ }
420
+ for(let file of uploadedFiles){
421
+ let filePath = file.path;
422
+ if(!FileHandler.exists(filePath)){
423
+ return {
424
+ success: false,
425
+ error: {
426
+ message: 'Uploaded file not found in bucket: '+file.filename,
427
+ filePath,
428
+ bucket: property.bucket
429
+ }
430
+ };
431
+ }
432
+ if(!FileHandler.isFile(filePath)){
433
+ return {
434
+ success: false,
435
+ error: {message: 'Uploaded file is not a valid file: '+file.filename, filePath}
436
+ };
437
+ }
438
+ }
439
+ }
440
+ return {success: true};
441
+ }
442
+
443
+ async countTotalEntities(driverResource, filters)
444
+ {
445
+ let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
446
+ if(!entityRepository){
447
+ return false;
448
+ }
449
+ return await entityRepository.count(filters);
450
+ }
451
+
452
+ async loadEntitiesForList(driverResource, pageSize, currentPage, req, filters, sortBy, sortDirection)
453
+ {
454
+ let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
455
+ entityRepository.limit = pageSize;
456
+ if(1 < currentPage){
457
+ entityRepository.offset = (currentPage - 1) * pageSize;
458
+ }
459
+ entityRepository.sortBy = sortBy || false;
460
+ entityRepository.sortDirection = sortDirection || false;
461
+ let loadedEntities = await entityRepository.loadWithRelations(filters, []);
462
+ entityRepository.limit = 0;
463
+ entityRepository.offset = 0;
464
+ entityRepository.sortBy = false;
465
+ entityRepository.sortDirection = false;
466
+ let entityRows = [];
467
+ let resourceProperties = driverResource.options?.properties;
468
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
469
+ for(let entity of loadedEntities){
470
+ let fields = [];
471
+ for(let property of driverResource.options.listProperties){
472
+ let {fieldValue, fieldName} = this.generatePropertyRenderedValueWithLabel(
473
+ entity,
474
+ property,
475
+ resourceProperties[property]
476
+ );
477
+ fields.push({
478
+ name: property,
479
+ value: await this.generatePropertyRenderedValue(
480
+ fieldValue,
481
+ fieldName,
482
+ resourceProperties[property]
483
+ ),
484
+ viewLink: '' !== idProperty
485
+ ? this.generateEntityRoute('viewPath', driverResource, idProperty, entity)
486
+ : ''
487
+ });
488
+ }
489
+ entityRows.push({
490
+ fields,
491
+ editLink: '' !== idProperty
492
+ ? this.generateEntityRoute('editPath', driverResource, idProperty, entity)
493
+ : '',
494
+ deleteLink: this.rootPath + '/' + driverResource.entityPath + this.deletePath,
495
+ id: entity[idProperty]
496
+ });
497
+ }
498
+ return entityRows;
499
+ }
500
+
501
+ async loadEntityById(driverResource, id)
502
+ {
503
+ let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
504
+ if(!entityRepository){
505
+ return false;
506
+ }
507
+ await this.emitEvent('reldens.adminBeforeEntityLoad', {
508
+ driverResource,
509
+ entityId: id
510
+ });
511
+ let idProperty = this.fetchEntityIdPropertyKey(driverResource);
512
+ if(!idProperty){
513
+ return false;
514
+ }
515
+ return await entityRepository.loadOneByWithRelations(idProperty, id);
516
+ }
517
+
518
+ async saveEntity(id, entityRepository, entityDataPatch)
519
+ {
520
+ if('' === id){
521
+ return entityRepository.create(entityDataPatch);
522
+ }
523
+ return entityRepository.updateById(id, entityDataPatch);
524
+ }
525
+
526
+ async deleteEntitiesRelatedFiles(driverResource, entities)
527
+ {
528
+ for(let propertyKey of Object.keys(driverResource.options.properties)){
529
+ let property = driverResource.options.properties[propertyKey];
530
+ if(!property.isUpload){
531
+ continue;
532
+ }
533
+ for(let entity of entities){
534
+ let bucket = sc.get(property, 'bucket', '');
535
+ if(!property.isArray){
536
+ FileHandler.remove([bucket, entity[propertyKey]]);
537
+ continue;
538
+ }
539
+ for(let entityFile of entity[propertyKey].split(property.isArray)){
540
+ FileHandler.remove([bucket, entityFile]);
541
+ }
542
+ }
543
+ }
544
+ }
545
+
546
+ preparePatchData(driverResource, idProperty, req, resourceProperties, id, loadedEntity)
547
+ {
548
+ let entityDataPatch = {};
549
+ for(let i of Object.keys(resourceProperties)){
550
+ if(i === idProperty){
551
+ continue;
552
+ }
553
+ let property = resourceProperties[i];
554
+ let shouldProcess = driverResource.options.editProperties.includes(i);
555
+ if(!id && !shouldProcess && property.isRequired && 'reference' === property.type){
556
+ shouldProcess = true;
557
+ }
558
+ if(!shouldProcess){
559
+ continue;
560
+ }
561
+ let clearFieldParam = sc.get(req.body, 'clear_'+i, false);
562
+ let isExplicitClear = '1' === clearFieldParam;
563
+ let propertyUpdateValue = sc.get(req.body, i, null);
564
+ if('null' === propertyUpdateValue){
565
+ propertyUpdateValue = null;
566
+ }
567
+ let propertyType = sc.get(property, 'type', 'string');
568
+ if(property.isUpload){
569
+ propertyType = 'upload';
570
+ if(isExplicitClear){
571
+ propertyUpdateValue = null;
572
+ }
573
+ if(!isExplicitClear){
574
+ let currentValue = loadedEntity ? sc.get(loadedEntity, i, '') : '';
575
+ propertyUpdateValue = this.prepareUploadPatchData(req, i, currentValue, property);
576
+ }
577
+ }
578
+ if('boolean' === propertyType){
579
+ propertyUpdateValue = '1' === propertyUpdateValue || 'on' === propertyUpdateValue;
580
+ }
581
+ let isEmpty = '' === propertyUpdateValue;
582
+ let isNull = null === propertyUpdateValue;
583
+ if('number' === propertyType && !isNull && !isEmpty){
584
+ let numValue = Number(propertyUpdateValue);
585
+ if(isNaN(numValue)){
586
+ Logger.critical(
587
+ 'Invalid number value for property.',
588
+ {propertyKey: i, value: propertyUpdateValue, entity: driverResource.entityKey}
589
+ );
590
+ return false;
591
+ }
592
+ propertyUpdateValue = numValue;
593
+ }
594
+ if('string' === propertyType && !isNull && !isEmpty){
595
+ propertyUpdateValue = String(propertyUpdateValue);
596
+ }
597
+ if(isEmpty && !property.isRequired){
598
+ propertyUpdateValue = null;
599
+ }
600
+ let isUploadCreate = property.isUpload && !id;
601
+ if(property.isRequired && (isNull || isEmpty) && (!property.isUpload || isUploadCreate)){
602
+ Logger.critical(
603
+ 'Missing property on prepare patch data.',
604
+ {propertyKey: i, config: property, propertyUpdateValue, entity: driverResource.entityKey}
605
+ );
606
+ return false;
607
+ }
608
+ if(-1 !== this.passwordFieldNames.indexOf(i)){
609
+ if(!propertyUpdateValue || '' === propertyUpdateValue){
610
+ continue;
611
+ }
612
+ }
613
+ if(!property.isUpload || (property.isUpload && (!isNull || isExplicitClear))){
614
+ entityDataPatch[i] = propertyUpdateValue;
615
+ }
616
+ }
617
+ return entityDataPatch;
618
+ }
619
+
620
+
621
+ prepareUploadPatchData(req, i, propertyUpdateValue, property)
622
+ {
623
+ let filesData = sc.get(req.files, i, null);
624
+ let removedFiles = sc.get(req.body, 'removed_'+i, '');
625
+ let existingFiles = propertyUpdateValue || '';
626
+ if(property.isArray && removedFiles && '' !== removedFiles){
627
+ let removedFilesArray = removedFiles.split(',');
628
+ let existingFilesArray = existingFiles ? existingFiles.split(property.isArray) : [];
629
+ existingFilesArray = existingFilesArray.filter(file => {
630
+ return -1 === removedFilesArray.indexOf(file);
631
+ });
632
+ existingFiles = existingFilesArray.join(property.isArray);
633
+ }
634
+ if(!filesData || 0 === filesData.length){
635
+ if(removedFiles && '' !== removedFiles){
636
+ return existingFiles;
637
+ }
638
+ return null;
639
+ }
640
+ let newFileNames = [];
641
+ for(let file of filesData){
642
+ newFileNames.push(file.filename);
643
+ }
644
+ if(property.isArray && existingFiles && '' !== existingFiles){
645
+ let existingFilesArray = existingFiles.split(property.isArray);
646
+ let allFiles = existingFilesArray.concat(newFileNames);
647
+ return allFiles.join(property.isArray);
648
+ }
649
+ return newFileNames.join(property.isArray);
650
+ }
651
+
652
+ async generatePropertyRenderedValue(fieldValue, fieldName, resourceProperty, templateType)
653
+ {
654
+ let fieldOriginalValue = fieldValue;
655
+ if('view' === templateType){
656
+ if(resourceProperty.isArray){
657
+ fieldValue = fieldValue.split(resourceProperty.isArray).map((value) => {
658
+ let target = resourceProperty.isUpload ? ' target="_blank"' : '';
659
+ let fieldValuePart = resourceProperty.isUpload && resourceProperty.bucketPath && value
660
+ ? resourceProperty.bucketPath+value
661
+ : value;
662
+ return {fieldValuePart, fieldOriginalValuePart: value, target};
663
+ });
664
+ }
665
+ if(!resourceProperty.isArray && resourceProperty.isUpload && fieldValue){
666
+ fieldValue = resourceProperty.bucketPath+fieldValue;
667
+ }
668
+ }
669
+ return await this.adminContentsRender(
670
+ this.adminFilesContents.fields.view[this.propertyType(resourceProperty, templateType, fieldValue)],
671
+ {fieldName, fieldValue, fieldOriginalValue, target: ' target="_blank"'}
672
+ );
673
+ }
674
+
675
+ generatePropertyRenderedValueWithLabel(entity, propertyKey, resourceProperty)
676
+ {
677
+ let fieldValue = (0 === entity[propertyKey] ? '0' : entity[propertyKey] || '');
678
+ if((sc.isObject(fieldValue) || sc.isArray(fieldValue)) && 'json' === resourceProperty.dbType){
679
+ fieldValue = sc.toJsonString(fieldValue);
680
+ }
681
+ let fieldName = propertyKey;
682
+ if('boolean' === resourceProperty.type){
683
+ fieldValue = 1 === entity[propertyKey]
684
+ || '1' === entity[propertyKey]
685
+ || true === entity[propertyKey] ? 'Yes' : 'No';
686
+ }
687
+ if('datetime' === resourceProperty.type){
688
+ fieldValue = '' !== fieldValue ? sc.formatDate(new Date(fieldValue)) : '';
689
+ }
690
+ if('reference' === resourceProperty.type){
691
+ let relationEntity = entity[resourceProperty.alias || 'related_'+resourceProperty.reference];
692
+ if(relationEntity){
693
+ let relation = this.relations()[resourceProperty.reference];
694
+ if(relation){
695
+ let relationTitleProperty = relation[resourceProperty.alias || resourceProperty.reference];
696
+ if(relationTitleProperty && '' !== String(relationEntity[relationTitleProperty] || '')){
697
+ fieldName = relationTitleProperty;
698
+ fieldValue = relationEntity[relationTitleProperty]+(' ('+fieldValue+')');
699
+ }
700
+ }
701
+ }
702
+ }
703
+ if(resourceProperty.availableValues){
704
+ let optionData = resourceProperty.availableValues.filter((availableValue) => {
705
+ return String(availableValue.value) === String(fieldValue);
706
+ }).shift();
707
+ if(optionData){
708
+ fieldValue = optionData.label + ' (' + fieldValue + ')';
709
+ }
710
+ }
711
+ return {fieldValue, fieldName};
712
+ }
713
+
714
+ async generatePropertyEditRenderedValue(entity, propertyKey, resourceProperty)
715
+ {
716
+ if(-1 !== this.passwordFieldNames.indexOf(propertyKey)){
717
+ return '';
718
+ }
719
+ let entityPropertyValue = sc.get(entity, propertyKey, null);
720
+ let fieldValue = (0 === entityPropertyValue ? '0' : entityPropertyValue || '');
721
+ if('null' === fieldValue){
722
+ fieldValue = '';
723
+ }
724
+ if((sc.isObject(fieldValue) || sc.isArray(fieldValue)) && 'json' === resourceProperty.dbType){
725
+ fieldValue = sc.toJsonString(fieldValue);
726
+ }
727
+ if('boolean' === resourceProperty.type){
728
+ fieldValue = 1 === entityPropertyValue
729
+ || '1' === entityPropertyValue
730
+ || true === entityPropertyValue ? ' checked="checked"' : '';
731
+ }
732
+ if('datetime' === resourceProperty.type){
733
+ fieldValue = !entityPropertyValue || '' === entityPropertyValue
734
+ ? ''
735
+ : sc.formatDate(new Date(entityPropertyValue), 'Y-m-d H:i:s');
736
+ }
737
+ if('reference' === resourceProperty.type){
738
+ let relationDriverResource = this.resourcesByReference()[resourceProperty.reference];
739
+ if(!relationDriverResource){
740
+ Logger.critical('Driver resource not found by reference.', resourceProperty);
741
+ return fieldValue;
742
+ }
743
+ let relation = this.relations()[resourceProperty.reference];
744
+ let relationKey = resourceProperty.alias || resourceProperty.reference;
745
+ let relationTitleProperty = relation
746
+ ? relation[relationKey]
747
+ : this.fetchEntityIdPropertyKey(relationDriverResource);
748
+ let options = (await this.fetchRelationOptions(relationDriverResource)).map((option) => {
749
+ let value = option[this.fetchEntityIdPropertyKey(relationDriverResource)];
750
+ return {
751
+ label: option[relationTitleProperty]+' (ID: '+value+')',
752
+ value,
753
+ selected: entity && entity[propertyKey] === value ? ' selected="selected"' : ''
754
+ };
755
+ });
756
+ if(!resourceProperty.isRequired){
757
+ let isSelected = !entity || null === entity[propertyKey] || '' === entity[propertyKey];
758
+ options.unshift({
759
+ label: '-- Select --',
760
+ value: '',
761
+ selected: isSelected ? ' selected="selected"' : ''
762
+ });
763
+ }
764
+ return options;
765
+ }
766
+ return fieldValue;
767
+ }
768
+
769
+ async fetchRelationOptions(relationDriverResource)
770
+ {
771
+ let relationEntityRepository = this.dataServer.getEntity(relationDriverResource.entityKey);
772
+ if(!relationEntityRepository){
773
+ return [];
774
+ }
775
+ return await relationEntityRepository.loadAll();
776
+ }
777
+
778
+ propertyType(resourceProperty, templateType, fieldValue)
779
+ {
780
+ let propertyType = sc.get(resourceProperty, 'type', 'text');
781
+ if('reference' === propertyType && 'edit' === templateType){
782
+ return 'select';
783
+ }
784
+ if(resourceProperty.isUpload){
785
+ if('edit' === templateType){
786
+ return 'file';
787
+ }
788
+ if('view' === templateType){
789
+ if(!fieldValue || '' === fieldValue){
790
+ return 'text';
791
+ }
792
+ let multiple = resourceProperty.isArray ? 's' : '';
793
+ let allowedTypes = sc.get(resourceProperty, 'allowedTypes', '');
794
+ if('text' === allowedTypes){
795
+ return 'link'+multiple;
796
+ }
797
+ if('' !== allowedTypes){
798
+ let templateName = allowedTypes + multiple;
799
+ if(sc.hasOwn(this.adminFilesContents.fields.view, templateName)){
800
+ return templateName;
801
+ }
802
+ }
803
+ return 'text';
804
+ }
805
+ }
806
+ if('textarea' === resourceProperty.type){
807
+ return 'textarea';
808
+ }
809
+ if(-1 !== ['reference', 'number', 'datetime'].indexOf(propertyType)){
810
+ propertyType = 'text';
811
+ }
812
+ return propertyType;
813
+ }
814
+
815
+ getInputType(resourceProperty, propertyKey, fieldDisabled)
816
+ {
817
+ if(-1 !== this.passwordFieldNames.indexOf(propertyKey) && !fieldDisabled){
818
+ return 'password';
819
+ }
820
+ if('datetime' === resourceProperty.type && !fieldDisabled){
821
+ return 'datetime-local';
822
+ }
823
+ if('number' === resourceProperty.type){
824
+ return 'number';
825
+ }
826
+ return 'text';
827
+ }
828
+
829
+ generateEntityRoute(routeType, driverResource, idProperty, entity, entityId)
830
+ {
831
+ if(!idProperty || (!entity && !entityId)){
832
+ return this.rootPath + '/' + driverResource.entityPath;
833
+ }
834
+ let idParam = '?' + idProperty + '=';
835
+ if(entity){
836
+ idParam = idParam + entity[idProperty];
837
+ }
838
+ if(entityId){
839
+ idParam = idParam + entityId;
840
+ }
841
+ return this.rootPath + '/' + driverResource.entityPath + this[routeType] + idParam;
842
+ }
843
+
844
+ appendResultParam(route, result)
845
+ {
846
+ if(route.includes('?')){
847
+ return route+'&result='+result;
848
+ }
849
+ return route+'?result='+result;
850
+ }
851
+
852
+ }
853
+
854
+ module.exports.RouterContents = RouterContents;