I am using "@strapi/strapi": "^4.12.5",
and have to say that this is one amazing open-source CMS.
There are things missing, like the one I need help with and will explain below.
I have the following content-type named Block
with the schema as below
{
"kind": "collectionType",
"collectionName": "blocks",
"info": {
"singularName": "block",
"pluralName": "blocks",
"displayName": "Block"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"container": {
"pluginOptions": {
"i18n": {
"localized": false
}
},
"type": "string",
"maxLength": 512,
"required": true,
"unique": false
},
"key": {
"pluginOptions": {
"i18n": {
"localized": false
}
},
"type": "string",
"maxLength": 512,
"required": true
},
"sections": {
"pluginOptions": {
"i18n": {
"localized": true
}
},
"type": "dynamiczone",
"components": [
"sections.rich-text"
],
"required": true
}
}
}
I want to make sure that the combination of container
-key
-locale
cannot be present 2 times, so that combination is unique.
Example of the desired functionality:
This is the default generated src/api/block/controllers/block.ts
controller
/**
* block controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::block.block');
What is the best approach to add this validation logic and show a meaningful message to the admin UI ?
With the code and schema that I shared there is no validation in place and the content editor can add duplicate configs like the one I want to avoid.
PS: Strapi does not allow combinated keys or unique key per locale
yet out of the box via the UI.
I found the solution I was looking for.
I used lifecycles.ts
to hook into the beforeCreate
and beforeUpdate
event of my content type.
https://docs.strapi.io/dev-docs/backend-customization/models#lifecycle-hooks
I did this by creating src/api/block/content-types/block/lifecycles.ts
with the following code:
// eslint-disable-next-line import/no-extraneous-dependencies
import { YupValidationError } from '@strapi/utils/dist/errors';
// eslint-disable-next-line import/no-extraneous-dependencies
import { ValidationError } from 'yup';
const resourceType = 'api::block.block';
type Data = { locale: string; container: string; key: string };
export default {
async beforeCreate(event) {
const { data } = event.params;
const existing = await getExisting(data);
if (existing) {
throwDuplicateCombinedKeyValidationError();
}
},
async beforeUpdate(event) {
const id = event.params.where.id;
const { data } = event.params;
// Strapi has a bug that does not send the locale during update so we have to fetch the existing entry and see its locale
// only the updated values are sent into data, we have to merge with existing actual ones
const actual = await getById(id);
// eslint-disable-next-line no-undef
const existingWithCombinedKey = await getExisting({
locale: actual.locale,
container: data.container ?? actual.container,
key: data.key ?? actual.key,
});
if (existingWithCombinedKey !== null && existingWithCombinedKey.id !== id) {
throwDuplicateCombinedKeyValidationError();
}
},
};
async function getById(id: number): Promise<Data | null> {
// @ts-ignore
// eslint-disable-next-line no-undef
const block = await strapi.service(resourceType).findOne(id);
if (block === null) {
return null;
}
return block;
}
function throwDuplicateCombinedKeyValidationError(): void {
const errorMessage: ValidationError = {
inner: [
{
name: 'ValidationError', // Always set to ValidationError
path: 'container', // Name of field we want to show input validation on
message: 'duplicate', // Input validation message
},
{
name: 'ValidationError', // Always set to ValidationError
path: 'key', // Name of field we want to show input validation on
message: 'duplicate', // Input validation message
},
],
} as ValidationError;
throw new YupValidationError(errorMessage, 'Duplicate combined key container-key-locale error');
}
async function getExisting(data: Data): Promise<{ id: number } | null> {
// @ts-ignore
// eslint-disable-next-line no-undef
return await strapi.db
.query(resourceType)
.findOne({ where: { container: data.container, key: data.key, locale: data.locale } });
}
Throwing the YupValidationError
error is supported by the frontend as well and you can achieve my 2nd goal of showing proper message to the content editor user:
I want to thank the Strapi team for the great product and also suggest that they extend the Typescript
types for the events for easier development time.
event
has no type in Strapi atm, also the global strapi
does not work.