mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-04 03:43:34 +00:00 
			
		
		
		
	Merge branch 'main' into enhancement/axios-refactoring
This commit is contained in:
		
						commit
						2c234d4d2d
					
				@ -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: [],
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@ -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]));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -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(() => {
 | 
			
		||||
    instance = createEntityService({
 | 
			
		||||
      strapi: {
 | 
			
		||||
        getModel: () => ({
 | 
			
		||||
    const model = {
 | 
			
		||||
      kind: 'singleType',
 | 
			
		||||
      modelName: 'test-model',
 | 
			
		||||
      privateAttributes: [],
 | 
			
		||||
      attributes: {
 | 
			
		||||
        attr: { type: 'string' },
 | 
			
		||||
      },
 | 
			
		||||
        }),
 | 
			
		||||
    };
 | 
			
		||||
    instance = createEntityService({
 | 
			
		||||
      strapi: {
 | 
			
		||||
        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,20 +82,42 @@ 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 fakeEntities = {
 | 
			
		||||
          [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 fakeModel = {
 | 
			
		||||
        const fakeModels = {
 | 
			
		||||
          [entityUID]: {
 | 
			
		||||
            uid: entityUID,
 | 
			
		||||
            kind: 'contentType',
 | 
			
		||||
            modelName: 'test-model',
 | 
			
		||||
            privateAttributes: [],
 | 
			
		||||
            options: {},
 | 
			
		||||
            attributes: {
 | 
			
		||||
            attrStringDefaultRequired: { type: 'string', default: 'default value', required: true },
 | 
			
		||||
              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 },
 | 
			
		||||
@ -112,19 +135,53 @@ describe('Entity service', () => {
 | 
			
		||||
                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 () => {
 | 
			
		||||
      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({
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ const createUserBodySchema = yup.object().shape({
 | 
			
		||||
            connect: yup
 | 
			
		||||
              .array()
 | 
			
		||||
              .of(yup.object().shape({ id: yup.strapiID().required() }))
 | 
			
		||||
              .min(1)
 | 
			
		||||
              .min(1, 'Users must have a role')
 | 
			
		||||
              .required(),
 | 
			
		||||
          })
 | 
			
		||||
          .required()
 | 
			
		||||
@ -36,7 +36,16 @@ const updateUserBodySchema = yup.object().shape({
 | 
			
		||||
          connect: yup
 | 
			
		||||
            .array()
 | 
			
		||||
            .of(yup.object().shape({ id: yup.strapiID().required() }))
 | 
			
		||||
            .min(1)
 | 
			
		||||
            .required(),
 | 
			
		||||
          disconnect: yup
 | 
			
		||||
            .array()
 | 
			
		||||
            .test('CheckDisconnect', 'Cannot remove role', function test(disconnectValue) {
 | 
			
		||||
              if (value.connect.length === 0 && disconnectValue.length > 0) {
 | 
			
		||||
                return false;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              return true;
 | 
			
		||||
            })
 | 
			
		||||
            .required(),
 | 
			
		||||
        })
 | 
			
		||||
      : yup.strapiID()
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user