forked from pluginpal/strapi-webtools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathurl-pattern.ts
More file actions
270 lines (226 loc) 路 9 KB
/
Copy pathurl-pattern.ts
File metadata and controls
270 lines (226 loc) 路 9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
import { factories, Schema, UID } from '@strapi/strapi';
import { getPluginService } from '../util/getPluginService';
import { typedEntries } from '../util/typeHelpers';
import { Config } from '../config';
const contentTypeSlug = 'plugin::webtools.url-pattern';
const customServices = () => ({
/**
* Find URL patterns by UID and optionally language code.
*
* @param {string} uid - The UID of the content type.
* @param {string} langcode - The optional language code.
* @returns {Promise<string[]>} The array of URL patterns.
*/
findByUid: async (uid: string, langcode?: string): Promise<string[]> => {
let patterns = await strapi.documents(contentTypeSlug).findMany({
filters: {
contenttype: uid,
},
});
if (langcode) {
patterns = patterns.filter((pattern) => (pattern.languages as string).includes(langcode));
}
if (!patterns.length) {
return [strapi.config.get('plugin::webtools.default_pattern')];
}
const patternsArray = patterns.map((pattern) => pattern.pattern);
return patternsArray;
},
/**
* Get all field names allowed in the URL of a given content type.
*
* @param {string} contentType - The content type.
* @param {string[]} allowedFields - The allowed fields to include.
* @returns {string[]} The list of allowed field names.
*/
getAllowedFields: (contentType: Schema.ContentType, allowedFields: string[] = []) => {
const fields: string[] = [];
allowedFields.forEach((fieldType) => {
typedEntries(contentType.attributes).forEach(([fieldName, field]) => {
if ((field.type === fieldType || fieldName === fieldType) && field.type !== 'relation') {
fields.push(fieldName);
} else if (
field.type === 'relation'
&& fieldName !== 'localizations'
&& fieldName !== 'createdBy'
&& fieldName !== 'updatedBy'
) {
// @ts-expect-error
// field.target is not strongly typed in the Strapi Attribute types.
const relation = strapi.contentTypes[field.target as UID.ContentType];
if (allowedFields.includes('documentId') && !fields.includes(`${fieldName}.documentId`)) {
fields.push(`${fieldName}.documentId`);
}
typedEntries(relation.attributes).forEach(([subFieldName, subField]) => {
if (subField.type === fieldType || subFieldName === fieldType) {
fields.push(`${fieldName}.${subFieldName}`);
}
});
} else if (
field.type === 'component'
&& field.component
&& field.repeatable !== true // TODO: implement repeatable components.
) {
const relation = strapi.components[field.component];
if (allowedFields.includes('documentId') && !fields.includes(`${fieldName}.documentId`)) {
fields.push(`${fieldName}.documentId`);
}
Object.entries(relation.attributes).forEach(([subFieldName, subField]) => {
if (subField.type === fieldType || subFieldName === fieldType) {
fields.push(`${fieldName}.${subFieldName}`);
}
});
}
});
});
// Add documentId field manually because it is not on the attributes object of a content type.
if (allowedFields.includes('documentId')) {
fields.push('documentId');
}
if (allowedFields.includes('pluralName')) {
fields.push('pluralName');
}
return fields;
},
/**
* Get all fields from a pattern.
*
* @param {string} pattern - The patterns to extract fields from.
* @returns {string[]} The extracted fields.
*/
getFieldsFromPattern: (pattern: string): string[] => {
const fields = pattern.match(/\[[\w\d.\-\[\]]+\]/g); // Get all substrings between [] as array.
if (!fields) {
return [];
}
const newFields = fields.map((field) => field.slice(1, -1)); // Strip [] from string.
return newFields;
},
/**
* Get all relations from a pattern.
*
* @param {string} pattern - The patterns to extract relations from.
* @returns {string[]} The extracted relations.
*/
getRelationsFromPattern: (pattern: string): string[] => {
// Get fields from the pattern (assuming they are inside square brackets)
let fields = getPluginService('url-pattern').getFieldsFromPattern(pattern);
// Filter out fields that are empty or malformed
fields = fields.filter((field) => field);
// For fields containing dots, extract the first part (relation)
const relations = fields
.filter((field) => field.includes('.'))
.map((field) => field.split('.')[0])
.map((relation) => relation.replace(/\[\d+\]/g, '')); // Strip array index
return relations;
},
/**
* Resolve a pattern string from pattern to path for a single entity.
*
* @param {string} uid - The UID of the content type.
* @param {object} entity - The entity to resolve the pattern for.
* @param {string} [urlPattern] - The URL pattern to resolve.
* @returns {string} The resolved path.
*/
resolvePattern: (
uid: UID.ContentType,
entity: { [key: string]: any },
urlPattern?: string,
): string => {
const resolve = (pattern: string) => {
let resolvedPattern: string = pattern;
const fields = getPluginService('url-pattern').getFieldsFromPattern(
pattern,
);
fields.forEach((field) => {
const relationalField = field.split('.').length > 1 ? field.split('.') : null;
const { slugify } = strapi.config.get<Config>('plugin::webtools');
if (field === 'pluralName') {
const fieldValue = strapi.contentTypes[uid].info.pluralName;
if (!fieldValue) {
return;
}
resolvedPattern = resolvedPattern.replace(`[${field}]`, fieldValue || '');
} else if (!relationalField) {
const fieldValue = slugify(String(entity[field]));
resolvedPattern = resolvedPattern.replace(`[${field}]`, fieldValue || '');
} else {
let relationName = relationalField[0];
let relationIndex: number | null = null;
const arrayMatch = relationName.match(/^([\w-]+)\[(\d+)\]$/);
if (arrayMatch) {
relationName = arrayMatch[1];
relationIndex = parseInt(arrayMatch[2], 10);
}
const relationEntity = entity[relationName];
if (Array.isArray(relationEntity) && relationIndex !== null) {
const subEntity = relationEntity[relationIndex];
const value = subEntity?.[relationalField[1]];
resolvedPattern = resolvedPattern.replace(`[${field}]`, value ? slugify(String(value)) : '');
} else if (typeof relationEntity === 'object' && !Array.isArray(relationEntity)) {
const value = relationEntity?.[relationalField[1]];
resolvedPattern = resolvedPattern.replace(`[${field}]`, value ? slugify(String(value)) : '');
} else {
strapi.log.error('Something went wrong whilst resolving the pattern.');
}
}
});
resolvedPattern = resolvedPattern.replace(/\/+/g, '/'); // Remove duplicate forward slashes.
resolvedPattern = resolvedPattern.startsWith('/') ? resolvedPattern : `/${resolvedPattern}`; // Add a starting slash.
return resolvedPattern;
};
if (!urlPattern) {
return resolve(strapi.config.get('plugin::webtools.default_pattern'));
}
const path = resolve(urlPattern);
return path;
},
/**
* Validate if a pattern is correctly structured.
*
* @param {string[]} pattern - The pattern to validate.
* @param {string[]} allowedFieldNames - The allowed field names in the pattern.
* @returns {object} The validation result.
* @returns {boolean} object.valid - Validation boolean.
* @returns {string} object.message - Validation message.
*/
validatePattern: (pattern: string, allowedFieldNames: string[]) => {
if (!pattern.length) {
return {
valid: false,
message: 'Pattern cannot be empty',
};
}
const preCharCount = pattern.split('[').length - 1;
const postCharCount = pattern.split(']').length - 1;
if (preCharCount < 1 || postCharCount < 1) {
return {
valid: false,
message: 'Pattern should contain at least one field',
};
}
if (preCharCount !== postCharCount) {
return {
valid: false,
message: 'Fields in the pattern are not escaped correctly',
};
}
let fieldsAreAllowed = true;
// Pass the original `pattern` array to getFieldsFromPattern
getPluginService('url-pattern').getFieldsFromPattern(pattern).forEach((field) => {
const fieldName = field.replace(/\[\d+\]/g, '');
if (!allowedFieldNames.includes(fieldName)) fieldsAreAllowed = false;
});
if (!fieldsAreAllowed) {
return {
valid: false,
message: 'Pattern contains forbidden fields',
};
}
return {
valid: true,
message: 'Valid pattern',
};
},
});
export default factories.createCoreService(contentTypeSlug, customServices);