Merge branch 'features/deits' into deits/provider-results

This commit is contained in:
Ben Irvin 2022-11-16 10:55:37 +01:00
commit 7cf2ae8d05
21 changed files with 193 additions and 351 deletions

View File

@ -0,0 +1,15 @@
'use strict';
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
/**
* TODO: this should extend @strapi/eslint-config/typescript but doing so requires configuring parserOption.project, which requires tsconfig.json configuration
*/
// extends: ['plugin:@typescript-eslint/recommended'],
globals: {
strapi: false,
},
// Instead of extending (which includes values that interfere with this configuration), only take the rules field
rules: require('./.eslintrc.back.js').rules,
};

View File

@ -13,10 +13,20 @@ module.exports = {
},
overrides: [
{
// Backend javascript
files: ['packages/**/*.js', 'test/**/*.js', 'scripts/**/*.js'],
excludedFiles: frontPaths,
...require('./.eslintrc.back.js'),
},
// Backend typescript
{
files: ['packages/**/*.ts', 'test/**/*.ts', 'scripts/**/*.ts'],
excludedFiles: frontPaths,
...require('./.eslintrc.back.typescript.js'),
},
// Frontend
{
files: frontPaths,
...require('./.eslintrc.front.js'),

View File

@ -41,7 +41,7 @@
"format": "npm-run-all -p format:*",
"format:code": "npm run prettier:code -- --write",
"format:other": "npm run prettier:other -- --write",
"prettier:code": "prettier \"**/*.js\"",
"prettier:code": "prettier \"**/*.{js,ts}\"",
"prettier:other": "prettier \"**/*.{md,css,scss,yaml,yml}\"",
"test:clean": "rimraf ./coverage",
"test:front": "npm run test:clean && cross-env IS_EE=true jest --config ./jest.config.front.js",
@ -56,10 +56,10 @@
"doc:api": "node scripts/open-api/serve.js"
},
"lint-staged": {
"*.{js,md,css,scss,yaml,yml}": [
"*.{js,ts,md,css,scss,yaml,yml}": [
"prettier --write"
],
"*.js": [
"*.{js,ts}": [
"eslint --fix"
]
},
@ -70,12 +70,22 @@
"@strapi/eslint-config": "0.1.2",
"@swc/core": "1.2.224",
"@swc/jest": "0.2.22",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.42.1",
"babel-eslint": "10.1.0",
"chalk": "4.1.2",
"chokidar": "3.5.3",
"cross-env": "7.0.3",
"dotenv": "14.2.0",
"eslint": "8.21.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin": "1.0.1",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.31.10",
"eslint-plugin-react-hooks": "4.6.0",
"execa": "1.0.0",
"fs-extra": "10.1.0",
"get-port": "5.1.1",
@ -105,6 +115,7 @@
"stylelint-processor-styled-components": "1.10.0",
"supertest": "6.2.4",
"ts-jest": "29.0.3",
"typescript": "4.6.2",
"yargs": "17.6.0"
},
"engines": {

View File

@ -9,6 +9,7 @@ const FilterSelect = ({ message, value, onChange, possibleFilters, onClear, cust
return (
<Select
data-testid={`${message}-button`}
aria-label={message}
placeholder={message}
size="M"
@ -20,7 +21,7 @@ const FilterSelect = ({ message, value, onChange, possibleFilters, onClear, cust
>
{Object.entries(possibleFilters).map(([filterName, count]) => {
return (
<Option key={filterName} value={filterName}>
<Option data-testid={`${filterName}-${count}`} key={filterName} value={filterName}>
{computeFilterMessage(filterName, count)}
</Option>
);

View File

@ -51,6 +51,7 @@ const NpmPackagesFilters = ({
<ButtonToggle
variant="tertiary"
ref={buttonRef}
data-testid="filters-button"
startIcon={<Filter />}
onClick={handleToggle}
size="S"

View File

@ -1378,6 +1378,7 @@ exports[`Marketplace page - layout renders the online layout 1`] = `
<button
aria-disabled="false"
class="c52 c53 c54"
data-testid="filters-button"
type="button"
>
<div

View File

@ -1378,6 +1378,7 @@ exports[`Marketplace page - plugins tab renders and matches the plugin tab snaps
<button
aria-disabled="false"
class="c52 c53 c54"
data-testid="filters-button"
type="button"
>
<div

View File

@ -1373,6 +1373,7 @@ exports[`Marketplace page - providers tab renders and matches the providers tab
<button
aria-disabled="false"
class="c52 c53 c54"
data-testid="filters-button"
type="button"
>
<div

View File

@ -66,7 +66,6 @@ const App = (
const waitForReload = async () => {
await waitFor(() => {
expect(screen.queryByTestId('loader')).toBe(null);
expect(screen.getByRole('heading', { name: /marketplace/i })).toBeInTheDocument();
});
};

View File

@ -54,7 +54,6 @@ const waitForReload = async () => {
await waitFor(
() => {
expect(screen.queryByTestId('loader')).toBe(null);
expect(screen.getByRole('heading', { name: /marketplace/i })).toBeInTheDocument();
},
{ timeout: 5000 }
);
@ -151,21 +150,21 @@ describe('Marketplace page - plugins tab', () => {
});
it('shows plugins filters popover', () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
userEvent.click(filtersButton);
const collectionsButton = screen.getByRole('button', { name: 'No collections selected' });
const categoriesButton = screen.getByRole('button', { name: 'No categories selected' });
const collectionsButton = screen.getByTestId('Collections-button');
const categoriesButton = screen.getByTestId('Categories-button');
expect(collectionsButton).toBeVisible();
expect(categoriesButton).toBeVisible();
});
it('shows the collections filter options', () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
userEvent.click(filtersButton);
const collectionsButton = screen.getByRole('button', { name: 'No collections selected' });
const collectionsButton = screen.getByTestId('Collections-button');
userEvent.click(collectionsButton);
@ -177,16 +176,16 @@ describe('Marketplace page - plugins tab', () => {
};
Object.entries(mockedServerCollections).forEach(([collectionName, count]) => {
const option = screen.getByRole('option', { name: `${collectionName} (${count})` });
const option = screen.getByTestId(`${collectionName}-${count}`);
expect(option).toBeVisible();
});
});
it('shows the categories filter options', () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
userEvent.click(filtersButton);
const categoriesButton = screen.getByRole('button', { name: 'No categories selected' });
const categoriesButton = screen.getByTestId('Categories-button');
userEvent.click(categoriesButton);
@ -197,16 +196,16 @@ describe('Marketplace page - plugins tab', () => {
};
Object.entries(mockedServerCategories).forEach(([categoryName, count]) => {
const option = screen.getByRole('option', { name: `${categoryName} (${count})` });
const option = screen.getByTestId(`${categoryName}-${count}`);
expect(option).toBeVisible();
});
});
it('filters a collection option', async () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
userEvent.click(filtersButton);
const collectionsButton = screen.getByRole('button', { name: 'No collections selected' });
const collectionsButton = screen.getByTestId('Collections-button');
userEvent.click(collectionsButton);
const option = screen.getByRole('option', { name: `Made by Strapi (13)` });
@ -227,10 +226,10 @@ describe('Marketplace page - plugins tab', () => {
});
it('filters a category option', async () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
userEvent.click(filtersButton);
const categoriesButton = screen.getByRole('button', { name: 'No categories selected' });
const categoriesButton = screen.getByTestId('Categories-button');
userEvent.click(categoriesButton);
const option = screen.getByRole('option', { name: `Custom fields (4)` });
@ -252,9 +251,9 @@ describe('Marketplace page - plugins tab', () => {
it('filters a category and a collection option', async () => {
// When a user clicks the filters button
userEvent.click(screen.getByRole('button', { name: /filters/i }));
userEvent.click(screen.getByTestId('filters-button'));
// They should see a select button for collections with no options selected
const collectionsButton = screen.getByRole('button', { name: 'No collections selected' });
const collectionsButton = screen.getByTestId('Collections-button');
// When they click the select button
userEvent.click(collectionsButton);
// They should see a Made by Strapi option
@ -264,11 +263,11 @@ describe('Marketplace page - plugins tab', () => {
// The page should reload
await waitForReload();
// When they click the filters button again
userEvent.click(screen.getByRole('button', { name: 'Filters' }));
userEvent.click(screen.getByTestId('filters-button'));
// They should see the collections button indicating 1 option selected
userEvent.click(screen.getByRole('button', { name: '1 collection selected Made by Strapi' }));
// They should the categories button with no options selected
const categoriesButton = screen.getByRole('button', { name: 'No categories selected' });
const categoriesButton = screen.getByTestId('Categories-button');
userEvent.click(categoriesButton);
const categoryOption = screen.getByRole('option', { name: `Custom fields (4)` });
userEvent.click(categoryOption);
@ -295,13 +294,13 @@ describe('Marketplace page - plugins tab', () => {
});
it('filters multiple collection options', async () => {
userEvent.click(screen.getByRole('button', { name: /filters/i }));
userEvent.click(screen.getByRole('button', { name: 'No collections selected' }));
userEvent.click(screen.getByTestId('filters-button'));
userEvent.click(screen.getByTestId('Collections-button'));
userEvent.click(screen.getByRole('option', { name: `Made by Strapi (13)` }));
await waitForReload();
userEvent.click(screen.getByRole('button', { name: /filters/i }));
userEvent.click(screen.getByTestId('filters-button'));
userEvent.click(screen.getByRole('button', { name: `1 collection selected Made by Strapi` }));
userEvent.click(screen.getByRole('option', { name: `Verified (29)` }));
@ -318,13 +317,13 @@ describe('Marketplace page - plugins tab', () => {
});
it('filters multiple category options', async () => {
userEvent.click(screen.getByRole('button', { name: /filters/i }));
userEvent.click(screen.getByRole('button', { name: 'No categories selected' }));
userEvent.click(screen.getByTestId('filters-button'));
userEvent.click(screen.getByTestId('Categories-button'));
userEvent.click(screen.getByRole('option', { name: `Custom fields (4)` }));
await waitForReload();
userEvent.click(screen.getByRole('button', { name: /filters/i }));
userEvent.click(screen.getByTestId('filters-button'));
userEvent.click(screen.getByRole('button', { name: `1 category selected Custom fields` }));
userEvent.click(screen.getByRole('option', { name: `Monitoring (1)` }));
@ -341,10 +340,10 @@ describe('Marketplace page - plugins tab', () => {
});
it('removes a filter option tag', async () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
userEvent.click(filtersButton);
const collectionsButton = screen.getByRole('button', { name: 'No collections selected' });
const collectionsButton = screen.getByTestId('Collections-button');
userEvent.click(collectionsButton);
const option = screen.getByRole('option', { name: `Made by Strapi (13)` });
@ -362,10 +361,10 @@ describe('Marketplace page - plugins tab', () => {
});
it('only filters in the plugins tab', async () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
userEvent.click(filtersButton);
const collectionsButton = screen.getByRole('button', { name: 'No collections selected' });
const collectionsButton = screen.getByTestId('Collections-button');
userEvent.click(collectionsButton);
const option = screen.getByRole('option', { name: `Made by Strapi (13)` });

View File

@ -53,7 +53,6 @@ const waitForReload = async () => {
await waitFor(
() => {
expect(screen.queryByTestId('loader')).toBe(null);
expect(screen.getByRole('heading', { name: /marketplace/i })).toBeInTheDocument();
},
{ timeout: 5000 }
);
@ -158,7 +157,7 @@ describe('Marketplace page - providers tab', () => {
});
it('shows providers filters popover', () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
// Only show collections filters on providers
const providersTab = screen.getByRole('tab', { name: /providers/i });
@ -168,10 +167,10 @@ describe('Marketplace page - providers tab', () => {
});
it('shows the collections filter options', () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
userEvent.click(filtersButton);
const collectionsButton = screen.getByRole('button', { name: 'No collections selected' });
const collectionsButton = screen.getByTestId('Collections-button');
userEvent.click(collectionsButton);
@ -183,16 +182,16 @@ describe('Marketplace page - providers tab', () => {
};
Object.entries(mockedServerCollections).forEach(([collectionName, count]) => {
const option = screen.getByRole('option', { name: `${collectionName} (${count})` });
const option = screen.getByTestId(`${collectionName}-${count}`);
expect(option).toBeVisible();
});
});
it('filters a collection option', async () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
userEvent.click(filtersButton);
const collectionsButton = screen.getByRole('button', { name: 'No collections selected' });
const collectionsButton = screen.getByTestId('Collections-button');
userEvent.click(collectionsButton);
const option = screen.getByRole('option', { name: `Made by Strapi (6)` });
@ -213,13 +212,13 @@ describe('Marketplace page - providers tab', () => {
});
it('filters multiple collection options', async () => {
userEvent.click(screen.getByRole('button', { name: /filters/i }));
userEvent.click(screen.getByRole('button', { name: 'No collections selected' }));
userEvent.click(screen.getByTestId('filters-button'));
userEvent.click(screen.getByTestId('Collections-button'));
userEvent.click(screen.getByRole('option', { name: `Made by Strapi (6)` }));
await waitForReload();
userEvent.click(screen.getByRole('button', { name: /filters/i }));
userEvent.click(screen.getByTestId('filters-button'));
userEvent.click(screen.getByRole('button', { name: `1 collection selected Made by Strapi` }));
userEvent.click(screen.getByRole('option', { name: `Verified (6)` }));
@ -236,10 +235,10 @@ describe('Marketplace page - providers tab', () => {
});
it('removes a filter option tag', async () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
userEvent.click(filtersButton);
const collectionsButton = screen.getByRole('button', { name: 'No collections selected' });
const collectionsButton = screen.getByTestId('Collections-button');
userEvent.click(collectionsButton);
const option = screen.getByRole('option', { name: `Made by Strapi (6)` });
@ -257,10 +256,10 @@ describe('Marketplace page - providers tab', () => {
});
it('only filters in the providers tab', async () => {
const filtersButton = screen.getByRole('button', { name: /filters/i });
const filtersButton = screen.getByTestId('filters-button');
userEvent.click(filtersButton);
const collectionsButton = screen.getByRole('button', { name: 'No collections selected' });
const collectionsButton = screen.getByTestId('Collections-button');
userEvent.click(collectionsButton);
const option = screen.getByRole('option', { name: `Made by Strapi (6)` });

View File

@ -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([]);
});
});

View File

@ -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]));
});
});
});

View File

@ -20,6 +20,11 @@ export interface ILocalFileDestinationProviderOptions {
enabled: boolean;
};
// Archive
archive: {
enabled: boolean;
};
// File
file: {
path: string;

View File

@ -38,7 +38,10 @@
},
"dependencies": {
"@strapi/logger": "4.5.0",
"@strapi/strapi": "4.5.0",
"chalk": "4.1.2",
"fs-extra": "10.0.0",
"lodash": "4.17.21",
"prettier": "2.7.1",
"stream-chain": "2.2.5",
"stream-json": "1.7.4",
@ -51,9 +54,8 @@
"@types/stream-chain": "2.0.1",
"@types/stream-json": "1.7.2",
"@types/tar": "6.1.3",
"fs-extra": "10.0.0",
"rimraf": "3.0.2",
"typescript": "4.8.4"
"typescript": "4.6.2"
},
"engines": {
"node": ">=14.19.1 <=18.x.x",

View File

@ -277,7 +277,15 @@ program
.argParser(parseInputBool)
)
.addOption(
new Option('--compress [boolean]', 'Compress output file using gz')
new Option('--compress [boolean]', 'Compress output file using gzip compression')
.default(true)
.argParser(parseInputBool)
)
.addOption(
new Option(
'--archive [boolean]',
'Export all backup files into a single tar archive instead of a folder'
)
.default(true)
.argParser(parseInputBool)
)
@ -310,7 +318,7 @@ program
.addOption(
new Option(
'--schemaComparison <schemaComparison>',
'exact requires every field to match, strict requires Strapi version and schemas to match, subset requires source schema to exist in destination, bypass skips checks',
'exact requires every field to match, strict requires Strapi version and content type schema fields do not break, subset requires source schema to exist in destination, bypass skips checks',
parseInputList
)
.choices(['exact', 'strict', 'subset', 'bypass'])

View File

@ -78,6 +78,9 @@ module.exports = async (filename, opts) => {
compression: {
enabled: opts.compress,
},
archive: {
enabled: opts.archive,
},
};
const destination = createLocalFileDestinationProvider(destinationOptions);

View File

@ -3,7 +3,7 @@ import { Attribute, ConfigurableOption, PrivateOption } from './base';
import { GetAttributesByType, GetAttributesValues } from './utils';
export type BasicRelationsType = 'oneToOne' | 'oneToMany' | 'manyToOne' | 'manyToMany';
export type PolymorphicRelationsType = 'morphToOne' | 'morphToMany' | 'morphOne' | 'morphMany';
export type PolymorphicRelationsType = 'morphToOne' | 'morphToMany' | 'morphOne' | 'morphMany';
export type RelationsType = BasicRelationsType | PolymorphicRelationsType;
export interface BasicRelationAttributeProperties<
@ -17,16 +17,14 @@ export interface BasicRelationAttributeProperties<
mappedBy?: RelationsKeysFromTo<T, S>;
}
export interface PolymorphicRelationAttributeProperties<
R extends RelationsType,
> {
export interface PolymorphicRelationAttributeProperties<R extends RelationsType> {
relation: R;
}
export type RelationAttribute<
S extends SchemaUID,
R extends RelationsType,
T extends R extends PolymorphicRelationsType ? never: SchemaUID = never
T extends R extends PolymorphicRelationsType ? never : SchemaUID = never
> = Attribute<'relation'> &
// Properties
(R extends BasicRelationsType
@ -34,22 +32,21 @@ export type RelationAttribute<
: PolymorphicRelationAttributeProperties<R>) &
// Options
ConfigurableOption &
PrivateOption
PrivateOption;
export type RelationsKeysFromTo<
TTarget extends SchemaUID,
TSource extends SchemaUID
> = keyof PickRelationsFromTo<TTarget, TSource>;
export type PickRelationsFromTo<TTarget extends SchemaUID, TSource extends SchemaUID> = GetAttributesByType<
TTarget,
'relation',
{ target: TSource }
>;
export type PickRelationsFromTo<
TTarget extends SchemaUID,
TSource extends SchemaUID
> = GetAttributesByType<TTarget, 'relation', { target: TSource }>;
export type RelationPluralityModifier<
TRelation extends RelationsType,
TValue extends Object
TValue extends Record<string, unknown>
> = TRelation extends `${string}Many` ? TValue[] : TValue;
export type RelationValue<

View File

@ -1,8 +1,8 @@
import { Service,GenericService } from '../core-api/service';
import { Service, GenericService } from '../core-api/service';
import { Controller, GenericController } from '../core-api/controller';
import { Middleware } from '../middlewares';
import { Policy } from '../core/registries/policies';
import { Strapi } from '@strapi/strapi';
import { Strapi } from './core/strapi';
type ControllerConfig<T extends Controller = Controller> = T;

View File

@ -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()

View File

@ -6505,7 +6505,7 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^5.14.0":
"@typescript-eslint/eslint-plugin@5.43.0", "@typescript-eslint/eslint-plugin@^5.14.0":
version "5.43.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.43.0.tgz#4a5248eb31b454715ddfbf8cfbf497529a0a78bc"
integrity sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA==
@ -6520,6 +6520,16 @@
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/parser@5.42.1":
version "5.42.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.42.1.tgz#3e66156f2f74b11690b45950d8f5f28a62751d35"
integrity sha512-kAV+NiNBWVQDY9gDJDToTE/NO8BHi4f6b7zTsVAJoTkmB/zlfOpiEVBzHOKtlgTndCKe8vj9F/PuolemZSh50Q==
dependencies:
"@typescript-eslint/scope-manager" "5.42.1"
"@typescript-eslint/types" "5.42.1"
"@typescript-eslint/typescript-estree" "5.42.1"
debug "^4.3.4"
"@typescript-eslint/parser@^5.14.0":
version "5.43.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.43.0.tgz#9c86581234b88f2ba406f0b99a274a91c11630fd"
@ -6530,6 +6540,14 @@
"@typescript-eslint/typescript-estree" "5.43.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@5.42.1":
version "5.42.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.42.1.tgz#05e5e1351485637d466464237e5259b49f609b18"
integrity sha512-QAZY/CBP1Emx4rzxurgqj3rUinfsh/6mvuKbLNMfJMMKYLRBfweus8brgXF8f64ABkIZ3zdj2/rYYtF8eiuksQ==
dependencies:
"@typescript-eslint/types" "5.42.1"
"@typescript-eslint/visitor-keys" "5.42.1"
"@typescript-eslint/scope-manager@5.43.0":
version "5.43.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.43.0.tgz#566e46303392014d5d163704724872e1f2dd3c15"
@ -6548,11 +6566,29 @@
debug "^4.3.4"
tsutils "^3.21.0"
"@typescript-eslint/types@5.42.1":
version "5.42.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.42.1.tgz#0d4283c30e9b70d2aa2391c36294413de9106df2"
integrity sha512-Qrco9dsFF5lhalz+lLFtxs3ui1/YfC6NdXu+RAGBa8uSfn01cjO7ssCsjIsUs484vny9Xm699FSKwpkCcqwWwA==
"@typescript-eslint/types@5.43.0":
version "5.43.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.43.0.tgz#e4ddd7846fcbc074325293515fa98e844d8d2578"
integrity sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg==
"@typescript-eslint/typescript-estree@5.42.1":
version "5.42.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.1.tgz#f9a223ecb547a781d37e07a5ac6ba9ff681eaef0"
integrity sha512-qElc0bDOuO0B8wDhhW4mYVgi/LZL+igPwXtV87n69/kYC/7NG3MES0jHxJNCr4EP7kY1XVsRy8C/u3DYeTKQmw==
dependencies:
"@typescript-eslint/types" "5.42.1"
"@typescript-eslint/visitor-keys" "5.42.1"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@5.43.0":
version "5.43.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz#b6883e58ba236a602c334be116bfc00b58b3b9f2"
@ -6580,6 +6616,14 @@
eslint-utils "^3.0.0"
semver "^7.3.7"
"@typescript-eslint/visitor-keys@5.42.1":
version "5.42.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.1.tgz#df10839adf6605e1cdb79174cf21e46df9be4872"
integrity sha512-LOQtSF4z+hejmpUvitPlc4hA7ERGoj2BVkesOcG91HCn8edLGUXbTrErmutmPbl8Bo9HjAvOO/zBKQHExXNA2A==
dependencies:
"@typescript-eslint/types" "5.42.1"
eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@5.43.0":
version "5.43.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.43.0.tgz#cbbdadfdfea385310a20a962afda728ea106befa"
@ -11072,7 +11116,7 @@ eslint-config-airbnb@^19.0.4:
object.assign "^4.1.2"
object.entries "^1.1.5"
eslint-config-prettier@^8.5.0:
eslint-config-prettier@8.5.0, eslint-config-prettier@^8.5.0:
version "8.5.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1"
integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==
@ -11100,7 +11144,7 @@ eslint-plugin-es@^3.0.0:
eslint-utils "^2.0.0"
regexpp "^3.0.0"
eslint-plugin-import@^2.25.4:
eslint-plugin-import@2.26.0, eslint-plugin-import@^2.25.4:
version "2.26.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b"
integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==
@ -11119,7 +11163,7 @@ eslint-plugin-import@^2.25.4:
resolve "^1.22.0"
tsconfig-paths "^3.14.1"
eslint-plugin-jsx-a11y@^6.5.1:
eslint-plugin-jsx-a11y@6.6.1, eslint-plugin-jsx-a11y@^6.5.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz#93736fc91b83fdc38cc8d115deedfc3091aef1ff"
integrity sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==
@ -11138,7 +11182,7 @@ eslint-plugin-jsx-a11y@^6.5.1:
minimatch "^3.1.2"
semver "^6.3.0"
eslint-plugin-node@^11.1.0:
eslint-plugin-node@11.1.0, eslint-plugin-node@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==
@ -11150,19 +11194,19 @@ eslint-plugin-node@^11.1.0:
resolve "^1.10.1"
semver "^6.1.0"
eslint-plugin-prettier@^4.0.0:
eslint-plugin-prettier@4.2.1, eslint-plugin-prettier@^4.0.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
dependencies:
prettier-linter-helpers "^1.0.0"
eslint-plugin-react-hooks@^4.3.0:
eslint-plugin-react-hooks@4.6.0, eslint-plugin-react-hooks@^4.3.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3"
integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==
eslint-plugin-react@^7.29.3:
eslint-plugin-react@7.31.10, eslint-plugin-react@^7.29.3:
version "7.31.10"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.10.tgz#6782c2c7fe91c09e715d536067644bbb9491419a"
integrity sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==
@ -11182,6 +11226,13 @@ eslint-plugin-react@^7.29.3:
semver "^6.3.0"
string.prototype.matchall "^4.0.7"
eslint-plugin@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin/-/eslint-plugin-1.0.1.tgz#24bf3136d167c90ed0d4d5702d8b9b1ed2beef79"
integrity sha512-ervp8C09On0fLA258TvE08AqAr/bhRYgHVZd3BrJjD4JfOA2JGANDLGs06j51oWqfPd7Feoo3OoqHD+fuI2sFQ==
dependencies:
requireindex "~1.1.0"
eslint-scope@5.1.1, eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@ -19891,6 +19942,11 @@ require-from-string@2.0.2, require-from-string@^2.0.2:
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
requireindex@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.1.0.tgz#e5404b81557ef75db6e49c5a72004893fe03e162"
integrity sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
@ -22059,11 +22115,6 @@ typescript@4.6.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4"
integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==
typescript@4.8.4:
version "4.8.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
typescript@^4.6.2:
version "4.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db"