Skip to content

Commit 888d218

Browse files
committed
feat: Add validation for array index in ToMany relations within URL patterns.
1 parent 2a6bbf6 commit 888d218

File tree

3 files changed

+56
-37
lines changed

3 files changed

+56
-37
lines changed

packages/core/server/controllers/url-pattern.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default factories.createCoreController(contentTypeSlug, ({ strapi }) => (
4848
'uid',
4949
'documentId',
5050
]);
51-
const validated = urlPatternService.validatePattern(pattern, fields);
51+
const validated = urlPatternService.validatePattern(pattern, fields, contentType);
5252

5353
ctx.body = validated;
5454
},

packages/core/server/services/__tests__/url-pattern.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,25 @@ describe('URL Pattern Service', () => {
146146
});
147147

148148
describe('validatePattern', () => {
149+
it('should invalidate pattern with ToMany relation missing array index', () => {
150+
const pattern = '/test/[private_categories.slug]/1';
151+
const allowedFields = ['private_categories.slug'];
152+
const contentType = {
153+
attributes: {
154+
private_categories: {
155+
type: 'relation',
156+
relation: 'manyToMany',
157+
target: 'api::category.category',
158+
},
159+
},
160+
} as any;
161+
162+
const result = service.validatePattern(pattern, allowedFields, contentType);
163+
164+
expect(result.valid).toBe(false);
165+
expect(result.message).toContain('must include an array index');
166+
});
167+
149168
it('should validate pattern with underscored relation name', () => {
150169
const pattern = '/test/[private_categories[0].slug]/1';
151170
const allowedFields = ['private_categories.slug'];

packages/core/server/services/url-pattern.ts

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -214,55 +214,55 @@ const customServices = () => ({
214214
/**
215215
* Validate if a pattern is correctly structured.
216216
*
217-
* @param {string[]} pattern - The pattern to validate.
218-
* @param {string[]} allowedFieldNames - The allowed field names in the pattern.
217+
* @param {string} pattern - The pattern to validate.
218+
* @param {string[]} allowedFieldNames - The allowed fields.
219+
* @param {Schema.ContentType} contentType - The content type.
219220
* @returns {object} The validation result.
220-
* @returns {boolean} object.valid - Validation boolean.
221-
* @returns {string} object.message - Validation message.
222221
*/
223-
validatePattern: (pattern: string, allowedFieldNames: string[]) => {
224-
if (!pattern.length) {
222+
validatePattern: (pattern: string, allowedFieldNames: string[], contentType?: Schema.ContentType): { valid: boolean, message: string } => {
223+
if (!pattern) {
225224
return {
226225
valid: false,
227226
message: 'Pattern cannot be empty',
228227
};
229228
}
230229

231-
const preCharCount = pattern.split('[').length - 1;
232-
const postCharCount = pattern.split(']').length - 1;
230+
const fields = getPluginService('url-pattern').getFieldsFromPattern(pattern);
231+
let valid = true;
232+
let message = '';
233233

234-
if (preCharCount < 1 || postCharCount < 1) {
235-
return {
236-
valid: false,
237-
message: 'Pattern should contain at least one field',
238-
};
239-
}
240-
241-
if (preCharCount !== postCharCount) {
242-
return {
243-
valid: false,
244-
message: 'Fields in the pattern are not escaped correctly',
245-
};
246-
}
247-
248-
let fieldsAreAllowed = true;
249-
250-
// Pass the original `pattern` array to getFieldsFromPattern
251-
getPluginService('url-pattern').getFieldsFromPattern(pattern).forEach((field) => {
234+
fields.forEach((field) => {
235+
// Check if the field is allowed.
236+
// We strip the array index from the field name to check if it is allowed.
237+
// e.g. private_categories[0].slug -> private_categories.slug
252238
const fieldName = field.replace(/\[\d+\]/g, '');
253-
if (!allowedFieldNames.includes(fieldName)) fieldsAreAllowed = false;
239+
if (!allowedFieldNames.includes(fieldName)) {
240+
valid = false;
241+
message = `Pattern contains forbidden fields: ${field}`;
242+
}
243+
244+
// Check if the field is a ToMany relation and has an array index.
245+
if (contentType && field.includes('.')) {
246+
const [relationName] = field.split('.');
247+
// Strip array index to get the attribute name
248+
const attributeName = relationName.replace(/\[\d+\]/g, '');
249+
const attribute = contentType.attributes[attributeName];
250+
251+
if (
252+
attribute
253+
&& attribute.type === 'relation'
254+
&& !attribute.relation.endsWith('ToOne')
255+
&& !relationName.includes('[')
256+
) {
257+
valid = false;
258+
message = `The relation ${attributeName} is a ToMany relation and must include an array index (e.g. ${attributeName}[0]).`;
259+
}
260+
}
254261
});
255262

256-
if (!fieldsAreAllowed) {
257-
return {
258-
valid: false,
259-
message: 'Pattern contains forbidden fields',
260-
};
261-
}
262-
263263
return {
264-
valid: true,
265-
message: 'Valid pattern',
264+
valid,
265+
message,
266266
};
267267
},
268268
});

0 commit comments

Comments
 (0)