Skip to content

Commit 215fb17

Browse files
fix: fix population schema (#628)
* fix: fix population schema * feat: remove test console.log * fix: fallback to draft status in case invalid value was provided * fix: test coverage
1 parent cd99d4c commit 215fb17

8 files changed

Lines changed: 135 additions & 38 deletions

File tree

admin/src/pages/HomePage/components/NavigationItemForm/components/AdditionalFields/AdditionalFieldInput/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useIntl } from 'react-intl';
1313
import { Toggle } from '@strapi/design-system';
1414
import { NavigationItemCustomField } from '../../../../../../../schemas';
1515
import { getTrad } from '../../../../../../../translations';
16+
import { Textarea } from '@strapi/design-system';
1617

1718
export type AdditionalFieldInputProps = {
1819
name?: string;
@@ -93,7 +94,7 @@ export const AdditionalFieldInput: React.FC<AdditionalFieldInputProps> = ({
9394
);
9495
case 'string':
9596
return (
96-
<TextInput
97+
<Textarea
9798
{...defaultInputProps}
9899
onChange={(eventOrPath: React.ChangeEvent<any> | string, value?: any) =>
99100
onChangeEnhancer(eventOrPath, value, onChange)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "strapi-plugin-navigation",
3-
"version": "3.2.6",
3+
"version": "3.2.7",
44
"description": "Strapi - Navigation plugin",
55
"strapi": {
66
"name": "navigation",

server/src/controllers/client.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { Core } from '@strapi/strapi';
22
import { Context as KoaContext } from 'koa';
33
import * as z from 'zod';
44
import { getPluginService } from '../utils';
5-
import { sanitizePopulateField } from './utils';
65
import {
7-
populateSchema,
86
readAllQuerySchema,
97
renderChildQueryParams,
108
renderQuerySchema,
@@ -53,17 +51,7 @@ export default function clientController(context: { strapi: Core.Strapi }) {
5351
menuOnly: menuOnly === 'true',
5452
rootPath,
5553
locale,
56-
populate: sanitizePopulateField(
57-
populateSchema.parse(
58-
populate === 'true'
59-
? true
60-
: populate === 'false'
61-
? false
62-
: Array.isArray(populate)
63-
? populate.map((x) => (x === 'true' ? true : x === 'false' ? false : populate))
64-
: populate
65-
)
66-
),
54+
populate,
6755
status,
6856
});
6957
},

server/src/controllers/utils.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,6 @@
11
import { z } from 'zod';
22
import { idSchema } from './validators';
33

4-
type Populate = string | boolean | string[] | undefined;
5-
6-
export const sanitizePopulateField = (populate: Populate): Populate => {
7-
if (!populate || populate === true || populate === '*') {
8-
return undefined;
9-
}
10-
11-
if ('object' === typeof populate) {
12-
return undefined;
13-
}
14-
15-
if (Array.isArray(populate)) {
16-
return populate;
17-
}
18-
19-
return populate;
20-
};
21-
224
export const parseId = (id: string) => {
235
return Number.isNaN(parseInt(id)) ? z.string().parse(id) : idSchema.parse(parseInt(id));
246
};

server/src/controllers/validators.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,47 @@ export const readAllQuerySchema = z.object({
1212

1313
export const renderTypeSchema = z.enum(['FLAT', 'TREE', 'RFR']);
1414

15-
export const statusSchema = z.enum(['draft', 'published']);
15+
export const statusSchema = z
16+
.string()
17+
.transform((v) => v === 'published' ? 'published' : 'draft')
18+
.pipe(z.enum(['draft', 'published']));
1619

17-
export const populateSchema = z.union([z.boolean(), z.string(), z.string().array(), z.undefined()]);
20+
// TODO in the zod v3 we can't use z.lazy and recursive types without creating a custom type. Let's align on this when Strapi will use zod v4
21+
// in the zod v4 there's also z.stringbool that should simplify this logic
22+
type PopulatePrimitive = boolean | string | string[] | undefined;
23+
24+
export interface PopulateObject {
25+
[key: string]: Populate;
26+
}
27+
28+
type Populate = PopulatePrimitive | PopulateObject;
29+
30+
const sanitizePopulateField = (populate: unknown) => {
31+
if (typeof populate === 'string') {
32+
if (populate === 'true') {
33+
return true;
34+
}
35+
if (populate === 'false') {
36+
return false;
37+
}
38+
}
39+
return populate;
40+
};
41+
42+
export const populateSchema: z.ZodType<Populate, z.ZodTypeDef, unknown> = z.lazy(() =>
43+
z.preprocess(
44+
sanitizePopulateField,
45+
z.union([
46+
z.boolean(),
47+
z.string(),
48+
z.string().array(),
49+
z.undefined(),
50+
z.record(populateSchema)
51+
]),
52+
),
53+
);
54+
55+
export type PopulateQueryParam = z.infer<typeof populateSchema>;
1856

1957
export const renderQuerySchema = z.object({
2058
type: renderTypeSchema.optional(),

server/src/services/client/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { NavigationItemDTO, RFRNavigationItemDTO } from '../../dtos';
2+
import { PopulateQueryParam } from '../../controllers/validators';
23

34
export type RenderType = 'FLAT' | 'TREE' | 'RFR';
45

5-
export type PopulateQueryParam = string | boolean | string[];
6-
76
export type NestedPath = {
87
id?: number;
98
documentId?: string;

server/src/services/common/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const commonService = (context: { strapi: Core.Strapi }) => ({
7979
return item;
8080
}
8181

82-
const fieldsToPopulate = config.contentTypesPopulate[item.related.__type];
82+
const fieldsToPopulate = populate ?? config.contentTypesPopulate[item.related.__type];
8383

8484
const repository = getGenericRepository({ strapi }, item.related.__type as UID.ContentType);
8585

server/tests/controllers/client.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,95 @@ describe('Navigation', () => {
195195
);
196196
}).rejects.toThrow();
197197
});
198+
199+
it('should fallback to status=draft if status differs from draft or published', async () => {
200+
// Given
201+
const navigation = getMockNavigation();
202+
const render = jest.fn();
203+
const mockClientService = asProxy<ClientService>({ render });
204+
const idOrSlug = faker.string.uuid();
205+
const type = faker.helpers.arrayElement(['FLAT', 'TREE', 'RFR']);
206+
const menuOnly = faker.datatype.boolean();
207+
208+
render.mockResolvedValue(navigation);
209+
210+
(getPluginService as jest.Mock).mockReturnValue(mockClientService);
211+
212+
const clientController = buildClientController({ strapi });
213+
// When
214+
const resultDraft = await clientController.render(
215+
asProxy<KoaContext>({
216+
params: { idOrSlug },
217+
query: {
218+
type,
219+
menuOnly,
220+
status: faker.string.sample(),
221+
},
222+
})
223+
);
224+
225+
// Then
226+
expect(resultDraft).toEqual(navigation);
227+
// When
228+
const resultPublished = await clientController.render(
229+
asProxy<KoaContext>({
230+
params: { idOrSlug },
231+
query: {
232+
type,
233+
menuOnly,
234+
status: 'published',
235+
},
236+
})
237+
);
238+
239+
// Then
240+
expect(resultPublished).toEqual(navigation);
241+
});
242+
243+
it('should sanitize populate query param', async () => {
244+
// Given
245+
const navigation = getMockNavigation();
246+
const render = jest.fn();
247+
const mockClientService = asProxy<ClientService>({ render });
248+
const idOrSlug = faker.string.uuid();
249+
const type = faker.helpers.arrayElement(['FLAT', 'TREE', 'RFR']);
250+
const menuOnly = faker.datatype.boolean();
251+
252+
render.mockResolvedValue(navigation);
253+
254+
(getPluginService as jest.Mock).mockReturnValue(mockClientService);
255+
256+
const clientController = buildClientController({ strapi });
257+
// When
258+
const resultTrue = await clientController.render(
259+
asProxy<KoaContext>({
260+
params: { idOrSlug },
261+
query: {
262+
type,
263+
menuOnly,
264+
populate: { foo: { populate: 'true' }}
265+
},
266+
})
267+
);
268+
269+
// Then
270+
expect(resultTrue).toEqual(navigation);
271+
272+
// When
273+
const resultFalse = await clientController.render(
274+
asProxy<KoaContext>({
275+
params: { idOrSlug },
276+
query: {
277+
type,
278+
menuOnly,
279+
populate: { foo: { populate: 'false' }}
280+
},
281+
})
282+
);
283+
284+
// Then
285+
expect(resultFalse).toEqual(navigation);
286+
});
198287
});
199288

200289
describe('renderChild()', () => {

0 commit comments

Comments
 (0)