Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useIntl } from 'react-intl';
import { Toggle } from '@strapi/design-system';
import { NavigationItemCustomField } from '../../../../../../../schemas';
import { getTrad } from '../../../../../../../translations';
import { Textarea } from '@strapi/design-system';

export type AdditionalFieldInputProps = {
name?: string;
Expand Down Expand Up @@ -93,7 +94,7 @@ export const AdditionalFieldInput: React.FC<AdditionalFieldInputProps> = ({
);
case 'string':
return (
<TextInput
<Textarea
{...defaultInputProps}
onChange={(eventOrPath: React.ChangeEvent<any> | string, value?: any) =>
onChangeEnhancer(eventOrPath, value, onChange)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "strapi-plugin-navigation",
"version": "3.2.6",
"version": "3.2.7",
"description": "Strapi - Navigation plugin",
"strapi": {
"name": "navigation",
Expand Down
14 changes: 1 addition & 13 deletions server/src/controllers/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { Core } from '@strapi/strapi';
import { Context as KoaContext } from 'koa';
import * as z from 'zod';
import { getPluginService } from '../utils';
import { sanitizePopulateField } from './utils';
import {
populateSchema,
readAllQuerySchema,
renderChildQueryParams,
renderQuerySchema,
Expand Down Expand Up @@ -53,17 +51,7 @@ export default function clientController(context: { strapi: Core.Strapi }) {
menuOnly: menuOnly === 'true',
rootPath,
locale,
populate: sanitizePopulateField(
populateSchema.parse(
populate === 'true'
? true
: populate === 'false'
? false
: Array.isArray(populate)
? populate.map((x) => (x === 'true' ? true : x === 'false' ? false : populate))
: populate
)
),
populate,
status,
});
},
Expand Down
18 changes: 0 additions & 18 deletions server/src/controllers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import { z } from 'zod';
import { idSchema } from './validators';

type Populate = string | boolean | string[] | undefined;

export const sanitizePopulateField = (populate: Populate): Populate => {
if (!populate || populate === true || populate === '*') {
return undefined;
}

if ('object' === typeof populate) {
return undefined;
}

if (Array.isArray(populate)) {
return populate;
}

return populate;
};

export const parseId = (id: string) => {
return Number.isNaN(parseInt(id)) ? z.string().parse(id) : idSchema.parse(parseInt(id));
};
42 changes: 40 additions & 2 deletions server/src/controllers/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,47 @@ export const readAllQuerySchema = z.object({

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

export const statusSchema = z.enum(['draft', 'published']);
export const statusSchema = z
.string()
.transform((v) => v === 'published' ? 'published' : 'draft')
.pipe(z.enum(['draft', 'published']));

export const populateSchema = z.union([z.boolean(), z.string(), z.string().array(), z.undefined()]);
// 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
// in the zod v4 there's also z.stringbool that should simplify this logic
type PopulatePrimitive = boolean | string | string[] | undefined;

export interface PopulateObject {
[key: string]: Populate;
}

type Populate = PopulatePrimitive | PopulateObject;

const sanitizePopulateField = (populate: unknown) => {
if (typeof populate === 'string') {
if (populate === 'true') {
return true;
}
if (populate === 'false') {
return false;
}
}
return populate;
};

export const populateSchema: z.ZodType<Populate, z.ZodTypeDef, unknown> = z.lazy(() =>
z.preprocess(
sanitizePopulateField,
z.union([
z.boolean(),
z.string(),
z.string().array(),
z.undefined(),
z.record(populateSchema)
]),
),
);

export type PopulateQueryParam = z.infer<typeof populateSchema>;

export const renderQuerySchema = z.object({
type: renderTypeSchema.optional(),
Expand Down
3 changes: 1 addition & 2 deletions server/src/services/client/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { NavigationItemDTO, RFRNavigationItemDTO } from '../../dtos';
import { PopulateQueryParam } from '../../controllers/validators';

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

export type PopulateQueryParam = string | boolean | string[];

export type NestedPath = {
id?: number;
documentId?: string;
Expand Down
2 changes: 1 addition & 1 deletion server/src/services/common/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const commonService = (context: { strapi: Core.Strapi }) => ({
return item;
}

const fieldsToPopulate = config.contentTypesPopulate[item.related.__type];
const fieldsToPopulate = populate ?? config.contentTypesPopulate[item.related.__type];

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

Expand Down
89 changes: 89 additions & 0 deletions server/tests/controllers/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,95 @@ describe('Navigation', () => {
);
}).rejects.toThrow();
});

it('should fallback to status=draft if status differs from draft or published', async () => {
// Given
const navigation = getMockNavigation();
const render = jest.fn();
const mockClientService = asProxy<ClientService>({ render });
const idOrSlug = faker.string.uuid();
const type = faker.helpers.arrayElement(['FLAT', 'TREE', 'RFR']);
const menuOnly = faker.datatype.boolean();

render.mockResolvedValue(navigation);

(getPluginService as jest.Mock).mockReturnValue(mockClientService);

const clientController = buildClientController({ strapi });
// When
const resultDraft = await clientController.render(
asProxy<KoaContext>({
params: { idOrSlug },
query: {
type,
menuOnly,
status: faker.string.sample(),
},
})
);

// Then
expect(resultDraft).toEqual(navigation);
// When
const resultPublished = await clientController.render(
asProxy<KoaContext>({
params: { idOrSlug },
query: {
type,
menuOnly,
status: 'published',
},
})
);

// Then
expect(resultPublished).toEqual(navigation);
});

it('should sanitize populate query param', async () => {
// Given
const navigation = getMockNavigation();
const render = jest.fn();
const mockClientService = asProxy<ClientService>({ render });
const idOrSlug = faker.string.uuid();
const type = faker.helpers.arrayElement(['FLAT', 'TREE', 'RFR']);
const menuOnly = faker.datatype.boolean();

render.mockResolvedValue(navigation);

(getPluginService as jest.Mock).mockReturnValue(mockClientService);

const clientController = buildClientController({ strapi });
// When
const resultTrue = await clientController.render(
asProxy<KoaContext>({
params: { idOrSlug },
query: {
type,
menuOnly,
populate: { foo: { populate: 'true' }}
},
})
);

// Then
expect(resultTrue).toEqual(navigation);

// When
const resultFalse = await clientController.render(
asProxy<KoaContext>({
params: { idOrSlug },
query: {
type,
menuOnly,
populate: { foo: { populate: 'false' }}
},
})
);

// Then
expect(resultFalse).toEqual(navigation);
});
});

describe('renderChild()', () => {
Expand Down