mirror of
https://github.com/strapi/strapi.git
synced 2025-11-17 10:38:30 +00:00
Merge branch 'main' into master
This commit is contained in:
commit
2207f0c22e
@ -2,6 +2,7 @@ module.exports = {
|
|||||||
rootDir: __dirname,
|
rootDir: __dirname,
|
||||||
setupFilesAfterEnv: ['<rootDir>/test/unit.setup.js'],
|
setupFilesAfterEnv: ['<rootDir>/test/unit.setup.js'],
|
||||||
modulePathIgnorePatterns: ['.cache'],
|
modulePathIgnorePatterns: ['.cache'],
|
||||||
|
testPathIgnorePatterns: ['.testdata.js'],
|
||||||
testMatch: ['/**/__tests__/**/*.[jt]s?(x)'],
|
testMatch: ['/**/__tests__/**/*.[jt]s?(x)'],
|
||||||
// Use `jest-watch-typeahead` version 0.6.5. Newest version 1.0.0 does not support jest@26
|
// 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
|
// Reference: https://github.com/jest-community/jest-watch-typeahead/releases/tag/v1.0.0
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strapi/generate-new": "4.5.0",
|
"@strapi/generate-new": "4.5.0",
|
||||||
"chalk": "4.1.1",
|
"chalk": "4.1.1",
|
||||||
"ci-info": "3.3.2",
|
"ci-info": "3.5.0",
|
||||||
"commander": "7.1.0",
|
"commander": "7.1.0",
|
||||||
"execa": "5.1.1",
|
"execa": "5.1.1",
|
||||||
"fs-extra": "10.0.0",
|
"fs-extra": "10.0.0",
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { getDisplayedValue } from '../useMainValue';
|
||||||
|
|
||||||
|
describe('getDisplayedValue', () => {
|
||||||
|
it('returns the mainField value', () => {
|
||||||
|
const modifiedData = {
|
||||||
|
DeepComplex: [
|
||||||
|
{
|
||||||
|
Title: 'File',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const componentFieldPath = ['DeepComplex', 0];
|
||||||
|
const mainField = 'Title';
|
||||||
|
|
||||||
|
const normalizedContent = getDisplayedValue(modifiedData, componentFieldPath, mainField);
|
||||||
|
|
||||||
|
expect(normalizedContent).toEqual('File');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import toString from 'lodash/toString';
|
||||||
|
import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
||||||
|
|
||||||
|
export function getDisplayedValue(modifiedData, componentFieldPath, mainField) {
|
||||||
|
return toString(get(modifiedData, [...componentFieldPath, mainField], ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMainValue(schema, componentFieldPath) {
|
||||||
|
const { modifiedData } = useCMEditViewDataManager();
|
||||||
|
|
||||||
|
const mainField = useMemo(() => get(schema, ['settings', 'mainField'], 'id'), [schema]);
|
||||||
|
|
||||||
|
const displayedValue =
|
||||||
|
mainField === 'id' ? '' : getDisplayedValue(modifiedData, componentFieldPath, mainField);
|
||||||
|
|
||||||
|
return displayedValue.trim().length < 1 ? '' : ` - ${displayedValue}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMainValue;
|
||||||
@ -18,6 +18,7 @@ import { useContentTypeLayout } from '../../../../hooks';
|
|||||||
import { getTrad } from '../../../../utils';
|
import { getTrad } from '../../../../utils';
|
||||||
import FieldComponent from '../../../FieldComponent';
|
import FieldComponent from '../../../FieldComponent';
|
||||||
import Rectangle from './Rectangle';
|
import Rectangle from './Rectangle';
|
||||||
|
import { connect, select } from './utils';
|
||||||
|
|
||||||
const ActionStack = styled(Stack)`
|
const ActionStack = styled(Stack)`
|
||||||
svg {
|
svg {
|
||||||
@ -55,6 +56,8 @@ const Component = ({
|
|||||||
removeComponentFromDynamicZone,
|
removeComponentFromDynamicZone,
|
||||||
showDownIcon,
|
showDownIcon,
|
||||||
showUpIcon,
|
showUpIcon,
|
||||||
|
// Passed with the select function
|
||||||
|
mainValue,
|
||||||
}) => {
|
}) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { getComponentLayout } = useContentTypeLayout();
|
const { getComponentLayout } = useContentTypeLayout();
|
||||||
@ -144,7 +147,7 @@ const Component = ({
|
|||||||
)}
|
)}
|
||||||
</ActionStack>
|
</ActionStack>
|
||||||
}
|
}
|
||||||
title={friendlyName}
|
title={`${friendlyName}${mainValue}`}
|
||||||
togglePosition="left"
|
togglePosition="left"
|
||||||
/>
|
/>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
@ -186,6 +189,9 @@ Component.propTypes = {
|
|||||||
removeComponentFromDynamicZone: PropTypes.func.isRequired,
|
removeComponentFromDynamicZone: PropTypes.func.isRequired,
|
||||||
showDownIcon: PropTypes.bool.isRequired,
|
showDownIcon: PropTypes.bool.isRequired,
|
||||||
showUpIcon: PropTypes.bool.isRequired,
|
showUpIcon: PropTypes.bool.isRequired,
|
||||||
|
mainValue: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(Component, isEqual);
|
const Memoized = memo(Component, isEqual);
|
||||||
|
|
||||||
|
export default connect(Memoized, select);
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function connect(WrappedComponent, select) {
|
||||||
|
return (props) => {
|
||||||
|
const selectors = select(props);
|
||||||
|
|
||||||
|
return <WrappedComponent {...props} {...selectors} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export { default as connect } from './connect';
|
||||||
|
export { default as select } from './select';
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import useMainValue from '../hooks/useMainValue';
|
||||||
|
import { useContentTypeLayout } from '../../../../../hooks';
|
||||||
|
|
||||||
|
function useSelect({ componentUid, name, index }) {
|
||||||
|
const { getComponentLayout } = useContentTypeLayout();
|
||||||
|
const componentLayoutData = useMemo(() => {
|
||||||
|
const layout = getComponentLayout(componentUid);
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}, [componentUid, getComponentLayout]);
|
||||||
|
const mainValue = useMainValue(componentLayoutData, [name, index]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mainValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSelect;
|
||||||
@ -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 = () => {
|
const HomePage = () => {
|
||||||
// // Temporary until we develop the menu API
|
// Temporary until we develop the menu API
|
||||||
const { collectionTypes, singleTypes, isLoading: isLoadingForModels } = useModels();
|
const { collectionTypes, singleTypes, isLoading: isLoadingForModels } = useModels();
|
||||||
const { guidedTourState, isGuidedTourVisible, isSkipped } = useGuidedTour();
|
const { guidedTourState, isGuidedTourVisible, isSkipped } = useGuidedTour();
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
"@casl/ability": "^5.4.3",
|
"@casl/ability": "^5.4.3",
|
||||||
"@fingerprintjs/fingerprintjs": "3.3.3",
|
"@fingerprintjs/fingerprintjs": "3.3.3",
|
||||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.1.2",
|
"@fortawesome/fontawesome-svg-core": "6.2.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.3",
|
"@fortawesome/free-brands-svg-icons": "^5.15.3",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
|||||||
@ -271,7 +271,7 @@ describe('CM API - Basic + dz + draftAndPublish', () => {
|
|||||||
error: {
|
error: {
|
||||||
status: 400,
|
status: 400,
|
||||||
name: 'ValidationError',
|
name: 'ValidationError',
|
||||||
message: 'dz[0].__component is a required field',
|
message: '2 errors occurred',
|
||||||
details: {
|
details: {
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
@ -279,6 +279,11 @@ describe('CM API - Basic + dz + draftAndPublish', () => {
|
|||||||
message: 'dz[0].__component is a required field',
|
message: 'dz[0].__component is a required field',
|
||||||
name: 'ValidationError',
|
name: 'ValidationError',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
message: "Cannot read properties of undefined (reading 'attributes')",
|
||||||
|
name: 'ValidationError',
|
||||||
|
path: [],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -301,7 +301,7 @@ describe('CM API - Basic + dz', () => {
|
|||||||
error: {
|
error: {
|
||||||
status: 400,
|
status: 400,
|
||||||
name: 'ValidationError',
|
name: 'ValidationError',
|
||||||
message: 'dz[0].__component is a required field',
|
message: '2 errors occurred',
|
||||||
details: {
|
details: {
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
@ -309,6 +309,11 @@ describe('CM API - Basic + dz', () => {
|
|||||||
message: 'dz[0].__component is a required field',
|
message: 'dz[0].__component is a required field',
|
||||||
name: 'ValidationError',
|
name: 'ValidationError',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
message: "Cannot read properties of undefined (reading 'attributes')",
|
||||||
|
name: 'ValidationError',
|
||||||
|
path: [],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// Helpers.
|
|
||||||
const { createTestBuilder } = require('../../../../../../test/helpers/builder');
|
|
||||||
const { createStrapiInstance } = require('../../../../../../test/helpers/strapi');
|
|
||||||
const form = require('../../../../../../test/helpers/generators');
|
|
||||||
const { createAuthRequest } = require('../../../../../../test/helpers/request');
|
|
||||||
|
|
||||||
const builder = createTestBuilder();
|
|
||||||
let strapi;
|
|
||||||
let rq;
|
|
||||||
|
|
||||||
const restart = async () => {
|
|
||||||
await strapi.destroy();
|
|
||||||
strapi = await createStrapiInstance();
|
|
||||||
rq = await createAuthRequest({ strapi });
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Content Manager - Hide relations', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
await builder.addContentTypes([form.article]).build();
|
|
||||||
|
|
||||||
strapi = await createStrapiInstance();
|
|
||||||
rq = await createAuthRequest({ strapi });
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await strapi.destroy();
|
|
||||||
await builder.cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Hide relations', async () => {
|
|
||||||
await rq({
|
|
||||||
url: '/content-manager/content-types/api::article.article/configuration',
|
|
||||||
method: 'PUT',
|
|
||||||
body: {
|
|
||||||
layouts: {
|
|
||||||
edit: [],
|
|
||||||
editRelations: [],
|
|
||||||
list: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { body } = await rq({
|
|
||||||
url: '/content-manager/content-types/api::article.article/configuration',
|
|
||||||
method: 'GET',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(body.data.contentType.layouts.editRelations).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Hide relations after server restart', async () => {
|
|
||||||
await rq({
|
|
||||||
url: '/content-manager/content-types/api::article.article/configuration',
|
|
||||||
method: 'PUT',
|
|
||||||
body: {
|
|
||||||
layouts: {
|
|
||||||
edit: [],
|
|
||||||
editRelations: [],
|
|
||||||
list: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await restart();
|
|
||||||
|
|
||||||
const { body } = await rq({
|
|
||||||
url: '/content-manager/content-types/api::article.article/configuration',
|
|
||||||
method: 'GET',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(body.data.contentType.layouts.editRelations).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,196 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// Test a simple default API with no relations
|
|
||||||
|
|
||||||
const { omit, pick } = require('lodash/fp');
|
|
||||||
|
|
||||||
const { createTestBuilder } = require('../../../../../test/helpers/builder');
|
|
||||||
const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
|
|
||||||
const { createAuthRequest } = require('../../../../../test/helpers/request');
|
|
||||||
|
|
||||||
let strapi;
|
|
||||||
let rq;
|
|
||||||
const data = {
|
|
||||||
products: [],
|
|
||||||
shops: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const productModel = {
|
|
||||||
attributes: {
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
displayName: 'Product',
|
|
||||||
singularName: 'product',
|
|
||||||
pluralName: 'products',
|
|
||||||
description: '',
|
|
||||||
collectionName: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const productWithDPModel = {
|
|
||||||
attributes: {
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
displayName: 'Product',
|
|
||||||
singularName: 'product',
|
|
||||||
pluralName: 'products',
|
|
||||||
draftAndPublish: true,
|
|
||||||
description: '',
|
|
||||||
collectionName: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const shopModel = {
|
|
||||||
attributes: {
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
products: {
|
|
||||||
type: 'relation',
|
|
||||||
relation: 'manyToMany',
|
|
||||||
target: 'api::product.product',
|
|
||||||
targetAttribute: 'shops',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
displayName: 'Shop',
|
|
||||||
singularName: 'shop',
|
|
||||||
pluralName: 'shops',
|
|
||||||
};
|
|
||||||
|
|
||||||
const shops = [
|
|
||||||
{
|
|
||||||
name: 'market',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const products =
|
|
||||||
({ withPublished = false }) =>
|
|
||||||
({ shop }) => {
|
|
||||||
const shops = [shop[0].id];
|
|
||||||
|
|
||||||
const entries = [
|
|
||||||
{
|
|
||||||
name: 'tomato',
|
|
||||||
shops,
|
|
||||||
publishedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'apple',
|
|
||||||
shops,
|
|
||||||
publishedAt: null,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (withPublished) {
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries.map(omit('publishedAt'));
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Relation-list route', () => {
|
|
||||||
describe('without draftAndPublish', () => {
|
|
||||||
const builder = createTestBuilder();
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await builder
|
|
||||||
.addContentTypes([productModel, shopModel])
|
|
||||||
.addFixtures(shopModel.singularName, shops)
|
|
||||||
.addFixtures(productModel.singularName, products({ withPublished: false }))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
strapi = await createStrapiInstance();
|
|
||||||
rq = await createAuthRequest({ strapi });
|
|
||||||
|
|
||||||
data.shops = await builder.sanitizedFixturesFor(shopModel.singularName, strapi);
|
|
||||||
data.products = await builder.sanitizedFixturesFor(productModel.singularName, strapi);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await strapi.destroy();
|
|
||||||
await builder.cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Can get relation-list for products of a shop', async () => {
|
|
||||||
const res = await rq({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/content-manager/relations/api::shop.shop/products',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.body).toHaveLength(data.products.length);
|
|
||||||
data.products.forEach((product, index) => {
|
|
||||||
expect(res.body[index]).toStrictEqual(pick(['_id', 'id', 'name'], product));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Can get relation-list for products of a shop and omit some results', async () => {
|
|
||||||
const res = await rq({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/content-manager/relations/api::shop.shop/products',
|
|
||||||
body: {
|
|
||||||
idsToOmit: [data.products[0].id],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.body).toHaveLength(1);
|
|
||||||
expect(res.body[0]).toStrictEqual(pick(['_id', 'id', 'name'], data.products[1]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with draftAndPublish', () => {
|
|
||||||
const builder = createTestBuilder();
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await builder
|
|
||||||
.addContentTypes([productWithDPModel, shopModel])
|
|
||||||
.addFixtures(shopModel.singularName, shops)
|
|
||||||
.addFixtures(productWithDPModel.singularName, products({ withPublished: true }))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
strapi = await createStrapiInstance();
|
|
||||||
rq = await createAuthRequest({ strapi });
|
|
||||||
|
|
||||||
data.shops = await builder.sanitizedFixturesFor(shopModel.singularName, strapi);
|
|
||||||
data.products = await builder.sanitizedFixturesFor(productWithDPModel.singularName, strapi);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await strapi.destroy();
|
|
||||||
await builder.cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Can get relation-list for products of a shop', async () => {
|
|
||||||
const res = await rq({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/content-manager/relations/api::shop.shop/products',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.body).toHaveLength(data.products.length);
|
|
||||||
|
|
||||||
const tomatoProductRes = res.body.find((p) => p.name === 'tomato');
|
|
||||||
const appleProductRes = res.body.find((p) => p.name === 'apple');
|
|
||||||
|
|
||||||
expect(tomatoProductRes).toMatchObject(pick(['_id', 'id', 'name'], data.products[0]));
|
|
||||||
expect(tomatoProductRes.publishedAt).toBeISODate();
|
|
||||||
expect(appleProductRes).toStrictEqual({
|
|
||||||
...pick(['_id', 'id', 'name'], data.products[1]),
|
|
||||||
publishedAt: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Can get relation-list for products of a shop and omit some results', async () => {
|
|
||||||
const res = await rq({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/content-manager/relations/api::shop.shop/products',
|
|
||||||
body: {
|
|
||||||
idsToOmit: [data.products[1].id],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.body).toHaveLength(1);
|
|
||||||
expect(res.body[0]).toMatchObject(pick(['_id', 'id', 'name'], data.products[0]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -40,7 +40,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.15.2",
|
"@fortawesome/fontawesome-free": "^5.15.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.1.2",
|
"@fortawesome/fontawesome-svg-core": "6.2.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.2",
|
"@fortawesome/free-brands-svg-icons": "^5.15.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
|||||||
@ -4,28 +4,22 @@ const createEntityService = require('..');
|
|||||||
const entityValidator = require('../../entity-validator');
|
const entityValidator = require('../../entity-validator');
|
||||||
|
|
||||||
describe('Entity service triggers webhooks', () => {
|
describe('Entity service triggers webhooks', () => {
|
||||||
global.strapi = {
|
|
||||||
getModel: () => ({}),
|
|
||||||
config: {
|
|
||||||
get: () => [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let instance;
|
let instance;
|
||||||
const eventHub = { emit: jest.fn() };
|
const eventHub = { emit: jest.fn() };
|
||||||
let entity = { attr: 'value' };
|
let entity = { attr: 'value' };
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
const model = {
|
||||||
|
kind: 'singleType',
|
||||||
|
modelName: 'test-model',
|
||||||
|
privateAttributes: [],
|
||||||
|
attributes: {
|
||||||
|
attr: { type: 'string' },
|
||||||
|
},
|
||||||
|
};
|
||||||
instance = createEntityService({
|
instance = createEntityService({
|
||||||
strapi: {
|
strapi: {
|
||||||
getModel: () => ({
|
getModel: () => model,
|
||||||
kind: 'singleType',
|
|
||||||
modelName: 'test-model',
|
|
||||||
privateAttributes: [],
|
|
||||||
attributes: {
|
|
||||||
attr: { type: 'string' },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
db: {
|
db: {
|
||||||
query: () => ({
|
query: () => ({
|
||||||
@ -41,6 +35,13 @@ describe('Entity service triggers webhooks', () => {
|
|||||||
eventHub,
|
eventHub,
|
||||||
entityValidator,
|
entityValidator,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
getModel: () => model,
|
||||||
|
config: {
|
||||||
|
get: () => [],
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Emit event: Create', async () => {
|
test('Emit event: Create', async () => {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
jest.mock('bcryptjs', () => ({ hashSync: () => 'secret-password' }));
|
jest.mock('bcryptjs', () => ({ hashSync: () => 'secret-password' }));
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
|
const { ValidationError } = require('@strapi/utils').errors;
|
||||||
const createEntityService = require('..');
|
const createEntityService = require('..');
|
||||||
const entityValidator = require('../../entity-validator');
|
const entityValidator = require('../../entity-validator');
|
||||||
|
|
||||||
@ -81,50 +82,106 @@ describe('Entity service', () => {
|
|||||||
describe('Create', () => {
|
describe('Create', () => {
|
||||||
describe('assign default values', () => {
|
describe('assign default values', () => {
|
||||||
let instance;
|
let instance;
|
||||||
|
const entityUID = 'api::entity.entity';
|
||||||
|
const relationUID = 'api::relation.relation';
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
const fakeQuery = {
|
const fakeEntities = {
|
||||||
count: jest.fn(() => 0),
|
[relationUID]: {
|
||||||
create: jest.fn(({ data }) => data),
|
1: {
|
||||||
};
|
id: 1,
|
||||||
|
Name: 'TestRelation',
|
||||||
const fakeModel = {
|
createdAt: '2022-09-28T15:11:22.995Z',
|
||||||
kind: 'contentType',
|
updatedAt: '2022-09-29T09:01:02.949Z',
|
||||||
modelName: 'test-model',
|
publishedAt: null,
|
||||||
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: {
|
2: {
|
||||||
type: 'enumeration',
|
id: 2,
|
||||||
enum: ['a', 'b', 'c'],
|
Name: 'TestRelation2',
|
||||||
default: 'b',
|
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 = {
|
const fakeDB = {
|
||||||
query: jest.fn(() => fakeQuery),
|
query: jest.fn((uid) => fakeQuery(uid)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const fakeStrapi = {
|
global.strapi = {
|
||||||
getModel: jest.fn(() => fakeModel),
|
getModel: jest.fn((uid) => {
|
||||||
|
return fakeModels[uid];
|
||||||
|
}),
|
||||||
|
db: fakeDB,
|
||||||
};
|
};
|
||||||
|
|
||||||
instance = createEntityService({
|
instance = createEntityService({
|
||||||
strapi: fakeStrapi,
|
strapi: global.strapi,
|
||||||
db: fakeDB,
|
db: fakeDB,
|
||||||
eventHub: new EventEmitter(),
|
eventHub: new EventEmitter(),
|
||||||
entityValidator,
|
entityValidator,
|
||||||
@ -134,7 +191,7 @@ describe('Entity service', () => {
|
|||||||
test('should create record with all default attributes', async () => {
|
test('should create record with all default attributes', async () => {
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
await expect(instance.create('test-model', { data })).resolves.toMatchObject({
|
await expect(instance.create(entityUID, { data })).resolves.toMatchObject({
|
||||||
attrStringDefaultRequired: 'default value',
|
attrStringDefaultRequired: 'default value',
|
||||||
attrStringDefault: 'default value',
|
attrStringDefault: 'default value',
|
||||||
attrBoolDefaultRequired: true,
|
attrBoolDefaultRequired: true,
|
||||||
@ -154,7 +211,7 @@ describe('Entity service', () => {
|
|||||||
attrEnumDefault: 'c',
|
attrEnumDefault: 'c',
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(instance.create('test-model', { data })).resolves.toMatchObject({
|
await expect(instance.create(entityUID, { data })).resolves.toMatchObject({
|
||||||
attrStringDefault: 'my value',
|
attrStringDefault: 'my value',
|
||||||
attrBoolDefault: false,
|
attrBoolDefault: false,
|
||||||
attrIntDefault: 2,
|
attrIntDefault: 2,
|
||||||
@ -179,11 +236,225 @@ describe('Entity service', () => {
|
|||||||
attrPassword: 'fooBar',
|
attrPassword: 'fooBar',
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(instance.create('test-model', { data })).resolves.toMatchObject({
|
await expect(instance.create(entityUID, { data })).resolves.toMatchObject({
|
||||||
...data,
|
...data,
|
||||||
attrPassword: 'secret-password',
|
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';
|
'use strict';
|
||||||
|
|
||||||
const entityValidator = require('../entity-validator');
|
const entityValidator = require('..');
|
||||||
|
|
||||||
describe('Entity validator', () => {
|
describe('Entity validator', () => {
|
||||||
describe('Published input', () => {
|
describe('Published input', () => {
|
||||||
describe('General Errors', () => {
|
describe('General Errors', () => {
|
||||||
it('Throws a badRequest error on invalid input', async () => {
|
let model;
|
||||||
global.strapi = {
|
global.strapi = {
|
||||||
errors: {
|
errors: {
|
||||||
badRequest: jest.fn(),
|
badRequest: jest.fn(),
|
||||||
},
|
},
|
||||||
};
|
getModel: () => model,
|
||||||
|
};
|
||||||
|
|
||||||
const model = {
|
it('Throws a badRequest error on invalid input', async () => {
|
||||||
|
model = {
|
||||||
attributes: {
|
attributes: {
|
||||||
title: {
|
title: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -44,7 +46,7 @@ describe('Entity validator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Returns data on valid input', async () => {
|
it('Returns data on valid input', async () => {
|
||||||
const model = {
|
model = {
|
||||||
attributes: {
|
attributes: {
|
||||||
title: {
|
title: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -61,7 +63,7 @@ describe('Entity validator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Returns casted data when possible', async () => {
|
it('Returns casted data when possible', async () => {
|
||||||
const model = {
|
model = {
|
||||||
attributes: {
|
attributes: {
|
||||||
title: {
|
title: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -84,13 +86,7 @@ describe('Entity validator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Throws on required not respected', async () => {
|
test('Throws on required not respected', async () => {
|
||||||
global.strapi = {
|
model = {
|
||||||
errors: {
|
|
||||||
badRequest: jest.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const model = {
|
|
||||||
attributes: {
|
attributes: {
|
||||||
title: {
|
title: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -139,7 +135,7 @@ describe('Entity validator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Supports custom field types', async () => {
|
it('Supports custom field types', async () => {
|
||||||
const model = {
|
model = {
|
||||||
attributes: {
|
attributes: {
|
||||||
uuid: {
|
uuid: {
|
||||||
type: 'uuid',
|
type: 'uuid',
|
||||||
@ -164,6 +160,7 @@ describe('Entity validator', () => {
|
|||||||
errors: {
|
errors: {
|
||||||
badRequest: jest.fn(),
|
badRequest: jest.fn(),
|
||||||
},
|
},
|
||||||
|
getModel: () => model,
|
||||||
};
|
};
|
||||||
|
|
||||||
const model = {
|
const model = {
|
||||||
@ -199,12 +196,6 @@ describe('Entity validator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Throws on max length not respected', async () => {
|
test('Throws on max length not respected', async () => {
|
||||||
global.strapi = {
|
|
||||||
errors: {
|
|
||||||
badRequest: jest.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const model = {
|
const model = {
|
||||||
attributes: {
|
attributes: {
|
||||||
title: {
|
title: {
|
||||||
@ -329,9 +320,11 @@ describe('Entity validator', () => {
|
|||||||
errors: {
|
errors: {
|
||||||
badRequest: jest.fn(),
|
badRequest: jest.fn(),
|
||||||
},
|
},
|
||||||
|
getModel: () => model,
|
||||||
};
|
};
|
||||||
|
|
||||||
const model = {
|
const model = {
|
||||||
|
uid: 'api::test.test',
|
||||||
attributes: {
|
attributes: {
|
||||||
title: {
|
title: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -456,6 +449,13 @@ describe('Entity validator', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
errors: {
|
||||||
|
badRequest: jest.fn(),
|
||||||
|
},
|
||||||
|
getModel: () => model,
|
||||||
|
};
|
||||||
|
|
||||||
const input = { title: 'tooSmall' };
|
const input = { title: 'tooSmall' };
|
||||||
|
|
||||||
expect.hasAssertions();
|
expect.hasAssertions();
|
||||||
@ -465,12 +465,6 @@ describe('Entity validator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Throws on max length not respected', async () => {
|
test('Throws on max length not respected', async () => {
|
||||||
global.strapi = {
|
|
||||||
errors: {
|
|
||||||
badRequest: jest.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const model = {
|
const model = {
|
||||||
attributes: {
|
attributes: {
|
||||||
title: {
|
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';
|
'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 strapiUtils = require('@strapi/utils');
|
||||||
const validators = require('./validators');
|
const validators = require('./validators');
|
||||||
|
|
||||||
@ -222,10 +223,136 @@ const createValidateEntity =
|
|||||||
entity,
|
entity,
|
||||||
},
|
},
|
||||||
{ isDraft }
|
{ 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);
|
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 = {
|
module.exports = {
|
||||||
validateEntityCreation: createValidateEntity('creation'),
|
validateEntityCreation: createValidateEntity('creation'),
|
||||||
validateEntityUpdate: createValidateEntity('update'),
|
validateEntityUpdate: createValidateEntity('update'),
|
||||||
|
|||||||
@ -96,7 +96,7 @@
|
|||||||
"boxen": "5.1.2",
|
"boxen": "5.1.2",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"chokidar": "3.5.2",
|
"chokidar": "3.5.2",
|
||||||
"ci-info": "3.3.2",
|
"ci-info": "3.5.0",
|
||||||
"cli-table3": "0.6.2",
|
"cli-table3": "0.6.2",
|
||||||
"commander": "8.2.0",
|
"commander": "8.2.0",
|
||||||
"configstore": "5.0.1",
|
"configstore": "5.0.1",
|
||||||
|
|||||||
@ -341,7 +341,7 @@ describe('Core API - Basic + dz', () => {
|
|||||||
error: {
|
error: {
|
||||||
status: 400,
|
status: 400,
|
||||||
name: 'ValidationError',
|
name: 'ValidationError',
|
||||||
message: 'dz[0].__component is a required field',
|
message: '2 errors occurred',
|
||||||
details: {
|
details: {
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
@ -349,6 +349,11 @@ describe('Core API - Basic + dz', () => {
|
|||||||
message: 'dz[0].__component is a required field',
|
message: 'dz[0].__component is a required field',
|
||||||
name: 'ValidationError',
|
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);
|
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 () => {
|
test('Update article1 add tag2', async () => {
|
||||||
const { id, attributes } = data.articles[0];
|
const { id, attributes } = data.articles[0];
|
||||||
const entry = { ...attributes, tags: [data.tags[1].id] };
|
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);
|
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 () => {
|
test('Update article1 remove one tag', async () => {
|
||||||
const { id, attributes } = data.articles[0];
|
const { id, attributes } = data.articles[0];
|
||||||
|
|
||||||
|
|||||||
@ -18,8 +18,8 @@ const uploadAsset = (asset, folderId, cancelToken, onProgress) => {
|
|||||||
'fileInfo',
|
'fileInfo',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
name,
|
name,
|
||||||
caption: caption || name,
|
caption,
|
||||||
alternativeText: alternativeText || name,
|
alternativeText,
|
||||||
folder: folderId,
|
folder: folderId,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@ -209,6 +209,7 @@ describe('File', () => {
|
|||||||
data.files[1] = file;
|
data.files[1] = file;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Move a file from root level to a folder', () => {
|
describe('Move a file from root level to a folder', () => {
|
||||||
test('when replacing the file', async () => {
|
test('when replacing the file', async () => {
|
||||||
const res = await rq({
|
const res = await rq({
|
||||||
|
|||||||
39
yarn.lock
39
yarn.lock
@ -2135,10 +2135,10 @@
|
|||||||
intl-messageformat "9.13.0"
|
intl-messageformat "9.13.0"
|
||||||
tslib "^2.1.0"
|
tslib "^2.1.0"
|
||||||
|
|
||||||
"@fortawesome/fontawesome-common-types@6.1.2":
|
"@fortawesome/fontawesome-common-types@6.2.0":
|
||||||
version "6.1.2"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.2.tgz#c1095b1bbabf19f37f9ff0719db38d92a410bcfe"
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz#76467a94aa888aeb22aafa43eb6ff889df3a5a7f"
|
||||||
integrity sha512-wBaAPGz1Awxg05e0PBRkDRuTsy4B3dpBm+zreTTyd9TH4uUM27cAL4xWyWR0rLJCrRwzVsQ4hF3FvM6rqydKPA==
|
integrity sha512-rBevIsj2nclStJ7AxTdfsa3ovHb1H+qApwrxcTVo+NNdeJiB9V75hsKfrkG5AwNcRUNxrPPiScGYCNmLMoh8pg==
|
||||||
|
|
||||||
"@fortawesome/fontawesome-common-types@^0.2.36":
|
"@fortawesome/fontawesome-common-types@^0.2.36":
|
||||||
version "0.2.36"
|
version "0.2.36"
|
||||||
@ -2150,12 +2150,12 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5"
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5"
|
||||||
integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==
|
integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==
|
||||||
|
|
||||||
"@fortawesome/fontawesome-svg-core@6.1.2":
|
"@fortawesome/fontawesome-svg-core@6.2.0":
|
||||||
version "6.1.2"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.2.tgz#11e2e8583a7dea75d734e4d0e53d91c63fae7511"
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.0.tgz#11856eaf4dd1d865c442ddea1eed8ee855186ba2"
|
||||||
integrity sha512-853G/Htp0BOdXnPoeCPTjFrVwyrJHpe8MhjB/DYE9XjwhnNDfuBCd3aKc2YUYbEfHEcBws4UAA0kA9dymZKGjA==
|
integrity sha512-Cf2mAAeMWFMzpLC7Y9H1I4o3wEU+XovVJhTiNG8ZNgSQj53yl7OCJaS80K4YjrABWZzbAHVaoHE1dVJ27AAYXw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@fortawesome/fontawesome-common-types" "6.1.2"
|
"@fortawesome/fontawesome-common-types" "6.2.0"
|
||||||
|
|
||||||
"@fortawesome/free-brands-svg-icons@^5.15.2", "@fortawesome/free-brands-svg-icons@^5.15.3":
|
"@fortawesome/free-brands-svg-icons@^5.15.2", "@fortawesome/free-brands-svg-icons@^5.15.3":
|
||||||
version "5.15.4"
|
version "5.15.4"
|
||||||
@ -9150,10 +9150,10 @@ chrome-trace-event@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
|
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
|
||||||
integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
|
integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
|
||||||
|
|
||||||
ci-info@3.3.2, ci-info@^3.2.0:
|
ci-info@3.5.0, ci-info@^3.2.0:
|
||||||
version "3.3.2"
|
version "3.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128"
|
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.5.0.tgz#bfac2a29263de4c829d806b1ab478e35091e171f"
|
||||||
integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==
|
integrity sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==
|
||||||
|
|
||||||
ci-info@^2.0.0:
|
ci-info@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@ -15852,9 +15852,9 @@ loader-runner@^4.2.0:
|
|||||||
integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
|
integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
|
||||||
|
|
||||||
loader-utils@^1.2.3:
|
loader-utils@^1.2.3:
|
||||||
version "1.4.0"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
|
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.1.tgz#278ad7006660bccc4d2c0c1578e17c5c78d5c0e0"
|
||||||
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
|
integrity sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
big.js "^5.2.2"
|
big.js "^5.2.2"
|
||||||
emojis-list "^3.0.0"
|
emojis-list "^3.0.0"
|
||||||
@ -16712,11 +16712,16 @@ minimist-options@4.1.0:
|
|||||||
is-plain-obj "^1.1.0"
|
is-plain-obj "^1.1.0"
|
||||||
kind-of "^6.0.3"
|
kind-of "^6.0.3"
|
||||||
|
|
||||||
minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
|
minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
|
||||||
version "1.2.6"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
|
minimist@^1.2.0:
|
||||||
|
version "1.2.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
||||||
|
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
||||||
|
|
||||||
minipass-collect@^1.0.2:
|
minipass-collect@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
|
resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user