@reldens/cms 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,353 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - CollectionsTransformer
4
+ *
5
+ */
6
+
7
+ const { CollectionsTransformerBase } = require('./collections-transformer-base');
8
+ const { PaginationHandler } = require('../pagination-handler');
9
+ const { Logger, sc } = require('@reldens/utils');
10
+
11
+ class CollectionsTransformer extends CollectionsTransformerBase
12
+ {
13
+
14
+ constructor(props)
15
+ {
16
+ super(props);
17
+ this.renderEngine = sc.get(props, 'renderEngine', false);
18
+ this.getPartials = sc.get(props, 'getPartials', false);
19
+ this.findAllPartialTags = sc.get(props, 'findAllPartialTags', false);
20
+ this.loadPartialTemplate = sc.get(props, 'loadPartialTemplate', false);
21
+ this.paginationHandler = new PaginationHandler();
22
+ }
23
+
24
+ getLoopCollectionStartRegex()
25
+ {
26
+ return /<collection\s+([^>]+)>/g;
27
+ }
28
+
29
+ getLoopCollectionEndRegex()
30
+ {
31
+ return new RegExp('<\\/collection>');
32
+ }
33
+
34
+ async transform(template, domain, req)
35
+ {
36
+ let processedTemplate = template;
37
+ let matches = [...template.matchAll(this.getLoopCollectionStartRegex())];
38
+ for(let i = matches.length - 1; i >= 0; i--){
39
+ let startMatch = matches[i];
40
+ let tagContent = startMatch[1];
41
+ if(tagContent.includes('field=')){
42
+ continue;
43
+ }
44
+ let tableName = this.extractAttributeValue(tagContent, 'name');
45
+ let filtersJson = this.extractAttributeValue(tagContent, 'filters');
46
+ let queryOptionsJson = this.extractAttributeValue(tagContent, 'data');
47
+ let relationsString = this.extractAttributeValue(tagContent, 'relations');
48
+ let paginationId = this.extractAttributeValue(tagContent, 'pagination');
49
+ if(paginationId && '' !== paginationId){
50
+ let containerName = this.extractAttributeValue(tagContent, 'container');
51
+ let prevPages = this.extractAttributeValue(tagContent, 'prevPages');
52
+ let nextPages = this.extractAttributeValue(tagContent, 'nextPages');
53
+ let loopResult = await this.processPaginatedCollection(
54
+ processedTemplate,
55
+ startMatch,
56
+ tableName,
57
+ filtersJson,
58
+ relationsString,
59
+ queryOptionsJson,
60
+ paginationId,
61
+ containerName,
62
+ prevPages,
63
+ nextPages,
64
+ domain,
65
+ req
66
+ );
67
+ if(loopResult){
68
+ processedTemplate = loopResult;
69
+ }
70
+ continue;
71
+ }
72
+ let loopResult = await this.processLoopCollection(
73
+ processedTemplate,
74
+ startMatch,
75
+ tableName,
76
+ filtersJson,
77
+ relationsString,
78
+ queryOptionsJson,
79
+ domain
80
+ );
81
+ if(loopResult){
82
+ processedTemplate = loopResult;
83
+ }
84
+ }
85
+ return processedTemplate;
86
+ }
87
+
88
+ async processLoopCollection(
89
+ template,
90
+ startMatch,
91
+ tableName,
92
+ filtersJson,
93
+ relationsString,
94
+ queryOptionsJson,
95
+ domain
96
+ ){
97
+ return await this.processCollectionBase(
98
+ template,
99
+ startMatch,
100
+ tableName,
101
+ filtersJson,
102
+ relationsString,
103
+ queryOptionsJson,
104
+ domain,
105
+ false
106
+ );
107
+ }
108
+
109
+ async processPaginatedCollection(
110
+ template,
111
+ startMatch,
112
+ tableName,
113
+ filtersJson,
114
+ relationsString,
115
+ queryOptionsJson,
116
+ paginationId,
117
+ containerName,
118
+ prevPages,
119
+ nextPages,
120
+ domain,
121
+ req
122
+ ){
123
+ if(!req){
124
+ Logger.warning('No request provided for pagination, falling back to regular collection');
125
+ return await this.processLoopCollection(
126
+ template,
127
+ startMatch,
128
+ tableName,
129
+ filtersJson,
130
+ relationsString,
131
+ queryOptionsJson,
132
+ domain
133
+ );
134
+ }
135
+ return await this.processCollectionBase(
136
+ template,
137
+ startMatch,
138
+ tableName,
139
+ filtersJson,
140
+ relationsString,
141
+ queryOptionsJson,
142
+ domain,
143
+ {
144
+ paginationId,
145
+ containerName,
146
+ prevPages,
147
+ nextPages,
148
+ req
149
+ }
150
+ );
151
+ }
152
+
153
+ async processCollectionBase(
154
+ template,
155
+ startMatch,
156
+ tableName,
157
+ filtersJson,
158
+ relationsString,
159
+ queryOptionsJson,
160
+ domain,
161
+ paginationOptions
162
+ ){
163
+ let startPos = startMatch.index;
164
+ let startEnd = startPos + startMatch[0].length;
165
+ let endMatch = this.getLoopCollectionEndRegex().exec(template.substring(startEnd));
166
+ if(!endMatch){
167
+ Logger.warning('No matching end tag found for collection: '+tableName);
168
+ return false;
169
+ }
170
+ let endPos = startEnd + endMatch.index;
171
+ let loopContent = template.substring(startEnd, endPos);
172
+ let collectionData;
173
+ let finalContent;
174
+ if(paginationOptions){
175
+ let templateParams = this.parseCollectionTemplateParams(filtersJson, queryOptionsJson);
176
+ let requestParams = this.paginationHandler.extractCollectionKeyFromRequest(
177
+ paginationOptions.req,
178
+ paginationOptions.paginationId
179
+ );
180
+ let mergedParams = this.paginationHandler.mergeCollectionParameters(
181
+ templateParams,
182
+ requestParams
183
+ );
184
+ mergedParams.prevPages = paginationOptions.prevPages;
185
+ mergedParams.nextPages = paginationOptions.nextPages;
186
+ let entity = this.dataServer.getEntity(tableName);
187
+ if(!entity){
188
+ Logger.warning('Entity not found in dataServer: '+tableName);
189
+ return template.substring(0, startPos) + '' + template.substring(endPos + endMatch[0].length);
190
+ }
191
+ let paginationData = this.paginationHandler.calculatePaginationData(
192
+ await this.paginationHandler.getCollectionTotal(entity, mergedParams.filters),
193
+ mergedParams.page,
194
+ mergedParams.limit,
195
+ this.getCurrentUrl(paginationOptions.req),
196
+ paginationOptions.paginationId,
197
+ mergedParams,
198
+ templateParams
199
+ );
200
+ let originalState = entity.preserveEntityState();
201
+ collectionData = await entity.loadEntityData(mergedParams.filters, {
202
+ limit: mergedParams.limit,
203
+ offset: paginationData.offset,
204
+ sortBy: mergedParams.sortBy,
205
+ sortDirection: mergedParams.sortDirection
206
+ }, relationsString);
207
+ entity.restoreEntityState(originalState);
208
+ if(this.jsonFieldsParser){
209
+ collectionData = this.jsonFieldsParser.parseJsonFields(
210
+ collectionData,
211
+ this.jsonFieldsParser.getJsonFieldsForEntity(tableName)
212
+ );
213
+ }
214
+ let collectionContent = await this.renderCollectionLoop(loopContent, collectionData, domain);
215
+ finalContent = this.renderEngine.render(
216
+ 1 < paginationData.totalPages
217
+ ? this.loadPaginationTemplate(paginationOptions.containerName, domain)
218
+ : '{{&collectionContentForCurrentPage}}',
219
+ Object.assign(
220
+ {},
221
+ paginationData,
222
+ {
223
+ collectionContentForCurrentPage: collectionContent,
224
+ hasResults: collectionData && 0 < collectionData.length,
225
+ noResultsMessage: 'No items found.'
226
+ }
227
+ ),
228
+ this.getPartials(domain)
229
+ );
230
+ }
231
+ if(!paginationOptions){
232
+ collectionData = await this.fetchCollectionForTemplate(
233
+ tableName,
234
+ filtersJson,
235
+ queryOptionsJson,
236
+ relationsString
237
+ );
238
+ finalContent = await this.renderCollectionLoop(loopContent, collectionData, domain);
239
+ }
240
+ return template.substring(0, startPos) + finalContent + template.substring(endPos + endMatch[0].length);
241
+ }
242
+
243
+ parseCollectionTemplateParams(filtersJson, queryOptionsJson)
244
+ {
245
+ let filters = {};
246
+ let queryOptions = {};
247
+ if(filtersJson && '' !== filtersJson.trim()){
248
+ let convertedFiltersJson = this.convertJsObjectToJson(filtersJson);
249
+ filters = sc.parseJson(convertedFiltersJson, {});
250
+ if(!filters){
251
+ Logger.warning('Invalid filters JSON: '+filtersJson);
252
+ filters = {};
253
+ }
254
+ }
255
+ if(queryOptionsJson && '' !== queryOptionsJson.trim()){
256
+ let convertedOptionsJson = this.convertJsObjectToJson(queryOptionsJson);
257
+ queryOptions = sc.parseJson(convertedOptionsJson, {});
258
+ if(!queryOptions){
259
+ Logger.warning('Invalid query options JSON: '+queryOptionsJson);
260
+ queryOptions = {};
261
+ }
262
+ }
263
+ return {
264
+ filters,
265
+ limit: sc.get(queryOptions, 'limit', 10),
266
+ sortBy: sc.get(queryOptions, 'sortBy', 'id'),
267
+ sortDirection: sc.get(queryOptions, 'sortDirection', 'asc')
268
+ };
269
+ }
270
+
271
+ loadPaginationTemplate(containerName, domain)
272
+ {
273
+ if(!containerName || '' === containerName){
274
+ containerName = 'pagedCollection';
275
+ }
276
+ let partialContent = this.loadPartialTemplate(containerName, domain);
277
+ if(partialContent){
278
+ return partialContent;
279
+ }
280
+ Logger.critical('Pagination template not found: ' + containerName + '. Please create the template file.');
281
+ return '{{&collectionContentForCurrentPage}}';
282
+ }
283
+
284
+ async renderCollectionLoop(loopContent, collectionData, domain)
285
+ {
286
+ if(!collectionData || 0 === collectionData.length){
287
+ return this.renderNoResultsMessage(domain);
288
+ }
289
+ let renderedContent = '';
290
+ for(let row of collectionData){
291
+ renderedContent += await this.processPartialsInLoop(loopContent, row, domain);
292
+ }
293
+ return renderedContent;
294
+ }
295
+
296
+ renderNoResultsMessage(domain)
297
+ {
298
+ let noResultsTemplate = this.loadPartialTemplate('noResults', domain);
299
+ if(noResultsTemplate){
300
+ return this.renderEngine.render(
301
+ noResultsTemplate,
302
+ {
303
+ message: 'No results found.',
304
+ cssClass: 'no-results',
305
+ alertClass: 'alert-info'
306
+ },
307
+ this.getPartials(domain)
308
+ );
309
+ }
310
+ return '';
311
+ }
312
+
313
+ async processPartialsInLoop(content, rowData, domain)
314
+ {
315
+ let processedContent = content;
316
+ let partialTags = this.findAllPartialTags(content);
317
+ for(let i = partialTags.length - 1; i >= 0; i--){
318
+ let tag = partialTags[i];
319
+ let partialContent = this.loadPartialTemplate(tag.name, domain);
320
+ if(!partialContent){
321
+ Logger.warning('Partial template not found: ' + tag.name);
322
+ processedContent = processedContent.substring(0, tag.start)+''+processedContent.substring(tag.end);
323
+ continue;
324
+ }
325
+ if(sc.hasOwn(tag.attributes, 'row')){
326
+ processedContent = processedContent.substring(0, tag.start)
327
+ +this.renderEngine.render(partialContent, {row: rowData}, this.getPartials(domain))
328
+ +processedContent.substring(tag.end);
329
+ continue;
330
+ }
331
+ let wrapperTemplate = '{{#vars}}{{> ' + tag.name + '}}{{/vars}}';
332
+ let renderData = { vars: tag.attributes };
333
+ let partials = {[tag.name]: partialContent};
334
+ processedContent = processedContent.substring(0, tag.start) +
335
+ this.renderEngine.render(wrapperTemplate, renderData, partials) +
336
+ processedContent.substring(tag.end);
337
+ }
338
+ return this.renderEngine.render(processedContent, {row: rowData}, this.getPartials(domain));
339
+ }
340
+
341
+ getCurrentUrl(req)
342
+ {
343
+ if(!req){
344
+ return '';
345
+ }
346
+ return sc.get(req, 'protocol', 'http') + '://'
347
+ + (req.get('host') || 'localhost')
348
+ + sc.get(req, 'originalUrl', sc.get(req, 'path', '/')).split('?')[0];
349
+ }
350
+
351
+ }
352
+
353
+ module.exports.CollectionsTransformer = CollectionsTransformer;
@@ -0,0 +1,65 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - EntitiesTransformer
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+
9
+ class EntitiesTransformer
10
+ {
11
+
12
+ constructor(props)
13
+ {
14
+ this.dataServer = sc.get(props, 'dataServer', false);
15
+ this.jsonFieldsParser = sc.get(props, 'jsonFieldsParser', false);
16
+ this.processAllTemplateFunctions = sc.get(props, 'processAllTemplateFunctions', false);
17
+ }
18
+
19
+ getEntityRegex()
20
+ {
21
+ return /<entity\s+name="([^"]+)"(?:\s+field="([^"]+)"\s+value="([^"]+)"|\s+id="([^"]+)")?\s*\/?>/g;
22
+ }
23
+
24
+ async transform(template, domain)
25
+ {
26
+ let processedTemplate = template;
27
+ for(let match of template.matchAll(this.getEntityRegex())){
28
+ let tableName = match[1];
29
+ let field = sc.get(match, '2', 'id');
30
+ let value = sc.get(match, '3', sc.get(match, '4', ''));
31
+ if(!value){
32
+ Logger.warning('Entity tag missing value: '+match[0]);
33
+ continue;
34
+ }
35
+ processedTemplate = processedTemplate.replace(
36
+ match[0],
37
+ await this.processAllTemplateFunctions(
38
+ sc.get(await this.fetchEntityForTemplate(tableName, value, field), 'content', ''),
39
+ domain
40
+ )
41
+ );
42
+ }
43
+ return processedTemplate;
44
+ }
45
+
46
+ async fetchEntityForTemplate(tableName, identifier, identifierField)
47
+ {
48
+ let entity = this.dataServer.getEntity(tableName);
49
+ if(!entity){
50
+ Logger.warning('Entity not found in dataServer: '+tableName);
51
+ return false;
52
+ }
53
+ let result = await entity.loadOneBy(identifierField, identifier);
54
+ if(!result){
55
+ return false;
56
+ }
57
+ return this.jsonFieldsParser.parseJsonFields(
58
+ result,
59
+ this.jsonFieldsParser.getJsonFieldsForEntity(tableName)
60
+ );
61
+ }
62
+
63
+ }
64
+
65
+ module.exports.EntitiesTransformer = EntitiesTransformer;
@@ -0,0 +1,171 @@
1
+ /**
2
+ *
3
+ * Reldens - CMS - PartialsTransformer
4
+ *
5
+ */
6
+
7
+ const { Logger, sc } = require('@reldens/utils');
8
+
9
+ class PartialsTransformer
10
+ {
11
+
12
+ constructor(props)
13
+ {
14
+ this.renderEngine = sc.get(props, 'renderEngine', false);
15
+ this.getPartials = sc.get(props, 'getPartials', false);
16
+ }
17
+
18
+ async transform(template, domain)
19
+ {
20
+ let processedTemplate = template;
21
+ let partialTags = this.findAllPartialTags(template);
22
+ for(let i = partialTags.length - 1; i >= 0; i--){
23
+ let tag = partialTags[i];
24
+ let partialContent = this.loadPartialTemplate(tag.name, domain);
25
+ if(!partialContent){
26
+ Logger.warning('Partial template not found: ' + tag.name);
27
+ processedTemplate = processedTemplate.substring(0, tag.start)+''+processedTemplate.substring(tag.end);
28
+ continue;
29
+ }
30
+ let wrapperTemplate = '{{#vars}}{{> ' + tag.name + '}}{{/vars}}';
31
+ let renderData = { vars: tag.attributes };
32
+ let partials = {[tag.name]: partialContent};
33
+ processedTemplate = processedTemplate.substring(0, tag.start) +
34
+ this.renderEngine.render(wrapperTemplate, renderData, partials) +
35
+ processedTemplate.substring(tag.end);
36
+ }
37
+ return processedTemplate;
38
+ }
39
+
40
+ findAllPartialTags(template)
41
+ {
42
+ let partialTags = [];
43
+ let pos = 0;
44
+ let partial = '<partial';
45
+ for(let tagStart = template.indexOf(partial, pos); -1 !== tagStart; tagStart=template.indexOf(partial, pos)){
46
+ let tagEnd = this.findPartialTagEnd(template, tagStart);
47
+ if(-1 === tagEnd){
48
+ pos = tagStart + partial.length;
49
+ continue;
50
+ }
51
+ let fullTag = template.substring(tagStart, tagEnd);
52
+ let nameMatch = fullTag.match(/name=["']([^"']+)["']/);
53
+ if(!nameMatch){
54
+ pos = tagStart + partial.length;
55
+ continue;
56
+ }
57
+ let partialName = nameMatch[1];
58
+ let attributes = this.parsePartialAttributes(fullTag, partialName);
59
+ partialTags.push({
60
+ start: tagStart,
61
+ end: tagEnd,
62
+ name: partialName,
63
+ attributes: attributes,
64
+ fullTag: fullTag
65
+ });
66
+ pos = tagEnd;
67
+ }
68
+ return partialTags;
69
+ }
70
+
71
+ findPartialTagEnd(template, tagStart)
72
+ {
73
+ let inQuotes = false;
74
+ let quoteChar = '';
75
+ let selfCloseTag = '/>';
76
+ let openCloseTag = '</partial>';
77
+ for(let i = tagStart; i < template.length; i++){
78
+ let char = template[i];
79
+ if(!inQuotes && ('"' === char || "'" === char)){
80
+ inQuotes = true;
81
+ quoteChar = char;
82
+ continue;
83
+ }
84
+ if(inQuotes && char === quoteChar && '\\' !== template[i - 1]){
85
+ inQuotes = false;
86
+ quoteChar = '';
87
+ continue;
88
+ }
89
+ if(!inQuotes){
90
+ if(template.substring(i, i + selfCloseTag.length) === selfCloseTag){
91
+ return i + selfCloseTag.length;
92
+ }
93
+ if('>' === char){
94
+ let closeIndex = template.indexOf(openCloseTag, i);
95
+ if(-1 !== closeIndex){
96
+ return closeIndex + openCloseTag.length;
97
+ }
98
+ return i + 1;
99
+ }
100
+ }
101
+ }
102
+ return -1;
103
+ }
104
+
105
+ parsePartialAttributes(fullTag, partialName)
106
+ {
107
+ let namePattern = 'name=' + this.getQuotePattern(fullTag, partialName);
108
+ let nameIndex = fullTag.indexOf(namePattern);
109
+ if(-1 === nameIndex){
110
+ return {};
111
+ }
112
+ let attributesStart = nameIndex + namePattern.length;
113
+ let attributesEnd = fullTag.lastIndexOf('/>');
114
+ if(-1 === attributesEnd){
115
+ attributesEnd = fullTag.lastIndexOf('</partial>');
116
+ }
117
+ if(-1 === attributesEnd){
118
+ attributesEnd = fullTag.lastIndexOf('>');
119
+ }
120
+ if(-1 === attributesEnd || attributesEnd <= attributesStart){
121
+ return {};
122
+ }
123
+ let attributesString = fullTag.substring(attributesStart, attributesEnd).trim();
124
+ return this.extractAttributesObject(attributesString);
125
+ }
126
+
127
+ getQuotePattern(fullTag, partialName)
128
+ {
129
+ if(fullTag.includes('name="' + partialName + '"')){
130
+ return '"' + partialName + '"';
131
+ }
132
+ if(fullTag.includes("name='" + partialName + "'")){
133
+ return "'" + partialName + "'";
134
+ }
135
+ return '"' + partialName + '"';
136
+ }
137
+
138
+ extractAttributesObject(attributesString)
139
+ {
140
+ if(!attributesString){
141
+ return {};
142
+ }
143
+ let attributes = {};
144
+ let valueRegex = /(\w+)=(['"])((?:(?!\2)[^\\]|\\.)*)(\2)/g;
145
+ for(let match of attributesString.matchAll(valueRegex)){
146
+ attributes[match[1]] = match[3];
147
+ }
148
+ let booleanRegex = /\b(\w+)(?!\s*=)/g;
149
+ for(let match of attributesString.matchAll(booleanRegex)){
150
+ if(!sc.hasOwn(attributes, match[1])){
151
+ attributes[match[1]] = true;
152
+ }
153
+ }
154
+ return attributes;
155
+ }
156
+
157
+ loadPartialTemplate(partialName, domain)
158
+ {
159
+ if(!this.getPartials){
160
+ return false;
161
+ }
162
+ let partials = this.getPartials(domain);
163
+ if(sc.hasOwn(partials, partialName)){
164
+ return partials[partialName];
165
+ }
166
+ return false;
167
+ }
168
+
169
+ }
170
+
171
+ module.exports.PartialsTransformer = PartialsTransformer;