Skip to content

Commit 838ac14

Browse files
committed
feat: Add support for resolving URL patterns with indexed many-to-many relations.
1 parent 7a5b1ae commit 838ac14

File tree

2 files changed

+39
-8
lines changed

2 files changed

+39
-8
lines changed

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,18 @@ describe('url-pattern service', () => {
6262
const service = urlPatternService({ strapi: global.strapi });
6363

6464
describe('getAllowedFields', () => {
65-
it('should include fields from many-to-many relations', () => {
65+
it('should include fields from many-to-many relations with index', () => {
6666
const contentType = global.strapi.contentTypes['api::article.article'];
6767
const allowedFields = ['string'];
6868

6969
const fields = service.getAllowedFields(contentType, allowedFields);
7070

71-
expect(fields).toContain('categories.name');
71+
expect(fields).toContain('categories[0].name');
7272
});
7373
});
7474

7575
describe('resolvePattern', () => {
76-
it('should resolve pattern with many-to-many relation using the first item', () => {
76+
it('should resolve pattern with many-to-many relation using index', () => {
7777
const uid = 'api::article.article';
7878
const entity = {
7979
title: 'My Article',
@@ -82,20 +82,36 @@ describe('url-pattern service', () => {
8282
{ name: 'News' },
8383
],
8484
};
85-
const pattern = '/[categories.name]/[title]';
85+
const pattern = '/[categories[0].name]/[title]';
8686

8787
const resolvedPath = service.resolvePattern(uid as any, entity, pattern);
8888

8989
expect(resolvedPath).toBe('/tech/my-article');
9090
});
9191

92+
it('should resolve pattern with many-to-many relation using specific index', () => {
93+
const uid = 'api::article.article';
94+
const entity = {
95+
title: 'My Article',
96+
categories: [
97+
{ name: 'Tech' },
98+
{ name: 'News' },
99+
],
100+
};
101+
const pattern = '/[categories[1].name]/[title]';
102+
103+
const resolvedPath = service.resolvePattern(uid as any, entity, pattern);
104+
105+
expect(resolvedPath).toBe('/news/my-article');
106+
});
107+
92108
it('should handle empty many-to-many relation', () => {
93109
const uid = 'api::article.article';
94110
const entity = {
95111
title: 'My Article',
96112
categories: [],
97113
};
98-
const pattern = '/[categories.name]/[title]';
114+
const pattern = '/[categories[0].name]/[title]';
99115

100116
const resolvedPath = service.resolvePattern(uid as any, entity, pattern);
101117

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ const customServices = () => ({
6363

6464
typedEntries(relation.attributes).forEach(([subFieldName, subField]) => {
6565
if (subField.type === fieldType || subFieldName === fieldType) {
66-
fields.push(`${fieldName}.${subFieldName}`);
66+
if (field.relation.endsWith('ToMany')) {
67+
fields.push(`${fieldName}[0].${subFieldName}`);
68+
} else {
69+
fields.push(`${fieldName}.${subFieldName}`);
70+
}
6771
}
6872
});
6973
} else if (
@@ -105,7 +109,7 @@ const customServices = () => ({
105109
* @returns {string[]} The extracted fields.
106110
*/
107111
getFieldsFromPattern: (pattern: string): string[] => {
108-
const fields = pattern.match(/[[\w\d.]+]/g); // Get all substrings between [] as array.
112+
const fields = pattern.match(/\[.*?\]/g);
109113

110114
if (!fields) {
111115
return [];
@@ -130,7 +134,7 @@ const customServices = () => ({
130134
fields = fields.filter((field) => field);
131135

132136
// For fields containing dots, extract the first part (relation)
133-
const relations = fields.filter((field) => field.includes('.')).map((field) => field.split('.')[0]);
137+
const relations = fields.filter((field) => field.includes('.')).map((field) => field.split('.')[0].replace(/\[\d+\]/, ''));
134138

135139
return relations;
136140
},
@@ -171,6 +175,17 @@ const customServices = () => ({
171175
} else if (!relationalField) {
172176
const fieldValue = slugify(String(entity[field]));
173177
resolvedPattern = resolvedPattern.replace(`[${field}]`, fieldValue || '');
178+
} else if (relationalField[0].match(/\[\d+\]/)) {
179+
const relationName = relationalField[0].split('[')[0];
180+
const index = parseInt(relationalField[0].split('[')[1].split(']')[0], 10);
181+
182+
if (Array.isArray(entity[relationName]) && entity[relationName][index]) {
183+
const relation = entity[relationName][index];
184+
const fieldValue = slugify(String(relation[relationalField[1]]));
185+
resolvedPattern = resolvedPattern.replace(`[${field}]`, fieldValue || '');
186+
} else {
187+
resolvedPattern = resolvedPattern.replace(`[${field}]`, '');
188+
}
174189
} else if (Array.isArray(entity[relationalField[0]])) {
175190
// If the relation is a one-to-many or many-to-many, we use the first one.
176191
const relation = entity[relationalField[0]][0];

0 commit comments

Comments
 (0)