mirror of
https://github.com/strapi/strapi.git
synced 2025-08-10 09:47:46 +00:00
Merge branch 'chore/fix-iso-locales' of https://github.com/strapi/strapi into chore/fix-iso-locales
This commit is contained in:
commit
2addba9fe3
@ -2,6 +2,7 @@ module.exports = {
|
||||
rootDir: __dirname,
|
||||
setupFilesAfterEnv: ['<rootDir>/test/unit.setup.js'],
|
||||
modulePathIgnorePatterns: ['.cache'],
|
||||
testPathIgnorePatterns: ['.testdata.js'],
|
||||
testMatch: ['/**/__tests__/**/*.[jt]s?(x)'],
|
||||
// Use `jest-watch-typeahead` version 0.6.5. Newest version 1.0.0 does not support jest@26
|
||||
// Reference: https://github.com/jest-community/jest-watch-typeahead/releases/tag/v1.0.0
|
||||
|
@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Bold = () => {
|
||||
return (
|
||||
<svg width="9" height="10" xmlns="http://www.w3.org/2000/svg">
|
||||
<text
|
||||
transform="translate(-12 -10)"
|
||||
fill="#333740"
|
||||
fillRule="evenodd"
|
||||
fontSize="13"
|
||||
fontFamily="Baskerville-SemiBold, Baskerville"
|
||||
fontWeight="500"
|
||||
>
|
||||
<tspan x="12" y="20">
|
||||
B
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Bold;
|
@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Code = () => {
|
||||
return (
|
||||
<svg width="12" height="8" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#333740" fillRule="evenodd">
|
||||
<path d="M3.653 7.385a.632.632 0 0 1-.452-.191L.214 4.154a.66.66 0 0 1 0-.922L3.201.19a.632.632 0 0 1 .905 0 .66.66 0 0 1 0 .921l-2.534 2.58 2.534 2.58a.66.66 0 0 1 0 .922.632.632 0 0 1-.453.19zM8.347 7.385a.632.632 0 0 0 .452-.191l2.987-3.04a.66.66 0 0 0 0-.922L8.799.19a.632.632 0 0 0-.905 0 .66.66 0 0 0 0 .921l2.534 2.58-2.534 2.58a.66.66 0 0 0 0 .922c.125.127.289.19.453.19z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Code;
|
@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Cross = ({ fill, height, width, ...rest }) => {
|
||||
return (
|
||||
<svg {...rest} width={width} height={height} xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.78 6.72L5.06 4l2.72-2.72a.748.748 0 0 0 0-1.06.748.748 0 0 0-1.06 0L4 2.94 1.28.22a.748.748 0 0 0-1.06 0 .748.748 0 0 0 0 1.06L2.94 4 .22 6.72a.748.748 0 0 0 0 1.06.748.748 0 0 0 1.06 0L4 5.06l2.72 2.72a.748.748 0 0 0 1.06 0 .752.752 0 0 0 0-1.06z"
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Cross.defaultProps = {
|
||||
fill: '#b3b5b9',
|
||||
height: '8',
|
||||
width: '8',
|
||||
};
|
||||
|
||||
Cross.propTypes = {
|
||||
fill: PropTypes.string,
|
||||
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
};
|
||||
|
||||
export default Cross;
|
@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Italic = () => {
|
||||
return (
|
||||
<svg width="6" height="9" xmlns="http://www.w3.org/2000/svg">
|
||||
<text
|
||||
transform="translate(-13 -11)"
|
||||
fill="#333740"
|
||||
fillRule="evenodd"
|
||||
fontWeight="500"
|
||||
fontSize="13"
|
||||
fontFamily="Baskerville-SemiBoldItalic, Baskerville"
|
||||
fontStyle="italic"
|
||||
>
|
||||
<tspan x="13" y="20">
|
||||
I
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Italic;
|
@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Link = () => {
|
||||
return (
|
||||
<svg width="12" height="6" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M6.063 1.5H6h.063z" fill="#000" />
|
||||
<path
|
||||
d="M9.516 0H8s.813.531.988 1.5h.528c.55 0 .984.434.984.984v1c0 .55-.434 1.016-.984 1.016h-3.5A1.03 1.03 0 0 1 5 3.484V2.5H3.5v.984A2.518 2.518 0 0 0 6.016 6h3.5C10.896 6 12 4.866 12 3.484v-1A2.473 2.473 0 0 0 9.516 0z"
|
||||
fill="#333740"
|
||||
/>
|
||||
<path
|
||||
d="M8.3 1.5A2.473 2.473 0 0 0 6.016 0h-3.5C1.134 0 0 1.103 0 2.484v1A2.526 2.526 0 0 0 2.516 6H4s-.806-.531-1.003-1.5h-.481A1.03 1.03 0 0 1 1.5 3.484v-1c0-.55.466-.984 1.016-.984h3.5c.55 0 .984.434.984.984V3.5h1.5V2.484c0-.35-.072-.684-.2-.984z"
|
||||
fill="#333740"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Link;
|
@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Media = () => {
|
||||
return (
|
||||
<svg width="12" height="11" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#333740" fillRule="evenodd">
|
||||
<path d="M9 4.286a1.286 1.286 0 1 0 0-2.572 1.286 1.286 0 0 0 0 2.572z" />
|
||||
<path d="M11.25 0H.75C.332 0 0 .34 0 .758v8.77c0 .418.332.758.75.758h10.5c.418 0 .75-.34.75-.758V.758A.752.752 0 0 0 11.25 0zM8.488 5.296a.46.46 0 0 0-.342-.167c-.137 0-.234.065-.343.153l-.501.423c-.105.075-.188.126-.308.126a.443.443 0 0 1-.295-.11 3.5 3.5 0 0 1-.115-.11L5.143 4.054a.59.59 0 0 0-.897.008L.857 8.148V1.171a.353.353 0 0 1 .351-.314h9.581a.34.34 0 0 1 .346.322l.008 6.975-2.655-2.858z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Media;
|
@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Na = ({ fill, fontFamily, fontSize, fontWeight, height, textFill, width, ...rest }) => {
|
||||
return (
|
||||
<svg {...rest} width={width} height={height} xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<rect fill={fill} width={width} height={height} rx="17.5" />
|
||||
<text fontFamily={fontFamily} fontSize={fontSize} fontWeight={fontWeight} fill={textFill}>
|
||||
<tspan x="6" y="22">
|
||||
N/A
|
||||
</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Na.defaultProps = {
|
||||
fill: '#fafafb',
|
||||
fontFamily: 'Lato-Medium, Lato',
|
||||
fontSize: '12',
|
||||
fontWeight: '400',
|
||||
height: '35',
|
||||
textFill: '#838383',
|
||||
width: '35',
|
||||
};
|
||||
|
||||
Na.propTypes = {
|
||||
fill: PropTypes.string,
|
||||
fontFamily: PropTypes.string,
|
||||
fontSize: PropTypes.string,
|
||||
fontWeight: PropTypes.string,
|
||||
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
textFill: PropTypes.string,
|
||||
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
};
|
||||
|
||||
export default Na;
|
@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Ol = () => {
|
||||
return (
|
||||
<svg width="12" height="8" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#333740" fillRule="evenodd">
|
||||
<path d="M2.4 3H.594v-.214h.137c.123 0 .212-.01.266-.032.053-.022.086-.052.1-.092a.67.67 0 0 0 .018-.188V.74a.46.46 0 0 0-.03-.194C1.064.504 1.021.476.955.46A1.437 1.437 0 0 0 .643.435H.539V.23c.332-.035.565-.067.7-.096.135-.03.258-.075.37-.134h.275v2.507c0 .104.023.177.07.218.047.04.14.061.278.061H2.4V3zM2.736 6.695l-.132.528h-.246a.261.261 0 0 0 .015-.074c0-.058-.049-.087-.146-.087H.293v-.198c.258-.173.511-.367.76-.581.25-.215.457-.437.623-.667.166-.23.249-.447.249-.653a.49.49 0 0 0-.321-.478.794.794 0 0 0-.582-.006.482.482 0 0 0-.196.138.284.284 0 0 0-.07.182c0 .074.04.17.12.289.006.008.009.015.009.02 0 .012-.041.03-.123.053l-.19.057a.693.693 0 0 1-.115.03c-.031 0-.067-.038-.108-.114a.516.516 0 0 1 .071-.586.899.899 0 0 1 .405-.238c.18-.058.4-.087.657-.087.317 0 .566.044.749.132.183.087.306.187.37.3a.64.64 0 0 1 .094.312c0 .197-.089.389-.266.575a5.296 5.296 0 0 1-.916.74 62.947 62.947 0 0 1-.62.413h1.843zM4 0h8v1H4zM4 2h8v1H4zM4 4h8v1H4zM4 6h8v1H4z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Ol;
|
@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Quote = () => {
|
||||
return (
|
||||
<svg width="9" height="9" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#333740" fillRule="evenodd">
|
||||
<path d="M3 0C2.047 0 1.301.263.782.782.263 1.302 0 2.047 0 3v6h3.75V3H1.5c0-.54.115-.93.343-1.157C2.07 1.615 2.46 1.5 3 1.5M8.25 0c-.953 0-1.699.263-2.218.782-.519.52-.782 1.265-.782 2.218v6H9V3H6.75c0-.54.115-.93.343-1.157.227-.228.617-.343 1.157-.343" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Quote;
|
@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Striked = () => {
|
||||
return (
|
||||
<svg width="19" height="10" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<text
|
||||
fontFamily="Lato-Semibold, Lato"
|
||||
fontSize="11"
|
||||
fontWeight="500"
|
||||
fill="#41464E"
|
||||
transform="translate(0 -2)"
|
||||
>
|
||||
<tspan x="1" y="11">
|
||||
abc
|
||||
</tspan>
|
||||
</text>
|
||||
<path d="M.5 6.5h18" stroke="#2C3039" strokeLinecap="square" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Striked;
|
@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Ul = () => {
|
||||
return (
|
||||
<svg width="13" height="7" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path fill="#333740" d="M5 0h8v1H5zM5 2h8v1H5zM5 4h8v1H5zM5 6h8v1H5z" />
|
||||
<rect stroke="#333740" x=".5" y=".5" width="2" height="2" rx="1" />
|
||||
<rect stroke="#333740" x=".5" y="4.5" width="2" height="2" rx="1" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Ul;
|
@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Underline = () => {
|
||||
return (
|
||||
<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg">
|
||||
<text
|
||||
transform="translate(-10 -11)"
|
||||
fill="#101622"
|
||||
fillRule="evenodd"
|
||||
fontSize="13"
|
||||
fontFamily="Baskerville-SemiBold, Baskerville"
|
||||
fontWeight="500"
|
||||
>
|
||||
<tspan x="10" y="20">
|
||||
U
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Underline;
|
@ -32,7 +32,7 @@ const LogoContainer = styled(Box)`
|
||||
`;
|
||||
|
||||
const HomePage = () => {
|
||||
// // Temporary until we develop the menu API
|
||||
// Temporary until we develop the menu API
|
||||
const { collectionTypes, singleTypes, isLoading: isLoadingForModels } = useModels();
|
||||
const { guidedTourState, isGuidedTourVisible, isSkipped } = useGuidedTour();
|
||||
|
||||
|
@ -271,7 +271,7 @@ describe('CM API - Basic + dz + draftAndPublish', () => {
|
||||
error: {
|
||||
status: 400,
|
||||
name: 'ValidationError',
|
||||
message: 'dz[0].__component is a required field',
|
||||
message: '2 errors occurred',
|
||||
details: {
|
||||
errors: [
|
||||
{
|
||||
@ -279,6 +279,11 @@ describe('CM API - Basic + dz + draftAndPublish', () => {
|
||||
message: 'dz[0].__component is a required field',
|
||||
name: 'ValidationError',
|
||||
},
|
||||
{
|
||||
message: "Cannot read properties of undefined (reading 'attributes')",
|
||||
name: 'ValidationError',
|
||||
path: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -301,7 +301,7 @@ describe('CM API - Basic + dz', () => {
|
||||
error: {
|
||||
status: 400,
|
||||
name: 'ValidationError',
|
||||
message: 'dz[0].__component is a required field',
|
||||
message: '2 errors occurred',
|
||||
details: {
|
||||
errors: [
|
||||
{
|
||||
@ -309,6 +309,11 @@ describe('CM API - Basic + dz', () => {
|
||||
message: 'dz[0].__component is a required field',
|
||||
name: 'ValidationError',
|
||||
},
|
||||
{
|
||||
message: "Cannot read properties of undefined (reading 'attributes')",
|
||||
name: 'ValidationError',
|
||||
path: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -4,28 +4,22 @@ const createEntityService = require('..');
|
||||
const entityValidator = require('../../entity-validator');
|
||||
|
||||
describe('Entity service triggers webhooks', () => {
|
||||
global.strapi = {
|
||||
getModel: () => ({}),
|
||||
config: {
|
||||
get: () => [],
|
||||
},
|
||||
};
|
||||
|
||||
let instance;
|
||||
const eventHub = { emit: jest.fn() };
|
||||
let entity = { attr: 'value' };
|
||||
|
||||
beforeAll(() => {
|
||||
const model = {
|
||||
kind: 'singleType',
|
||||
modelName: 'test-model',
|
||||
privateAttributes: [],
|
||||
attributes: {
|
||||
attr: { type: 'string' },
|
||||
},
|
||||
};
|
||||
instance = createEntityService({
|
||||
strapi: {
|
||||
getModel: () => ({
|
||||
kind: 'singleType',
|
||||
modelName: 'test-model',
|
||||
privateAttributes: [],
|
||||
attributes: {
|
||||
attr: { type: 'string' },
|
||||
},
|
||||
}),
|
||||
getModel: () => model,
|
||||
},
|
||||
db: {
|
||||
query: () => ({
|
||||
@ -41,6 +35,13 @@ describe('Entity service triggers webhooks', () => {
|
||||
eventHub,
|
||||
entityValidator,
|
||||
});
|
||||
|
||||
global.strapi = {
|
||||
getModel: () => model,
|
||||
config: {
|
||||
get: () => [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test('Emit event: Create', async () => {
|
||||
|
@ -3,6 +3,7 @@
|
||||
jest.mock('bcryptjs', () => ({ hashSync: () => 'secret-password' }));
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const { ValidationError } = require('@strapi/utils').errors;
|
||||
const createEntityService = require('..');
|
||||
const entityValidator = require('../../entity-validator');
|
||||
|
||||
@ -81,50 +82,106 @@ describe('Entity service', () => {
|
||||
describe('Create', () => {
|
||||
describe('assign default values', () => {
|
||||
let instance;
|
||||
const entityUID = 'api::entity.entity';
|
||||
const relationUID = 'api::relation.relation';
|
||||
|
||||
beforeAll(() => {
|
||||
const fakeQuery = {
|
||||
count: jest.fn(() => 0),
|
||||
create: jest.fn(({ data }) => data),
|
||||
};
|
||||
|
||||
const fakeModel = {
|
||||
kind: 'contentType',
|
||||
modelName: 'test-model',
|
||||
privateAttributes: [],
|
||||
options: {},
|
||||
attributes: {
|
||||
attrStringDefaultRequired: { type: 'string', default: 'default value', required: true },
|
||||
attrStringDefault: { type: 'string', default: 'default value' },
|
||||
attrBoolDefaultRequired: { type: 'boolean', default: true, required: true },
|
||||
attrBoolDefault: { type: 'boolean', default: true },
|
||||
attrIntDefaultRequired: { type: 'integer', default: 1, required: true },
|
||||
attrIntDefault: { type: 'integer', default: 1 },
|
||||
attrEnumDefaultRequired: {
|
||||
type: 'enumeration',
|
||||
enum: ['a', 'b', 'c'],
|
||||
default: 'a',
|
||||
required: true,
|
||||
const fakeEntities = {
|
||||
[relationUID]: {
|
||||
1: {
|
||||
id: 1,
|
||||
Name: 'TestRelation',
|
||||
createdAt: '2022-09-28T15:11:22.995Z',
|
||||
updatedAt: '2022-09-29T09:01:02.949Z',
|
||||
publishedAt: null,
|
||||
},
|
||||
attrEnumDefault: {
|
||||
type: 'enumeration',
|
||||
enum: ['a', 'b', 'c'],
|
||||
default: 'b',
|
||||
2: {
|
||||
id: 2,
|
||||
Name: 'TestRelation2',
|
||||
createdAt: '2022-09-28T15:11:22.995Z',
|
||||
updatedAt: '2022-09-29T09:01:02.949Z',
|
||||
publishedAt: null,
|
||||
},
|
||||
attrPassword: { type: 'password' },
|
||||
},
|
||||
};
|
||||
|
||||
const fakeModels = {
|
||||
[entityUID]: {
|
||||
uid: entityUID,
|
||||
kind: 'contentType',
|
||||
modelName: 'test-model',
|
||||
privateAttributes: [],
|
||||
options: {},
|
||||
attributes: {
|
||||
attrStringDefaultRequired: {
|
||||
type: 'string',
|
||||
default: 'default value',
|
||||
required: true,
|
||||
},
|
||||
attrStringDefault: { type: 'string', default: 'default value' },
|
||||
attrBoolDefaultRequired: { type: 'boolean', default: true, required: true },
|
||||
attrBoolDefault: { type: 'boolean', default: true },
|
||||
attrIntDefaultRequired: { type: 'integer', default: 1, required: true },
|
||||
attrIntDefault: { type: 'integer', default: 1 },
|
||||
attrEnumDefaultRequired: {
|
||||
type: 'enumeration',
|
||||
enum: ['a', 'b', 'c'],
|
||||
default: 'a',
|
||||
required: true,
|
||||
},
|
||||
attrEnumDefault: {
|
||||
type: 'enumeration',
|
||||
enum: ['a', 'b', 'c'],
|
||||
default: 'b',
|
||||
},
|
||||
attrPassword: { type: 'password' },
|
||||
attrRelation: {
|
||||
type: 'relation',
|
||||
relation: 'oneToMany',
|
||||
target: relationUID,
|
||||
mappedBy: 'entity',
|
||||
},
|
||||
},
|
||||
},
|
||||
[relationUID]: {
|
||||
uid: relationUID,
|
||||
kind: 'contentType',
|
||||
modelName: 'relation',
|
||||
attributes: {
|
||||
Name: {
|
||||
type: 'string',
|
||||
default: 'default value',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const fakeQuery = (uid) => ({
|
||||
create: jest.fn(({ data }) => data),
|
||||
count: jest.fn(({ where }) => {
|
||||
let ret = 0;
|
||||
where.id.$in.forEach((id) => {
|
||||
const entity = fakeEntities[uid][id];
|
||||
if (!entity) return;
|
||||
ret += 1;
|
||||
});
|
||||
return ret;
|
||||
}),
|
||||
});
|
||||
|
||||
const fakeDB = {
|
||||
query: jest.fn(() => fakeQuery),
|
||||
query: jest.fn((uid) => fakeQuery(uid)),
|
||||
};
|
||||
|
||||
const fakeStrapi = {
|
||||
getModel: jest.fn(() => fakeModel),
|
||||
global.strapi = {
|
||||
getModel: jest.fn((uid) => {
|
||||
return fakeModels[uid];
|
||||
}),
|
||||
db: fakeDB,
|
||||
};
|
||||
|
||||
instance = createEntityService({
|
||||
strapi: fakeStrapi,
|
||||
strapi: global.strapi,
|
||||
db: fakeDB,
|
||||
eventHub: new EventEmitter(),
|
||||
entityValidator,
|
||||
@ -134,7 +191,7 @@ describe('Entity service', () => {
|
||||
test('should create record with all default attributes', async () => {
|
||||
const data = {};
|
||||
|
||||
await expect(instance.create('test-model', { data })).resolves.toMatchObject({
|
||||
await expect(instance.create(entityUID, { data })).resolves.toMatchObject({
|
||||
attrStringDefaultRequired: 'default value',
|
||||
attrStringDefault: 'default value',
|
||||
attrBoolDefaultRequired: true,
|
||||
@ -154,7 +211,7 @@ describe('Entity service', () => {
|
||||
attrEnumDefault: 'c',
|
||||
};
|
||||
|
||||
await expect(instance.create('test-model', { data })).resolves.toMatchObject({
|
||||
await expect(instance.create(entityUID, { data })).resolves.toMatchObject({
|
||||
attrStringDefault: 'my value',
|
||||
attrBoolDefault: false,
|
||||
attrIntDefault: 2,
|
||||
@ -179,11 +236,225 @@ describe('Entity service', () => {
|
||||
attrPassword: 'fooBar',
|
||||
};
|
||||
|
||||
await expect(instance.create('test-model', { data })).resolves.toMatchObject({
|
||||
await expect(instance.create(entityUID, { data })).resolves.toMatchObject({
|
||||
...data,
|
||||
attrPassword: 'secret-password',
|
||||
});
|
||||
});
|
||||
|
||||
test('should create record with valid relation', async () => {
|
||||
const data = {
|
||||
attrStringDefaultRequired: 'my value',
|
||||
attrStringDefault: 'my value',
|
||||
attrBoolDefaultRequired: true,
|
||||
attrBoolDefault: true,
|
||||
attrIntDefaultRequired: 10,
|
||||
attrIntDefault: 10,
|
||||
attrEnumDefaultRequired: 'c',
|
||||
attrEnumDefault: 'a',
|
||||
attrPassword: 'fooBar',
|
||||
attrRelation: {
|
||||
connect: [
|
||||
{
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const res = instance.create(entityUID, { data });
|
||||
|
||||
await expect(res).resolves.toMatchObject({
|
||||
...data,
|
||||
attrPassword: 'secret-password',
|
||||
});
|
||||
});
|
||||
|
||||
test('should fail to create a record with an invalid relation', async () => {
|
||||
const data = {
|
||||
attrStringDefaultRequired: 'my value',
|
||||
attrStringDefault: 'my value',
|
||||
attrBoolDefaultRequired: true,
|
||||
attrBoolDefault: true,
|
||||
attrIntDefaultRequired: 10,
|
||||
attrIntDefault: 10,
|
||||
attrEnumDefaultRequired: 'c',
|
||||
attrEnumDefault: 'a',
|
||||
attrPassword: 'fooBar',
|
||||
attrRelation: {
|
||||
connect: [
|
||||
{
|
||||
id: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const res = instance.create(entityUID, { data });
|
||||
await expect(res).rejects.toThrowError(
|
||||
new ValidationError(
|
||||
`1 relation(s) of type api::relation.relation associated with this entity do not exist`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update', () => {
|
||||
describe('assign default values', () => {
|
||||
let instance;
|
||||
|
||||
const entityUID = 'api::entity.entity';
|
||||
const relationUID = 'api::relation.relation';
|
||||
|
||||
const fakeEntities = {
|
||||
[entityUID]: {
|
||||
0: {
|
||||
id: 0,
|
||||
Name: 'TestEntity',
|
||||
createdAt: '2022-09-28T15:11:22.995Z',
|
||||
updatedAt: '2022-09-29T09:01:02.949Z',
|
||||
publishedAt: null,
|
||||
},
|
||||
},
|
||||
[relationUID]: {
|
||||
1: {
|
||||
id: 1,
|
||||
Name: 'TestRelation',
|
||||
createdAt: '2022-09-28T15:11:22.995Z',
|
||||
updatedAt: '2022-09-29T09:01:02.949Z',
|
||||
publishedAt: null,
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
Name: 'TestRelation2',
|
||||
createdAt: '2022-09-28T15:11:22.995Z',
|
||||
updatedAt: '2022-09-29T09:01:02.949Z',
|
||||
publishedAt: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
const fakeModels = {
|
||||
[entityUID]: {
|
||||
kind: 'collectionType',
|
||||
modelName: 'entity',
|
||||
collectionName: 'entity',
|
||||
uid: entityUID,
|
||||
privateAttributes: [],
|
||||
options: {},
|
||||
info: {
|
||||
singularName: 'entity',
|
||||
pluralName: 'entities',
|
||||
displayName: 'ENTITY',
|
||||
},
|
||||
attributes: {
|
||||
Name: {
|
||||
type: 'string',
|
||||
},
|
||||
addresses: {
|
||||
type: 'relation',
|
||||
relation: 'oneToMany',
|
||||
target: relationUID,
|
||||
mappedBy: 'entity',
|
||||
},
|
||||
},
|
||||
},
|
||||
[relationUID]: {
|
||||
kind: 'contentType',
|
||||
modelName: 'relation',
|
||||
attributes: {
|
||||
Name: {
|
||||
type: 'string',
|
||||
default: 'default value',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const fakeQuery = (key) => ({
|
||||
findOne: jest.fn(({ where }) => fakeEntities[key][where.id]),
|
||||
count: jest.fn(({ where }) => {
|
||||
let ret = 0;
|
||||
where.id.$in.forEach((id) => {
|
||||
const entity = fakeEntities[key][id];
|
||||
if (!entity) return;
|
||||
ret += 1;
|
||||
});
|
||||
return ret;
|
||||
}),
|
||||
update: jest.fn(({ where }) => ({
|
||||
...fakeEntities[key][where.id],
|
||||
addresses: {
|
||||
count: 1,
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
const fakeDB = {
|
||||
query: jest.fn((key) => fakeQuery(key)),
|
||||
};
|
||||
|
||||
global.strapi = {
|
||||
getModel: jest.fn((uid) => {
|
||||
return fakeModels[uid];
|
||||
}),
|
||||
db: fakeDB,
|
||||
};
|
||||
|
||||
instance = createEntityService({
|
||||
strapi: global.strapi,
|
||||
db: fakeDB,
|
||||
eventHub: new EventEmitter(),
|
||||
entityValidator,
|
||||
});
|
||||
});
|
||||
|
||||
test(`should fail if the entity doesn't exist`, async () => {
|
||||
expect(
|
||||
await instance.update(entityUID, Math.random() * (10000 - 100) + 100, {})
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('should successfully update an existing relation', async () => {
|
||||
const data = {
|
||||
Name: 'TestEntry',
|
||||
addresses: {
|
||||
connect: [
|
||||
{
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(await instance.update(entityUID, 0, { data })).toMatchObject({
|
||||
...fakeEntities[entityUID][0],
|
||||
addresses: {
|
||||
count: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should throw an error when trying to associate a relation that does not exist', async () => {
|
||||
const data = {
|
||||
Name: 'TestEntry',
|
||||
addresses: {
|
||||
connect: [
|
||||
{
|
||||
id: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const res = instance.update(entityUID, 0, { data });
|
||||
await expect(res).rejects.toThrowError(
|
||||
new ValidationError(
|
||||
`1 relation(s) of type api::relation.relation associated with this entity do not exist`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,18 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const entityValidator = require('../entity-validator');
|
||||
const entityValidator = require('..');
|
||||
|
||||
describe('Entity validator', () => {
|
||||
describe('Published input', () => {
|
||||
describe('General Errors', () => {
|
||||
it('Throws a badRequest error on invalid input', async () => {
|
||||
global.strapi = {
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
};
|
||||
let model;
|
||||
global.strapi = {
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
getModel: () => model,
|
||||
};
|
||||
|
||||
const model = {
|
||||
it('Throws a badRequest error on invalid input', async () => {
|
||||
model = {
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'string',
|
||||
@ -44,7 +46,7 @@ describe('Entity validator', () => {
|
||||
});
|
||||
|
||||
it('Returns data on valid input', async () => {
|
||||
const model = {
|
||||
model = {
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'string',
|
||||
@ -61,7 +63,7 @@ describe('Entity validator', () => {
|
||||
});
|
||||
|
||||
it('Returns casted data when possible', async () => {
|
||||
const model = {
|
||||
model = {
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'string',
|
||||
@ -84,13 +86,7 @@ describe('Entity validator', () => {
|
||||
});
|
||||
|
||||
test('Throws on required not respected', async () => {
|
||||
global.strapi = {
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const model = {
|
||||
model = {
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'string',
|
||||
@ -139,7 +135,7 @@ describe('Entity validator', () => {
|
||||
});
|
||||
|
||||
it('Supports custom field types', async () => {
|
||||
const model = {
|
||||
model = {
|
||||
attributes: {
|
||||
uuid: {
|
||||
type: 'uuid',
|
||||
@ -164,6 +160,7 @@ describe('Entity validator', () => {
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
getModel: () => model,
|
||||
};
|
||||
|
||||
const model = {
|
||||
@ -199,12 +196,6 @@ describe('Entity validator', () => {
|
||||
});
|
||||
|
||||
test('Throws on max length not respected', async () => {
|
||||
global.strapi = {
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const model = {
|
||||
attributes: {
|
||||
title: {
|
||||
@ -329,9 +320,11 @@ describe('Entity validator', () => {
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
getModel: () => model,
|
||||
};
|
||||
|
||||
const model = {
|
||||
uid: 'api::test.test',
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'string',
|
||||
@ -456,6 +449,13 @@ describe('Entity validator', () => {
|
||||
},
|
||||
};
|
||||
|
||||
global.strapi = {
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
getModel: () => model,
|
||||
};
|
||||
|
||||
const input = { title: 'tooSmall' };
|
||||
|
||||
expect.hasAssertions();
|
||||
@ -465,12 +465,6 @@ describe('Entity validator', () => {
|
||||
});
|
||||
|
||||
test('Throws on max length not respected', async () => {
|
||||
global.strapi = {
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const model = {
|
||||
attributes: {
|
||||
title: {
|
@ -0,0 +1,123 @@
|
||||
'use strict';
|
||||
|
||||
const { ValidationError } = require('@strapi/utils').errors;
|
||||
|
||||
const entityValidator = require('../..');
|
||||
const { models, existentIDs, nonExistentIds } = require('./utils/relations.testdata');
|
||||
|
||||
/**
|
||||
* Test that relations can be successfully validated and non existent relations
|
||||
* can be detected at the Attribute level.
|
||||
*/
|
||||
describe('Entity validator | Relations | Attribute', () => {
|
||||
const strapi = {
|
||||
components: {
|
||||
'basic.dev-compo': {},
|
||||
},
|
||||
db: {
|
||||
query() {
|
||||
return {
|
||||
count: ({
|
||||
where: {
|
||||
id: { $in },
|
||||
},
|
||||
}) => existentIDs.filter((value) => $in.includes(value)).length,
|
||||
};
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
getModel: (uid) => models.get(uid),
|
||||
};
|
||||
|
||||
describe('Success', () => {
|
||||
const testData = [
|
||||
[
|
||||
'Connect',
|
||||
{
|
||||
categories: {
|
||||
disconnect: [],
|
||||
connect: [
|
||||
{
|
||||
id: existentIDs[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'Set',
|
||||
{
|
||||
categories: {
|
||||
set: [
|
||||
{
|
||||
id: existentIDs[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'Number',
|
||||
{
|
||||
categories: existentIDs[0],
|
||||
},
|
||||
],
|
||||
[
|
||||
'Array',
|
||||
{
|
||||
categories: existentIDs.slice(-Math.floor(existentIDs.length / 2)),
|
||||
},
|
||||
],
|
||||
];
|
||||
test.each(testData)('%s', async (__, input = {}) => {
|
||||
global.strapi = strapi;
|
||||
const res = entityValidator.validateEntityCreation(models.get('api::dev.dev'), input, {
|
||||
isDraft: true,
|
||||
});
|
||||
await expect(res).resolves.not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error', () => {
|
||||
const expectError = new ValidationError(
|
||||
`2 relation(s) of type api::category.category associated with this entity do not exist`
|
||||
);
|
||||
const testData = [
|
||||
[
|
||||
'Connect',
|
||||
{
|
||||
categories: {
|
||||
disconnect: [],
|
||||
connect: [existentIDs[0], ...nonExistentIds.slice(-2)].map((id) => ({
|
||||
id,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'Set',
|
||||
{
|
||||
categories: {
|
||||
set: [existentIDs[0], ...nonExistentIds.slice(-2)].map((id) => ({ id })),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'Number',
|
||||
{
|
||||
categories: nonExistentIds.slice(-2),
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
test.each(testData)('%s', async (__, input = {}) => {
|
||||
global.strapi = strapi;
|
||||
const res = entityValidator.validateEntityCreation(models.get('api::dev.dev'), input, {
|
||||
isDraft: true,
|
||||
});
|
||||
await expect(res).rejects.toThrowError(expectError);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,275 @@
|
||||
'use strict';
|
||||
|
||||
const { ValidationError } = require('@strapi/utils').errors;
|
||||
|
||||
const entityValidator = require('../..');
|
||||
const { models, nonExistentIds, existentIDs } = require('./utils/relations.testdata');
|
||||
|
||||
/**
|
||||
* Test that relations can be successfully validated and non existent relations
|
||||
* can be detected at the Component level.
|
||||
*/
|
||||
describe('Entity validator | Relations | Component Level', () => {
|
||||
const strapi = {
|
||||
components: {
|
||||
'basic.dev-compo': {},
|
||||
},
|
||||
db: {
|
||||
query() {
|
||||
return {
|
||||
count: ({
|
||||
where: {
|
||||
id: { $in },
|
||||
},
|
||||
}) => existentIDs.filter((value) => $in.includes(value)).length,
|
||||
};
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
getModel: (uid) => models.get(uid),
|
||||
};
|
||||
|
||||
describe('Single Component', () => {
|
||||
describe('Success', () => {
|
||||
const testData = [
|
||||
[
|
||||
'Connect',
|
||||
{
|
||||
sCom: {
|
||||
categories: {
|
||||
disconnect: [],
|
||||
connect: [
|
||||
{
|
||||
id: existentIDs[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'Set',
|
||||
{
|
||||
sCom: {
|
||||
categories: {
|
||||
set: [
|
||||
{
|
||||
id: existentIDs[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'Number',
|
||||
{
|
||||
sCom: {
|
||||
categories: existentIDs[0],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'Array',
|
||||
{
|
||||
sCom: {
|
||||
categories: existentIDs.slice(-3),
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
test.each(testData)('%s', async (__, input = {}) => {
|
||||
global.strapi = strapi;
|
||||
const res = entityValidator.validateEntityCreation(models.get('api::dev.dev'), input, {
|
||||
isDraft: true,
|
||||
});
|
||||
await expect(res).resolves.not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error', () => {
|
||||
const expectedError = new ValidationError(
|
||||
`1 relation(s) of type api::category.category associated with this entity do not exist`
|
||||
);
|
||||
const testData = [
|
||||
[
|
||||
'Connect',
|
||||
{
|
||||
sCom: {
|
||||
categories: {
|
||||
disconnect: [],
|
||||
connect: [
|
||||
{
|
||||
id: nonExistentIds[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'Set',
|
||||
{
|
||||
sCom: {
|
||||
categories: {
|
||||
set: [
|
||||
{
|
||||
id: nonExistentIds[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'Number',
|
||||
{
|
||||
sCom: {
|
||||
categories: nonExistentIds[0],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'Array',
|
||||
{
|
||||
sCom: {
|
||||
categories: [nonExistentIds[0]],
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
test.each(testData)('%s', async (__, input = {}) => {
|
||||
global.strapi = strapi;
|
||||
const res = entityValidator.validateEntityCreation(models.get('api::dev.dev'), input, {
|
||||
isDraft: true,
|
||||
});
|
||||
await expect(res).rejects.toThrowError(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Repeatable Component', () => {
|
||||
describe('Success', () => {
|
||||
const testData = [
|
||||
[
|
||||
'Connect',
|
||||
{
|
||||
rCom: [
|
||||
{
|
||||
categories: {
|
||||
disconnect: [],
|
||||
connect: [
|
||||
{
|
||||
id: existentIDs[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'Set',
|
||||
{
|
||||
rCom: [
|
||||
{
|
||||
categories: {
|
||||
set: existentIDs.slice(-Math.floor(existentIDs.length / 2)).map((id) => ({
|
||||
id,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'Number',
|
||||
{
|
||||
rCom: [
|
||||
{
|
||||
categories: existentIDs[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'Array',
|
||||
{
|
||||
rCom: [
|
||||
{
|
||||
categories: existentIDs.slice(-Math.floor(existentIDs.length / 2)),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
test.each(testData)('%s', async (__, input = {}) => {
|
||||
global.strapi = strapi;
|
||||
const res = entityValidator.validateEntityCreation(models.get('api::dev.dev'), input, {
|
||||
isDraft: true,
|
||||
});
|
||||
await expect(res).resolves.not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error', () => {
|
||||
const expectedError = new ValidationError(
|
||||
`4 relation(s) of type api::category.category associated with this entity do not exist`
|
||||
);
|
||||
const testData = [
|
||||
[
|
||||
'Connect',
|
||||
{
|
||||
rCom: [
|
||||
{
|
||||
categories: {
|
||||
disconnect: [],
|
||||
connect: [existentIDs[0], ...nonExistentIds.slice(-4)].map((id) => ({
|
||||
id,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'Set',
|
||||
{
|
||||
rCom: [
|
||||
{
|
||||
categories: {
|
||||
set: [existentIDs[0], ...nonExistentIds.slice(-4)].map((id) => ({
|
||||
id,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'Array',
|
||||
{
|
||||
rCom: [
|
||||
{
|
||||
categories: nonExistentIds.slice(-4),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
test.each(testData)('%s', async (__, input = {}) => {
|
||||
global.strapi = strapi;
|
||||
const res = entityValidator.validateEntityCreation(models.get('api::dev.dev'), input, {
|
||||
isDraft: true,
|
||||
});
|
||||
await expect(res).rejects.toThrowError(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,159 @@
|
||||
'use strict';
|
||||
|
||||
const { ValidationError } = require('@strapi/utils').errors;
|
||||
|
||||
const entityValidator = require('../..');
|
||||
const { models, nonExistentIds, existentIDs } = require('./utils/relations.testdata');
|
||||
|
||||
/**
|
||||
* Test that relations can be successfully validated and non existent relations
|
||||
* can be detected at the Dynamic Zone level.
|
||||
*/
|
||||
describe('Entity validator | Relations | Dynamic Zone', () => {
|
||||
const strapi = {
|
||||
components: {
|
||||
'basic.dev-compo': {},
|
||||
},
|
||||
db: {
|
||||
query() {
|
||||
return {
|
||||
count: ({
|
||||
where: {
|
||||
id: { $in },
|
||||
},
|
||||
}) => existentIDs.filter((value) => $in.includes(value)).length,
|
||||
};
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
getModel: (uid) => models.get(uid),
|
||||
};
|
||||
|
||||
describe('Success', () => {
|
||||
const testData = [
|
||||
[
|
||||
'Connect',
|
||||
{
|
||||
DZ: [
|
||||
{
|
||||
__component: 'basic.dev-compo',
|
||||
categories: {
|
||||
disconnect: [],
|
||||
connect: existentIDs.slice(-3).map((id) => ({
|
||||
id,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'Set',
|
||||
{
|
||||
DZ: [
|
||||
{
|
||||
__component: 'basic.dev-compo',
|
||||
categories: {
|
||||
set: existentIDs.slice(-3).map((id) => ({
|
||||
id,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'Number',
|
||||
{
|
||||
DZ: [
|
||||
{
|
||||
__component: 'basic.dev-compo',
|
||||
categories: existentIDs[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'Array',
|
||||
{
|
||||
DZ: [
|
||||
{
|
||||
__component: 'basic.dev-compo',
|
||||
categories: existentIDs.slice(-3),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
test.each(testData)('%s', async (__, input = {}) => {
|
||||
global.strapi = strapi;
|
||||
const res = entityValidator.validateEntityCreation(models.get('api::dev.dev'), input, {
|
||||
isDraft: true,
|
||||
});
|
||||
await expect(res).resolves.not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error', () => {
|
||||
const expectedError = new ValidationError(
|
||||
`2 relation(s) of type api::category.category associated with this entity do not exist`
|
||||
);
|
||||
const testData = [
|
||||
[
|
||||
'Connect',
|
||||
{
|
||||
DZ: [
|
||||
{
|
||||
__component: 'basic.dev-compo',
|
||||
categories: {
|
||||
disconnect: [],
|
||||
connect: [existentIDs[0], ...nonExistentIds.slice(-2)].map((id) => ({
|
||||
id,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'Set',
|
||||
{
|
||||
DZ: [
|
||||
{
|
||||
__component: 'basic.dev-compo',
|
||||
categories: {
|
||||
set: [existentIDs[0], ...nonExistentIds.slice(-2)].map((id) => ({
|
||||
id,
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'Array',
|
||||
{
|
||||
DZ: [
|
||||
{
|
||||
__component: 'basic.dev-compo',
|
||||
categories: [existentIDs[0], ...nonExistentIds.slice(-2)].map((id) => ({
|
||||
id,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
test.each(testData)('%s', async (__, input = {}) => {
|
||||
global.strapi = strapi;
|
||||
const res = entityValidator.validateEntityCreation(models.get('api::dev.dev'), input, {
|
||||
isDraft: true,
|
||||
});
|
||||
await expect(res).rejects.toThrowError(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,74 @@
|
||||
'use strict';
|
||||
|
||||
const { ValidationError } = require('@strapi/utils').errors;
|
||||
|
||||
const entityValidator = require('../..');
|
||||
const { models, existentIDs, nonExistentIds } = require('./utils/relations.testdata');
|
||||
|
||||
/**
|
||||
* Test that relations can be successfully validated and non existent relations
|
||||
* can be detected at the Media level.
|
||||
*/
|
||||
describe('Entity validator | Relations | Media', () => {
|
||||
const strapi = {
|
||||
components: {
|
||||
'basic.dev-compo': {},
|
||||
},
|
||||
db: {
|
||||
query() {
|
||||
return {
|
||||
count: ({
|
||||
where: {
|
||||
id: { $in },
|
||||
},
|
||||
}) => existentIDs.filter((value) => $in.includes(value)).length,
|
||||
};
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
badRequest: jest.fn(),
|
||||
},
|
||||
getModel: (uid) => models.get(uid),
|
||||
};
|
||||
|
||||
it('Success', async () => {
|
||||
global.strapi = strapi;
|
||||
const input = {
|
||||
media: [
|
||||
{
|
||||
id: existentIDs[0],
|
||||
name: 'img.jpeg',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res = entityValidator.validateEntityCreation(models.get('api::dev.dev'), input, {
|
||||
isDraft: true,
|
||||
});
|
||||
await expect(res).resolves.not.toThrowError();
|
||||
});
|
||||
|
||||
it('Error', async () => {
|
||||
global.strapi = strapi;
|
||||
const expectedError = new ValidationError(
|
||||
`1 relation(s) of type plugin::upload.file associated with this entity do not exist`
|
||||
);
|
||||
const input = {
|
||||
media: [
|
||||
{
|
||||
id: nonExistentIds[0],
|
||||
name: 'img.jpeg',
|
||||
},
|
||||
{
|
||||
id: existentIDs[0],
|
||||
name: 'img.jpeg',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const res = entityValidator.validateEntityCreation(models.get('api::dev.dev'), input, {
|
||||
isDraft: true,
|
||||
});
|
||||
await expect(res).rejects.toThrowError(expectedError);
|
||||
});
|
||||
});
|
@ -0,0 +1,153 @@
|
||||
'use strict';
|
||||
|
||||
const models = new Map();
|
||||
models.set('api::dev.dev', {
|
||||
kind: 'collectionType',
|
||||
collectionName: 'devs',
|
||||
modelType: 'contentType',
|
||||
modelName: 'dev',
|
||||
connection: 'default',
|
||||
uid: 'api::dev.dev',
|
||||
apiName: 'dev',
|
||||
globalId: 'Dev',
|
||||
info: {
|
||||
singularName: 'dev',
|
||||
pluralName: 'devs',
|
||||
displayName: 'Dev',
|
||||
description: '',
|
||||
},
|
||||
attributes: {
|
||||
categories: {
|
||||
type: 'relation',
|
||||
relation: 'manyToMany',
|
||||
target: 'api::category.category',
|
||||
inversedBy: 'devs',
|
||||
},
|
||||
sCom: {
|
||||
type: 'component',
|
||||
repeatable: false,
|
||||
component: 'basic.dev-compo',
|
||||
},
|
||||
rCom: {
|
||||
type: 'component',
|
||||
repeatable: true,
|
||||
component: 'basic.dev-compo',
|
||||
},
|
||||
DZ: {
|
||||
type: 'dynamiczone',
|
||||
components: ['basic.dev-compo'],
|
||||
},
|
||||
media: {
|
||||
allowedTypes: ['images', 'files', 'videos', 'audios'],
|
||||
type: 'media',
|
||||
multiple: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
publishedAt: {
|
||||
type: 'datetime',
|
||||
configurable: false,
|
||||
writable: true,
|
||||
visible: false,
|
||||
},
|
||||
createdBy: {
|
||||
type: 'relation',
|
||||
relation: 'oneToOne',
|
||||
target: 'admin::user',
|
||||
configurable: false,
|
||||
writable: false,
|
||||
visible: false,
|
||||
useJoinTable: false,
|
||||
private: true,
|
||||
},
|
||||
updatedBy: {
|
||||
type: 'relation',
|
||||
relation: 'oneToOne',
|
||||
target: 'admin::user',
|
||||
configurable: false,
|
||||
writable: false,
|
||||
visible: false,
|
||||
useJoinTable: false,
|
||||
private: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
models.set('api::category.category', {
|
||||
kind: 'collectionType',
|
||||
collectionName: 'categories',
|
||||
modelType: 'contentType',
|
||||
modelName: 'category',
|
||||
connection: 'default',
|
||||
uid: 'api::category.category',
|
||||
apiName: 'category',
|
||||
globalId: 'Category',
|
||||
info: {
|
||||
displayName: 'Category',
|
||||
singularName: 'category',
|
||||
pluralName: 'categories',
|
||||
description: '',
|
||||
name: 'Category',
|
||||
},
|
||||
attributes: {
|
||||
name: {
|
||||
type: 'string',
|
||||
pluginOptions: {
|
||||
i18n: {
|
||||
localized: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
models.set('basic.dev-compo', {
|
||||
collectionName: 'components_basic_dev_compos',
|
||||
uid: 'basic.dev-compo',
|
||||
category: 'basic',
|
||||
modelType: 'component',
|
||||
modelName: 'dev-compo',
|
||||
globalId: 'ComponentBasicDevCompo',
|
||||
info: {
|
||||
displayName: 'DevCompo',
|
||||
icon: 'allergies',
|
||||
},
|
||||
attributes: {
|
||||
categories: {
|
||||
type: 'relation',
|
||||
relation: 'oneToMany',
|
||||
target: 'api::category.category',
|
||||
},
|
||||
},
|
||||
});
|
||||
models.set('plugin::upload.file', {
|
||||
collectionName: 'files',
|
||||
info: {
|
||||
singularName: 'file',
|
||||
pluralName: 'files',
|
||||
displayName: 'File',
|
||||
description: '',
|
||||
},
|
||||
attributes: {
|
||||
name: {
|
||||
type: 'string',
|
||||
configurable: false,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
kind: 'collectionType',
|
||||
modelType: 'contentType',
|
||||
modelName: 'file',
|
||||
connection: 'default',
|
||||
uid: 'plugin::upload.file',
|
||||
plugin: 'upload',
|
||||
globalId: 'UploadFile',
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
models,
|
||||
existentIDs: [1, 2, 3, 4, 5, 6],
|
||||
nonExistentIds: [10, 11, 12, 13, 14, 15, 16],
|
||||
};
|
@ -5,7 +5,8 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const { has, assoc, prop, isObject } = require('lodash/fp');
|
||||
const { uniqBy, castArray, isNil } = require('lodash');
|
||||
const { has, assoc, prop, isObject, isEmpty, merge } = require('lodash/fp');
|
||||
const strapiUtils = require('@strapi/utils');
|
||||
const validators = require('./validators');
|
||||
|
||||
@ -222,10 +223,136 @@ const createValidateEntity =
|
||||
entity,
|
||||
},
|
||||
{ isDraft }
|
||||
).required();
|
||||
)
|
||||
.test('relations-test', 'check that all relations exist', async function (data) {
|
||||
try {
|
||||
await checkRelationsExist(buildRelationsStore({ uid: model.uid, data }));
|
||||
} catch (e) {
|
||||
return this.createError({
|
||||
path: this.path,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.required();
|
||||
|
||||
return validateYupSchema(validator, { strict: false, abortEarly: false })(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds an object containing all the media and relations being associated with an entity
|
||||
* @param {String} uid of the model
|
||||
* @param {Object} data
|
||||
* @returns {Object}
|
||||
*/
|
||||
const buildRelationsStore = ({ uid, data }) => {
|
||||
if (isEmpty(data)) {
|
||||
return {};
|
||||
}
|
||||
const currentModel = strapi.getModel(uid);
|
||||
|
||||
return Object.keys(currentModel.attributes).reduce((result, attributeName) => {
|
||||
const attribute = currentModel.attributes[attributeName];
|
||||
const value = data[attributeName];
|
||||
|
||||
if (isNil(value)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
switch (attribute.type) {
|
||||
case 'relation':
|
||||
case 'media': {
|
||||
if (attribute.relation === 'morphToMany' || attribute.relation === 'morphToOne') {
|
||||
// TODO: handle polymorphic relations
|
||||
break;
|
||||
}
|
||||
|
||||
const target = attribute.type === 'media' ? 'plugin::upload.file' : attribute.target;
|
||||
// As there are multiple formats supported for associating relations
|
||||
// with an entity, the value here can be an: array, object or number.
|
||||
let source;
|
||||
if (Array.isArray(value)) {
|
||||
source = value;
|
||||
} else if (isObject(value)) {
|
||||
source = value.connect ?? value.set ?? [];
|
||||
} else {
|
||||
source = castArray(value);
|
||||
}
|
||||
const idArray = source.map((v) => ({ id: v.id || v }));
|
||||
|
||||
// Update the relationStore to keep track of all associations being made
|
||||
// with relations and media.
|
||||
result[target] = result[target] || [];
|
||||
result[target].push(...idArray);
|
||||
break;
|
||||
}
|
||||
case 'component': {
|
||||
return castArray(value).reduce(
|
||||
(relationsStore, componentValue) =>
|
||||
merge(
|
||||
relationsStore,
|
||||
buildRelationsStore({
|
||||
uid: attribute.component,
|
||||
data: componentValue,
|
||||
})
|
||||
),
|
||||
result
|
||||
);
|
||||
}
|
||||
case 'dynamiczone': {
|
||||
return value.reduce(
|
||||
(relationsStore, dzValue) =>
|
||||
merge(
|
||||
relationsStore,
|
||||
buildRelationsStore({
|
||||
uid: dzValue.__component,
|
||||
data: dzValue,
|
||||
})
|
||||
),
|
||||
result
|
||||
);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterate through the relations store and validates that every relation or media
|
||||
* mentioned exists
|
||||
*/
|
||||
const checkRelationsExist = async (relationsStore = {}) => {
|
||||
const promises = [];
|
||||
|
||||
for (const [key, value] of Object.entries(relationsStore)) {
|
||||
const evaluate = async () => {
|
||||
const uniqueValues = uniqBy(value, `id`);
|
||||
const count = await strapi.db.query(key).count({
|
||||
where: {
|
||||
id: {
|
||||
$in: uniqueValues.map((v) => v.id),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (count !== uniqueValues.length) {
|
||||
throw new ValidationError(
|
||||
`${
|
||||
uniqueValues.length - count
|
||||
} relation(s) of type ${key} associated with this entity do not exist`
|
||||
);
|
||||
}
|
||||
};
|
||||
promises.push(evaluate());
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateEntityCreation: createValidateEntity('creation'),
|
||||
validateEntityUpdate: createValidateEntity('update'),
|
||||
|
@ -341,7 +341,7 @@ describe('Core API - Basic + dz', () => {
|
||||
error: {
|
||||
status: 400,
|
||||
name: 'ValidationError',
|
||||
message: 'dz[0].__component is a required field',
|
||||
message: '2 errors occurred',
|
||||
details: {
|
||||
errors: [
|
||||
{
|
||||
@ -349,6 +349,11 @@ describe('Core API - Basic + dz', () => {
|
||||
message: 'dz[0].__component is a required field',
|
||||
name: 'ValidationError',
|
||||
},
|
||||
{
|
||||
message: "Cannot read properties of undefined (reading 'attributes')",
|
||||
name: 'ValidationError',
|
||||
path: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -143,6 +143,30 @@ describe('Create Strapi API End to End', () => {
|
||||
expect(body.data.attributes.tags.data[0].id).toBe(data.tags[0].id);
|
||||
});
|
||||
|
||||
test('Create article with non existent tag', async () => {
|
||||
const entry = {
|
||||
title: 'Article 3',
|
||||
content: 'Content 3',
|
||||
tags: [1000],
|
||||
};
|
||||
|
||||
const res = await rq({
|
||||
url: '/articles',
|
||||
method: 'POST',
|
||||
body: {
|
||||
data: entry,
|
||||
},
|
||||
qs: {
|
||||
populate: ['tags'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(JSON.parse(res.error.text).error.message).toContain(
|
||||
`1 relation(s) of type api::tag.tag associated with this entity do not exist`
|
||||
);
|
||||
});
|
||||
|
||||
test('Update article1 add tag2', async () => {
|
||||
const { id, attributes } = data.articles[0];
|
||||
const entry = { ...attributes, tags: [data.tags[1].id] };
|
||||
@ -197,6 +221,30 @@ describe('Create Strapi API End to End', () => {
|
||||
expect(body.data.attributes.tags.data.length).toBe(3);
|
||||
});
|
||||
|
||||
test('Error when updating article1 with some non existent tags', async () => {
|
||||
const { id, attributes } = data.articles[0];
|
||||
const entry = { ...attributes };
|
||||
entry.tags = [1000, 1001, 1002, ...data.tags.slice(-1).map((t) => t.id)];
|
||||
|
||||
cleanDate(entry);
|
||||
|
||||
const res = await rq({
|
||||
url: `/articles/${id}`,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
data: entry,
|
||||
},
|
||||
qs: {
|
||||
populate: ['tags'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(JSON.parse(res.error.text).error.message).toContain(
|
||||
`3 relation(s) of type api::tag.tag associated with this entity do not exist`
|
||||
);
|
||||
});
|
||||
|
||||
test('Update article1 remove one tag', async () => {
|
||||
const { id, attributes } = data.articles[0];
|
||||
|
||||
|
@ -209,6 +209,7 @@ describe('File', () => {
|
||||
data.files[1] = file;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Move a file from root level to a folder', () => {
|
||||
test('when replacing the file', async () => {
|
||||
const res = await rq({
|
||||
|
Loading…
x
Reference in New Issue
Block a user