Merge pull request #5258 from strapi/single-types/validators

Init entity validator layer
This commit is contained in:
Alexandre BODIN 2020-02-18 12:18:58 +01:00 committed by GitHub
commit a6708c3e46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 675 additions and 229 deletions

View File

@ -4,4 +4,5 @@ module.exports = {
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
printWidth: 100,
};

View File

@ -28,8 +28,22 @@
"component": "default.closingperiod",
"type": "component"
},
"contact_email": {
"type": "email"
},
"stars": {
"required": true,
"type": "integer",
"min": 0,
"max": 3
},
"averagePrice": {
"type": "float",
"min": 0,
"max": 35.12
},
"address": {
"required": true,
"model": "address"
},
"cover": {
@ -38,6 +52,9 @@
"plugin": "upload",
"required": false
},
"timestamp": {
"type": "timestamp"
},
"images": {
"collection": "file",
"via": "related",
@ -55,7 +72,8 @@
},
"description": {
"type": "richtext",
"required": true
"required": true,
"minLength": 10
},
"services": {
"component": "default.restaurantservice",

View File

@ -20,14 +20,11 @@ describe('Test type boolean', () => {
}, 60000);
test('Create entry with value input JSON', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withboolean.withboolean',
{
body: {
field: true,
},
}
);
const res = await rq.post('/content-manager/explorer/application::withboolean.withboolean', {
body: {
field: true,
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
@ -36,14 +33,11 @@ describe('Test type boolean', () => {
});
test('Create entry with value input FromData', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withboolean.withboolean',
{
formData: {
data: JSON.stringify({ field: true }),
},
}
);
const res = await rq.post('/content-manager/explorer/application::withboolean.withboolean', {
formData: {
data: JSON.stringify({ field: true }),
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
@ -51,27 +45,27 @@ describe('Test type boolean', () => {
});
});
test.todo('Throws on invalid boolean value');
test('Throws on invalid boolean value', async () => {
let res = await rq.post('/content-manager/explorer/application::withboolean.withboolean', {
body: { field: 'random' },
});
expect(res.statusCode).toBe(400);
});
test('Convert integer to boolean value', async () => {
let res = await rq.post(
'/content-manager/explorer/application::withboolean.withboolean',
{
body: { field: 1 },
}
);
let res = await rq.post('/content-manager/explorer/application::withboolean.withboolean', {
body: { field: 1 },
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
field: true,
});
res = await rq.post(
'/content-manager/explorer/application::withboolean.withboolean',
{
body: { field: 0 },
}
);
res = await rq.post('/content-manager/explorer/application::withboolean.withboolean', {
body: { field: 0 },
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
@ -80,9 +74,7 @@ describe('Test type boolean', () => {
});
test('Reading entry, returns correct value', async () => {
const res = await rq.get(
'/content-manager/explorer/application::withboolean.withboolean'
);
const res = await rq.get('/content-manager/explorer/application::withboolean.withboolean');
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
@ -96,14 +88,11 @@ describe('Test type boolean', () => {
});
test('Updating entry sets the right value and format', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withboolean.withboolean',
{
body: {
field: true,
},
}
);
const res = await rq.post('/content-manager/explorer/application::withboolean.withboolean', {
body: {
field: true,
},
});
const updateRes = await rq.put(
`/content-manager/explorer/application::withboolean.withboolean/${res.body.id}`,

View File

@ -20,43 +20,43 @@ describe('Test type email', () => {
}, 60000);
test('Create entry with value input JSON', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withemail.withemail',
{
body: {
field: 'someemail',
},
}
);
const res = await rq.post('/content-manager/explorer/application::withemail.withemail', {
body: {
field: 'validemail@test.fr',
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
field: 'someemail',
field: 'validemail@test.fr',
});
});
test.todo('Should Throw on invalid email');
test('Should Throw on invalid email', async () => {
const res = await rq.post('/content-manager/explorer/application::withemail.withemail', {
body: {
field: 'invalidemail',
},
});
expect(res.statusCode).toBe(400);
});
test('Create entry with value input Formdata', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withemail.withemail',
{
body: {
field: 1234567,
},
}
);
const res = await rq.post('/content-manager/explorer/application::withemail.withemail', {
body: {
field: 'test@email.fr',
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
field: '1234567',
field: 'test@email.fr',
});
});
test('Reading entry returns correct value', async () => {
const res = await rq.get(
'/content-manager/explorer/application::withemail.withemail'
);
const res = await rq.get('/content-manager/explorer/application::withemail.withemail');
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
@ -70,20 +70,17 @@ describe('Test type email', () => {
});
test('Updating entry sets the right value and format', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withemail.withemail',
{
body: {
field: 'someemail',
},
}
);
const res = await rq.post('/content-manager/explorer/application::withemail.withemail', {
body: {
field: 'valid@email.fr',
},
});
const updateRes = await rq.put(
`/content-manager/explorer/application::withemail.withemail/${res.body.id}`,
{
body: {
field: 'otherPwd',
field: 'new-email@email.fr',
},
}
);
@ -91,7 +88,7 @@ describe('Test type email', () => {
expect(updateRes.statusCode).toBe(200);
expect(updateRes.body).toMatchObject({
id: res.body.id,
field: 'otherPwd',
field: 'new-email@email.fr',
});
});
});

View File

@ -12,13 +12,9 @@ describe('Test type enumeration', () => {
modelsUtils = createModelsUtils({ rq });
await modelsUtils.createContentTypeWithType(
'withenumeration',
'enumeration',
{
enum: ['one', 'two'],
}
);
await modelsUtils.createContentTypeWithType('withenumeration', 'enumeration', {
enum: ['one', 'two'],
});
}, 60000);
afterAll(async () => {
@ -95,8 +91,32 @@ describe('Test type enumeration', () => {
});
});
/*
* Waiting validation of input to work
*/
test.todo('Throws an error when the enumeration value is not in the options');
test('Allows null value', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withenumeration.withenumeration',
{
body: {
field: null,
},
}
);
expect(res.statusCode).toBe(200); // should return 201
expect(res.body).toMatchObject({
field: null,
});
});
test('Throws an error when the enumeration value is not in the options', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withenumeration.withenumeration',
{
body: {
field: 'invalid-value',
},
}
);
expect(res.statusCode).toBe(400);
});
});

View File

@ -23,14 +23,11 @@ describe('Test type json', () => {
const inputValue = {
key: 'value',
};
const res = await rq.post(
'/content-manager/explorer/application::withjson.withjson',
{
body: {
field: inputValue,
},
}
);
const res = await rq.post('/content-manager/explorer/application::withjson.withjson', {
body: {
field: inputValue,
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
@ -47,14 +44,11 @@ describe('Test type json', () => {
key: 'value',
},
];
const res = await rq.post(
'/content-manager/explorer/application::withjson.withjson',
{
body: {
field: inputValue,
},
}
);
const res = await rq.post('/content-manager/explorer/application::withjson.withjson', {
body: {
field: inputValue,
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
@ -66,14 +60,11 @@ describe('Test type json', () => {
const inputValue = {
number: '12',
};
const res = await rq.post(
'/content-manager/explorer/application::withjson.withjson',
{
formData: {
data: JSON.stringify({ field: inputValue }),
},
}
);
const res = await rq.post('/content-manager/explorer/application::withjson.withjson', {
formData: {
data: JSON.stringify({ field: inputValue }),
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
@ -82,9 +73,7 @@ describe('Test type json', () => {
});
test('Reading entry, returns correct value', async () => {
const res = await rq.get(
'/content-manager/explorer/application::withjson.withjson'
);
const res = await rq.get('/content-manager/explorer/application::withjson.withjson');
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
@ -98,16 +87,13 @@ describe('Test type json', () => {
test.todo('Throw when input is not a nested object');
test('Updating entry sets the right value and format', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withjson.withjson',
{
body: {
field: {
key: 'value',
},
const res = await rq.post('/content-manager/explorer/application::withjson.withjson', {
body: {
field: {
key: 'value',
},
}
);
},
});
const updateRes = await rq.put(
`/content-manager/explorer/application::withjson.withjson/${res.body.id}`,

View File

@ -20,14 +20,11 @@ describe('Test type password', () => {
}, 60000);
test('Create entry with value input JSON', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withpassword.withpassword',
{
body: {
field: 'somePassword',
},
}
);
const res = await rq.post('/content-manager/explorer/application::withpassword.withpassword', {
body: {
field: 'somePassword',
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
@ -38,14 +35,11 @@ describe('Test type password', () => {
test.todo('Should be private by default');
test('Create entry with value input Formdata', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withpassword.withpassword',
{
body: {
field: 1234567,
},
}
);
const res = await rq.post('/content-manager/explorer/application::withpassword.withpassword', {
body: {
field: '1234567',
},
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
@ -54,9 +48,7 @@ describe('Test type password', () => {
});
test('Reading entry returns correct value', async () => {
const res = await rq.get(
'/content-manager/explorer/application::withpassword.withpassword'
);
const res = await rq.get('/content-manager/explorer/application::withpassword.withpassword');
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
@ -70,14 +62,11 @@ describe('Test type password', () => {
});
test('Updating entry sets the right value and format', async () => {
const res = await rq.post(
'/content-manager/explorer/application::withpassword.withpassword',
{
body: {
field: 'somePassword',
},
}
);
const res = await rq.post('/content-manager/explorer/application::withpassword.withpassword', {
body: {
field: 'somePassword',
},
});
const updateRes = await rq.put(
`/content-manager/explorer/application::withpassword.withpassword/${res.body.id}`,

View File

@ -42,9 +42,7 @@ describe('convertRestQueryParams', () => {
expect(() => convertRestQueryParams({ _sort: {} })).toThrow();
expect(() => convertRestQueryParams({ _sort: 'id,,test' })).toThrow();
expect(() => convertRestQueryParams({ _sort: 'id,test,' })).toThrow();
expect(() =>
convertRestQueryParams({ _sort: 'id:asc,test:dasc' })
).toThrow();
expect(() => convertRestQueryParams({ _sort: 'id:asc,test:dasc' })).toThrow();
expect(() => convertRestQueryParams({ _sort: 'id:asc,:asc' })).toThrow();
});
@ -56,15 +54,24 @@ describe('convertRestQueryParams', () => {
['id:asc', [{ field: 'id', order: 'asc' }]],
[
'id,price',
[{ field: 'id', order: 'asc' }, { field: 'price', order: 'asc' }],
[
{ field: 'id', order: 'asc' },
{ field: 'price', order: 'asc' },
],
],
[
'id:desc,price',
[{ field: 'id', order: 'desc' }, { field: 'price', order: 'asc' }],
[
{ field: 'id', order: 'desc' },
{ field: 'price', order: 'asc' },
],
],
[
'id:desc,price:desc',
[{ field: 'id', order: 'desc' }, { field: 'price', order: 'desc' }],
[
{ field: 'id', order: 'desc' },
{ field: 'price', order: 'desc' },
],
],
[
'id:asc,price,date:desc',
@ -105,14 +112,15 @@ describe('convertRestQueryParams', () => {
expect(() => convertRestQueryParams({ _start: {} })).toThrow();
});
test.each([['1', 1], ['12', 12], ['0', 0]])(
'Converts start query "%s" correctly',
(input, expected) => {
expect(convertRestQueryParams({ _start: input })).toMatchObject({
start: expected,
});
}
);
test.each([
['1', 1],
['12', 12],
['0', 0],
])('Converts start query "%s" correctly', (input, expected) => {
expect(convertRestQueryParams({ _start: input })).toMatchObject({
start: expected,
});
});
});
describe('Limit param', () => {
@ -131,25 +139,20 @@ describe('convertRestQueryParams', () => {
expect(() => convertRestQueryParams({ _limit: {} })).toThrow();
});
test.each([['1', 1], ['12', 12], ['0', 0]])(
'Converts start query "%s" correctly',
(input, expected) => {
expect(convertRestQueryParams({ _start: input })).toMatchObject({
start: expected,
});
}
);
});
describe('Populate', () => {
test.todo('Not Eq');
test.each([
['1', 1],
['12', 12],
['0', 0],
])('Converts start query "%s" correctly', (input, expected) => {
expect(convertRestQueryParams({ _start: input })).toMatchObject({
start: expected,
});
});
});
describe('Filters', () => {
test('Can combine filters', () => {
expect(
convertRestQueryParams({ id: '1', test_ne: 'text', test_: 'content' })
).toMatchObject({
expect(convertRestQueryParams({ id: '1', test_ne: 'text', test_: 'content' })).toMatchObject({
where: [
{
field: 'id',
@ -186,9 +189,7 @@ describe('convertRestQueryParams', () => {
],
});
expect(
convertRestQueryParams({ id_eq: '1', test_eq: 'text' })
).toMatchObject({
expect(convertRestQueryParams({ id_eq: '1', test_eq: 'text' })).toMatchObject({
where: [
{
field: 'id',
@ -203,9 +204,7 @@ describe('convertRestQueryParams', () => {
],
});
expect(
convertRestQueryParams({ published_at: '2019-01-01:00:00:00' })
).toMatchObject({
expect(convertRestQueryParams({ published_at: '2019-01-01:00:00:00' })).toMatchObject({
where: [
{
field: 'published_at',
@ -325,9 +324,7 @@ describe('convertRestQueryParams', () => {
});
test('Not Contains', () => {
expect(
convertRestQueryParams({ sub_title_ncontains: 'text' })
).toMatchObject({
expect(convertRestQueryParams({ sub_title_ncontains: 'text' })).toMatchObject({
where: [
{
field: 'sub_title',
@ -339,9 +336,7 @@ describe('convertRestQueryParams', () => {
});
test('Not Contains sensitive', () => {
expect(
convertRestQueryParams({ content_text_ncontainss: 'test' })
).toMatchObject({
expect(convertRestQueryParams({ content_text_ncontainss: 'test' })).toMatchObject({
where: [
{
field: 'content_text',
@ -353,9 +348,7 @@ describe('convertRestQueryParams', () => {
});
test('Not Contains sensitive', () => {
expect(
convertRestQueryParams({ 'content.text_ncontainss': 'test' })
).toMatchObject({
expect(convertRestQueryParams({ 'content.text_ncontainss': 'test' })).toMatchObject({
where: [
{
field: 'content.text',
@ -367,9 +360,7 @@ describe('convertRestQueryParams', () => {
});
test('Null', () => {
expect(
convertRestQueryParams({ 'content.text_null': true })
).toMatchObject({
expect(convertRestQueryParams({ 'content.text_null': true })).toMatchObject({
where: [
{
field: 'content.text',
@ -381,9 +372,7 @@ describe('convertRestQueryParams', () => {
});
test('Not Null', () => {
expect(
convertRestQueryParams({ 'content.text_null': false })
).toMatchObject({
expect(convertRestQueryParams({ 'content.text_null': false })).toMatchObject({
where: [
{
field: 'content.text',

View File

@ -1,6 +1,21 @@
'use strict';
const yup = require('yup');
const _ = require('lodash');
const isNotNilTest = value => !_.isNil(value);
function isNotNill(msg = '${path} must be defined.') {
return this.test('defined', msg, isNotNilTest);
}
const isNotNullTest = value => !_.isNull(value);
function isNotNull(msg = '${path} cannot be null.') {
return this.test('defined', msg, isNotNullTest);
}
yup.addMethod(yup.mixed, 'notNil', isNotNill);
yup.addMethod(yup.mixed, 'notNull', isNotNull);
/**
* Returns a formatted error for http responses

View File

@ -28,6 +28,7 @@ const {
} = require('./services/webhook-store');
const { createCoreStore, coreStoreModel } = require('./services/core-store');
const createEntityService = require('./services/entity-service');
const createEntityValidator = require('./services/entity-validator');
const { createDatabaseManager } = require('strapi-database');
const CONFIG_PATHS = {
@ -362,9 +363,14 @@ class Strapi extends EventEmitter {
await this.startWebhooks();
this.entityValidator = createEntityValidator({
strapi: this,
});
this.entityService = createEntityService({
db: this.db,
eventHub: this.eventHub,
entityValidator: this.entityValidator,
});
// Initialize hooks and middlewares.

View File

@ -42,6 +42,22 @@ const boomMethods = [
'gatewayTimeout',
];
const formatBoomPayload = boomError => {
if (!Boom.isBoom(boomError)) {
boomError = Boom.boomify(boomError, {
statusCode: boomError.status || 500,
});
}
const { output } = boomError;
if (output.statusCode < 500 && !_.isNil(boomError.data)) {
output.payload.data = boomError.data;
}
return { status: output.statusCode, body: output.payload };
};
module.exports = strapi => {
return {
/**
@ -59,44 +75,26 @@ module.exports = strapi => {
await next();
} catch (error) {
// emit error if configured
if (
_.get(strapi, 'config.currentEnvironment.server.emitErrors', false)
) {
if (_.get(strapi, 'config.currentEnvironment.server.emitErrors', false)) {
strapi.app.emit('error', error, ctx);
}
// Log error.
strapi.log.error(error);
// if the error is a boom error (e.g throw strapi.errors.badRequest)
if (error.isBoom) {
ctx.status = error.output.statusCode;
ctx.body = error.output.payload;
} else {
// Wrap error into a Boom's response.
ctx.status = error.status || 500;
ctx.body = _.get(ctx.body, 'isBoom')
? ctx.body || (error && error.message)
: Boom.boomify(error, { statusCode: ctx.status });
}
const { status, body } = formatBoomPayload(error);
ctx.body = body;
ctx.status = status;
}
});
if (ctx.response.headers.location) {
return;
}
strapi.app.use(async (ctx, next) => {
await next();
// Empty body is considered as `notFound` response.
if (!ctx.body && ctx.body !== 0) {
ctx.notFound();
}
if (ctx.body.isBoom && ctx.body.data) {
ctx.body.output.payload.message = ctx.body.data;
}
// Format `ctx.body` and `ctx.status`.
ctx.status = ctx.body.isBoom ? ctx.body.output.statusCode : ctx.status;
ctx.body = ctx.body.isBoom ? ctx.body.output.payload : ctx.body;
});
},
@ -104,10 +102,15 @@ module.exports = strapi => {
createResponses() {
boomMethods.forEach(method => {
strapi.app.response[method] = function(...rest) {
const error = Boom[method](...rest) || {};
const boomError = Boom[method](...rest) || {};
this.status = error.isBoom ? error.output.statusCode : this.status;
this.body = error;
const { status, body } = formatBoomPayload(boomError);
// keep retro-compatibility for old error formats
body.message = body.data;
this.body = body;
this.status = status;
};
this.delegator.method(method);

View File

@ -0,0 +1,221 @@
'use strict';
const createEntityValidator = require('../entity-validator');
describe('Entity validator', () => {
describe('General Errors', () => {
it('Throws a badRequest error on invalid input', async () => {
const errors = {
badRequest: jest.fn(),
};
const entityValidator = createEntityValidator({
strapi: {
errors,
},
});
const model = {
attributes: {
title: {
type: 'string',
},
},
};
const input = { title: 1234 };
expect.hasAssertions();
await entityValidator.validateEntity(model, input).catch(() => {
expect(errors.badRequest).toHaveBeenCalledWith('ValidationError', expect.any(Object));
});
});
it('Returns data on valid input', async () => {
const errors = {
badRequest: jest.fn(),
};
const entityValidator = createEntityValidator({
strapi: {
errors,
},
});
const model = {
attributes: {
title: {
type: 'string',
},
},
};
const input = { title: 'test Title' };
expect.hasAssertions();
const data = await entityValidator.validateEntity(model, input);
expect(data).toEqual(input);
});
it('Returns casted data when possible', async () => {
const errors = {
badRequest: jest.fn(),
};
const entityValidator = createEntityValidator({
strapi: {
errors,
},
});
const model = {
attributes: {
title: {
type: 'string',
},
number: {
type: 'integer',
},
},
};
const input = { title: 'Test', number: '123' };
expect.hasAssertions();
const data = await entityValidator.validateEntity(model, input);
expect(data).toEqual({
title: 'Test',
number: 123,
});
});
test('Throws on required not respected', async () => {
const errors = {
badRequest: jest.fn(),
};
const entityValidator = createEntityValidator({
strapi: {
errors,
},
});
const model = {
attributes: {
title: {
type: 'string',
required: true,
},
},
};
expect.hasAssertions();
await entityValidator.validateEntity(model, {}).catch(() => {
expect(errors.badRequest).toHaveBeenCalledWith('ValidationError', {
title: [expect.stringMatching('must be defined')],
});
});
await entityValidator.validateEntity(model, { title: null }).catch(() => {
expect(errors.badRequest).toHaveBeenCalledWith('ValidationError', {
title: [expect.stringMatching('must be defined')],
});
});
});
});
describe('String validator', () => {
test('Throws on min length not respected', async () => {
const errors = {
badRequest: jest.fn(),
};
const entityValidator = createEntityValidator({
strapi: {
errors,
},
});
const model = {
attributes: {
title: {
type: 'string',
minLength: 10,
},
},
};
const input = { title: 'tooSmall' };
expect.hasAssertions();
await entityValidator.validateEntity(model, input).catch(() => {
expect(errors.badRequest).toHaveBeenCalledWith('ValidationError', {
title: [expect.stringMatching('at least 10 characters')],
});
});
});
test('Throws on max length not respected', async () => {
const errors = {
badRequest: jest.fn(),
};
const entityValidator = createEntityValidator({
strapi: {
errors,
},
});
const model = {
attributes: {
title: {
type: 'string',
maxLength: 2,
},
},
};
const input = { title: 'tooSmall' };
expect.hasAssertions();
await entityValidator.validateEntity(model, input).catch(() => {
expect(errors.badRequest).toHaveBeenCalledWith('ValidationError', {
title: [expect.stringMatching('at most 2 characters')],
});
});
});
test('Allows empty strings even when required', async () => {
const errors = {
badRequest: jest.fn(),
};
const entityValidator = createEntityValidator({
strapi: {
errors,
},
});
const model = {
attributes: {
title: {
type: 'string',
},
},
};
const input = { title: '' };
expect.hasAssertions();
const data = await entityValidator.validateEntity(model, input);
expect(data).toEqual(input);
});
});
});

View File

@ -3,7 +3,7 @@
const _ = require('lodash');
const uploadFiles = require('./utils/upload-files');
module.exports = ({ db, eventHub }) => ({
module.exports = ({ db, eventHub, entityValidator }) => ({
/**
* expose some utils so the end users can use them
*/
@ -62,7 +62,9 @@ module.exports = ({ db, eventHub }) => ({
}
}
let entry = await db.query(model).create(data);
const validData = await entityValidator.validateEntity(db.getModel(model), data);
let entry = await db.query(model).create(validData);
if (files) {
await this.uploadFiles(entry, files, { model });
@ -84,7 +86,9 @@ module.exports = ({ db, eventHub }) => ({
*/
async update({ params, data, files }, { model }) {
let entry = await db.query(model).update(params, data);
const validData = await entityValidator.validateEntityUpdate(db.getModel(model), data);
let entry = await db.query(model).update(params, validData);
if (files) {
await this.uploadFiles(entry, files, { model });

View File

@ -0,0 +1,94 @@
/**
* Entity validator
* Module that will validate input data for entity creation or edition
*/
'use strict';
const _ = require('lodash');
const { yup, formatYupErrors } = require('strapi-utils');
const validators = require('./validators');
module.exports = ({ strapi }) => ({
/**
* Validate some input based on a model schema
* @param {Object} model model schema
* @param {Object} data input data
*/
async validateEntity(model, data) {
const validator = createValidator(model);
return validator
.validate(data, {
abortEarly: false,
})
.catch(error => {
throw strapi.errors.badRequest('ValidationError', formatYupErrors(error));
});
},
/**
* Validate some input for updating based on a model schema
* @param {Object} model model schema
* @param {Object} data input data
*/
async validateEntityUpdate(model, data) {
const validator = createUpdateValidator(model);
return validator
.validate(data, {
abortEarly: false,
})
.catch(error => {
throw strapi.errors.badRequest('ValidationError', formatYupErrors(error));
});
},
});
const createValidator = model => {
return yup
.object(
_.mapValues(model.attributes, attr => {
const { required } = attr;
const validator = createAttributeValidator(attr).nullable();
if (required) {
return validator.notNil();
}
return validator;
})
)
.required();
};
const createUpdateValidator = model => {
return yup
.object(
_.mapValues(model.attributes, attr => {
const { required } = attr;
const validator = createAttributeValidator(attr);
if (required) {
// on edit you can omit a key to leave it unchanged, but if it is required you cannot set it to null
return validator.notNull();
}
return validator;
})
)
.required();
};
/**
* Validator for existing types
*/
const createAttributeValidator = attr => {
if (_.has(validators, attr.type)) {
return validators[attr.type](attr);
}
return yup.mixed();
};

View File

@ -0,0 +1,114 @@
'use strict';
const _ = require('lodash');
const { yup } = require('strapi-utils');
/**
* Utility function to compose validators
*/
const composeValidators = (...fns) => attr => {
return fns.reduce((validator, fn) => {
return fn(attr, validator);
}, yup.mixed());
};
/* Validator utils */
/**
* Adds minLength validator
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMinLengthValidator = ({ minLength }, validator) =>
_.isInteger(minLength) ? validator.min(minLength) : validator;
/**
* Adds maxLength validator
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMaxLengthValidator = ({ maxLength }, validator) =>
_.isInteger(maxLength) ? validator.max(maxLength) : validator;
/**
* Adds min integer validator
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMinIntegerValidator = ({ min }, validator) =>
_.isNumber(min) ? validator.min(_.toInteger(min)) : validator;
/**
* Adds max integer validator
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMaxIntegerValidator = ({ max }, validator) =>
_.isNumber(max) ? validator.max(_.toInteger(max)) : validator;
/**
* Adds min float/decimal validatore
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMinFloatValidator = ({ min }, validator) =>
_.isNumber(min) ? validator.min(min) : validator;
/**
* Adds max float/decimal validatore
* @param {Object} attribute model attribute
* @param {Object} validator yup validator
*/
const addMaxFloatValidator = ({ max }, validator) =>
_.isNumber(max) ? validator.max(max) : validator;
/* Type validators */
const stringValidator = composeValidators(
() => yup.string().strict(),
addMinLengthValidator,
addMaxLengthValidator
);
const emailValidator = composeValidators(stringValidator, (attr, validator) => validator.email());
const uidValidator = composeValidators(stringValidator, (attr, validator) =>
validator.matches(new RegExp('^[A-Za-z0-9-_.~]*$'))
);
const enumerationValidator = attr => {
return yup.string().oneOf((Array.isArray(attr.enum) ? attr.enum : [attr.enum]).concat(null));
};
const integerValidator = composeValidators(
() => yup.number().integer(),
addMinIntegerValidator,
addMaxIntegerValidator
);
const floatValidator = composeValidators(
() => yup.number(),
addMinFloatValidator,
addMaxFloatValidator
);
module.exports = {
string: stringValidator,
text: stringValidator,
richtext: stringValidator,
password: stringValidator,
email: emailValidator,
enumeration: enumerationValidator,
boolean: () => yup.boolean(),
uid: uidValidator,
json: () => yup.mixed(),
integer: integerValidator,
biginteger: () => yup.mixed(),
float: floatValidator,
decimal: floatValidator,
date: () => yup.mixed(),
time: () => yup.mixed(),
datetime: () => yup.mixed(),
timestamp: () => yup.mixed(),
};