mirror of
https://github.com/strapi/strapi.git
synced 2025-11-13 16:52:18 +00:00
Merge pull request #14014 from strapi/relations-main-view/find-new-relations
refactor relation findNew route
This commit is contained in:
commit
29376f8113
@ -1,238 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const createContext = require('../../../../../../test/helpers/create-context');
|
|
||||||
const relations = require('../relations');
|
|
||||||
|
|
||||||
describe('Relations', () => {
|
|
||||||
describe('find', () => {
|
|
||||||
test('Fails on model not found', async () => {
|
|
||||||
const notFound = jest.fn();
|
|
||||||
const ctx = createContext(
|
|
||||||
{
|
|
||||||
params: { model: 'test', targetField: 'field' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
notFound,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const getModel = jest.fn();
|
|
||||||
global.strapi = {
|
|
||||||
getModel,
|
|
||||||
plugins: {
|
|
||||||
'content-manager': {
|
|
||||||
services: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await relations.find(ctx);
|
|
||||||
|
|
||||||
expect(notFound).toHaveBeenCalledWith('model.notFound');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Fails on invalid target field', async () => {
|
|
||||||
const badRequest = jest.fn();
|
|
||||||
const ctx = createContext(
|
|
||||||
{
|
|
||||||
params: { model: 'test', targetField: 'field' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
badRequest,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const getModel = jest.fn(() => ({
|
|
||||||
attributes: {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
global.strapi = {
|
|
||||||
getModel,
|
|
||||||
plugins: {
|
|
||||||
'content-manager': {
|
|
||||||
services: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await relations.find(ctx);
|
|
||||||
|
|
||||||
expect(badRequest).toHaveBeenCalledWith('targetField.invalid');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Fails on model not found', async () => {
|
|
||||||
const notFound = jest.fn();
|
|
||||||
const ctx = createContext(
|
|
||||||
{
|
|
||||||
params: { model: 'test', targetField: 'target' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
notFound,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const getModel = jest
|
|
||||||
.fn()
|
|
||||||
.mockReturnValueOnce({
|
|
||||||
attributes: { target: { type: 'relation', target: 'test' } },
|
|
||||||
})
|
|
||||||
.mockReturnValueOnce(null);
|
|
||||||
|
|
||||||
global.strapi = {
|
|
||||||
getModel,
|
|
||||||
plugins: {
|
|
||||||
'content-manager': {
|
|
||||||
services: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await relations.find(ctx);
|
|
||||||
|
|
||||||
expect(notFound).toHaveBeenCalledWith('target.notFound');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Picks the mainField and id only', async () => {
|
|
||||||
const notFound = jest.fn();
|
|
||||||
const ctx = createContext(
|
|
||||||
{
|
|
||||||
params: { model: 'test', targetField: 'target' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
notFound,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const getModel = jest.fn(() => ({
|
|
||||||
attributes: { target: { type: 'relation', target: 'test' } },
|
|
||||||
}));
|
|
||||||
|
|
||||||
global.strapi = {
|
|
||||||
getModel,
|
|
||||||
plugins: {
|
|
||||||
'content-manager': {
|
|
||||||
services: {
|
|
||||||
'content-types': {
|
|
||||||
findConfiguration() {
|
|
||||||
return {
|
|
||||||
metadatas: {
|
|
||||||
target: {
|
|
||||||
edit: {
|
|
||||||
mainField: 'title',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'entity-manager': {
|
|
||||||
find() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'title1',
|
|
||||||
secret: 'some secret',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'title2',
|
|
||||||
secret: 'some secret 2',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await relations.find(ctx);
|
|
||||||
|
|
||||||
expect(ctx.body).toEqual([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'title1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'title2',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Omit somes ids', async () => {
|
|
||||||
const result = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'title1',
|
|
||||||
secret: 'some secret',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'title2',
|
|
||||||
secret: 'some secret 2',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const configuration = {
|
|
||||||
metadatas: {
|
|
||||||
target: {
|
|
||||||
edit: {
|
|
||||||
mainField: 'title',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const assocModel = { uid: 'api::test.test', attributes: {} };
|
|
||||||
const notFound = jest.fn();
|
|
||||||
const find = jest.fn(() => Promise.resolve(result));
|
|
||||||
const findConfiguration = jest.fn(() => Promise.resolve(configuration));
|
|
||||||
|
|
||||||
const getModel = jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => ({
|
|
||||||
attributes: { target: { type: 'relation', target: 'test' } },
|
|
||||||
}))
|
|
||||||
.mockImplementationOnce(() => assocModel);
|
|
||||||
|
|
||||||
global.strapi = {
|
|
||||||
getModel,
|
|
||||||
plugins: {
|
|
||||||
'content-manager': {
|
|
||||||
services: {
|
|
||||||
'content-types': { findConfiguration },
|
|
||||||
'entity-manager': { find },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ctx = createContext(
|
|
||||||
{
|
|
||||||
params: { model: 'test', targetField: 'target' },
|
|
||||||
body: { idsToOmit: [3, 4] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
notFound,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await relations.find(ctx);
|
|
||||||
|
|
||||||
expect(find).toHaveBeenCalledWith(
|
|
||||||
{ filters: { $and: [{ id: { $notIn: [3, 4] } }] } },
|
|
||||||
assocModel.uid,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
expect(ctx.body).toEqual([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'title1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'title2',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,64 +1,109 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { prop, pick } = require('lodash/fp');
|
const { prop, isEmpty } = require('lodash/fp');
|
||||||
|
const { hasDraftAndPublish } = require('@strapi/utils').contentTypes;
|
||||||
const { PUBLISHED_AT_ATTRIBUTE } = require('@strapi/utils').contentTypes.constants;
|
const { PUBLISHED_AT_ATTRIBUTE } = require('@strapi/utils').contentTypes.constants;
|
||||||
|
const { transformParamsToQuery } = require('@strapi/utils').convertQueryParams;
|
||||||
|
|
||||||
const { getService } = require('../utils');
|
const { getService } = require('../utils');
|
||||||
|
const { validateFindAvailable } = require('./validation/relations');
|
||||||
|
|
||||||
|
const addWhereClause = (params, whereClause) => {
|
||||||
|
params.where = params.where || {};
|
||||||
|
if (params.where.$and) {
|
||||||
|
params.where.$and.push(whereClause);
|
||||||
|
} else {
|
||||||
|
params.where.$and = [whereClause];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
async find(ctx) {
|
async findAvailable(ctx) {
|
||||||
|
const { userAbility } = ctx.state;
|
||||||
const { model, targetField } = ctx.params;
|
const { model, targetField } = ctx.params;
|
||||||
const { _component, ...query } = ctx.request.query;
|
|
||||||
const { idsToOmit } = ctx.request.body;
|
|
||||||
|
|
||||||
if (!targetField) {
|
await validateFindAvailable(ctx.request.query);
|
||||||
return ctx.badRequest();
|
|
||||||
|
const { component, entityId, idsToOmit, _q, ...query } = ctx.request.query;
|
||||||
|
|
||||||
|
const sourceModelUid = component || model;
|
||||||
|
|
||||||
|
const sourceModel = strapi.getModel(sourceModelUid);
|
||||||
|
if (!sourceModel) {
|
||||||
|
return ctx.badRequest("The model doesn't exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelDef = _component ? strapi.getModel(_component) : strapi.getModel(model);
|
// permission check
|
||||||
|
if (entityId) {
|
||||||
|
const entityManager = getService('entity-manager');
|
||||||
|
const permissionChecker = getService('permission-checker').create({
|
||||||
|
userAbility,
|
||||||
|
model,
|
||||||
|
});
|
||||||
|
|
||||||
if (!modelDef) {
|
if (permissionChecker.cannot.read()) {
|
||||||
return ctx.notFound('model.notFound');
|
return ctx.forbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
const entity = await entityManager.findOneWithCreatorRoles(entityId, model);
|
||||||
|
|
||||||
|
if (!entity) {
|
||||||
|
return ctx.notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissionChecker.cannot.read(entity)) {
|
||||||
|
return ctx.forbidden();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const attribute = modelDef.attributes[targetField];
|
const attribute = sourceModel.attributes[targetField];
|
||||||
if (!attribute || attribute.type !== 'relation') {
|
if (!attribute || attribute.type !== 'relation') {
|
||||||
return ctx.badRequest('targetField.invalid');
|
return ctx.badRequest("This relational field doesn't exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = strapi.getModel(attribute.target);
|
const targetedModel = strapi.getModel(attribute.target);
|
||||||
|
|
||||||
if (!target) {
|
const modelConfig = component
|
||||||
return ctx.notFound('target.notFound');
|
? await getService('components').findConfiguration(sourceModel)
|
||||||
|
: await getService('content-types').findConfiguration(sourceModel);
|
||||||
|
const mainField = prop(`metadatas.${targetField}.edit.mainField`, modelConfig) || 'id';
|
||||||
|
|
||||||
|
const fieldsToSelect = ['id', mainField];
|
||||||
|
if (hasDraftAndPublish(targetedModel)) {
|
||||||
|
fieldsToSelect.push(PUBLISHED_AT_ATTRIBUTE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (idsToOmit && Array.isArray(idsToOmit)) {
|
// TODO: for RBAC reasons, find a way to exclude filters that should not be there
|
||||||
query.filters = {
|
// i.e. all filters except locale for i18n
|
||||||
$and: [
|
const queryParams = {
|
||||||
{
|
orderBy: mainField,
|
||||||
id: {
|
...transformParamsToQuery(targetedModel.uid, query),
|
||||||
$notIn: idsToOmit,
|
select: fieldsToSelect, // cannot select other fields as the user may not have the permissions
|
||||||
},
|
};
|
||||||
},
|
|
||||||
].concat(query.filters || []),
|
if (!isEmpty(idsToOmit)) {
|
||||||
};
|
addWhereClause(queryParams, { id: { $notIn: idsToOmit } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityManager = getService('entity-manager');
|
// searching should be allowed only on mainField for permission reasons
|
||||||
|
if (_q) {
|
||||||
const entities = await entityManager.find(query, target.uid, []);
|
addWhereClause(queryParams, { [mainField]: { $containsi: _q } });
|
||||||
|
|
||||||
if (!entities) {
|
|
||||||
return ctx.notFound();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelConfig = _component
|
if (entityId) {
|
||||||
? await getService('components').findConfiguration(modelDef)
|
const subQuery = strapi.db.queryBuilder(sourceModel.uid);
|
||||||
: await getService('content-types').findConfiguration(modelDef);
|
|
||||||
|
|
||||||
const field = prop(`metadatas.${targetField}.edit.mainField`, modelConfig) || 'id';
|
const alias = subQuery.getAlias();
|
||||||
const pickFields = [field, 'id', target.primaryKey, PUBLISHED_AT_ATTRIBUTE];
|
|
||||||
|
|
||||||
ctx.body = entities.map(pick(pickFields));
|
const knexSubQuery = subQuery
|
||||||
|
.where({ id: entityId })
|
||||||
|
.join({ alias, targetField })
|
||||||
|
.select(`${alias}.id`)
|
||||||
|
.getKnexQuery();
|
||||||
|
|
||||||
|
addWhereClause(queryParams, { id: { $notIn: knexSubQuery } });
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = await strapi.query(targetedModel.uid).findPage(queryParams);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { yup, validateYupSchema } = require('@strapi/utils');
|
||||||
|
|
||||||
|
const validateFindAvailableSchema = yup
|
||||||
|
.object()
|
||||||
|
.shape({
|
||||||
|
component: yup.string(),
|
||||||
|
entityId: yup.strapiID(),
|
||||||
|
_q: yup.string(),
|
||||||
|
idsToOmit: yup.array().of(yup.strapiID()),
|
||||||
|
page: yup.number().integer().min(1),
|
||||||
|
pageSize: yup.number().integer().min(1).max(100),
|
||||||
|
})
|
||||||
|
.noUnknown()
|
||||||
|
.required();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateFindAvailable: validateYupSchema(validateFindAvailableSchema, { strict: false }),
|
||||||
|
};
|
||||||
@ -80,9 +80,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'GET',
|
||||||
path: '/relations/:model/:targetField',
|
path: '/relations/:model/:targetField',
|
||||||
handler: 'relations.find',
|
handler: 'relations.findAvailable',
|
||||||
config: {
|
config: {
|
||||||
policies: [
|
policies: [
|
||||||
'admin::isAuthenticatedAdmin',
|
'admin::isAuthenticatedAdmin',
|
||||||
|
|||||||
@ -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]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
280
packages/core/content-manager/server/tests/relations.test.e2e.js
Normal file
280
packages/core/content-manager/server/tests/relations.test.e2e.js
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
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 compo = {
|
||||||
|
displayName: 'compo',
|
||||||
|
attributes: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
compoProducts: {
|
||||||
|
type: 'relation',
|
||||||
|
relation: 'manyToMany',
|
||||||
|
target: 'api::product.product',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const productModel = (draftAndPublish = false) => ({
|
||||||
|
draftAndPublish,
|
||||||
|
attributes: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
displayName: 'Product',
|
||||||
|
singularName: 'product',
|
||||||
|
pluralName: 'products',
|
||||||
|
description: '',
|
||||||
|
collectionName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const shopModel = (draftAndPublish = false) => ({
|
||||||
|
draftAndPublish,
|
||||||
|
attributes: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
type: 'relation',
|
||||||
|
relation: 'manyToMany',
|
||||||
|
target: 'api::product.product',
|
||||||
|
targetAttribute: 'shops',
|
||||||
|
inversedBy: 'shops',
|
||||||
|
},
|
||||||
|
myCompo: {
|
||||||
|
type: 'component',
|
||||||
|
repeatable: false,
|
||||||
|
component: 'default.compo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
displayName: 'Shop',
|
||||||
|
singularName: 'shop',
|
||||||
|
pluralName: 'shops',
|
||||||
|
});
|
||||||
|
|
||||||
|
const createEntry = async (uid, data) => {
|
||||||
|
const { body } = await rq({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/content-manager/collection-types/${uid}`,
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
|
return body;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe.each([false, true])('Relations, with d&p: %s', (withDraftAndPublish) => {
|
||||||
|
const builder = createTestBuilder();
|
||||||
|
const addPublishedAtCheck = (value) => (withDraftAndPublish ? { publishedAt: value } : undefined);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await builder
|
||||||
|
.addContentTypes([productModel(withDraftAndPublish)])
|
||||||
|
.addComponent(compo)
|
||||||
|
.addContentTypes([shopModel(withDraftAndPublish)])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
strapi = await createStrapiInstance();
|
||||||
|
rq = await createAuthRequest({ strapi });
|
||||||
|
|
||||||
|
const createdProduct1 = await createEntry('api::product.product', { name: 'Skate' });
|
||||||
|
const createdProduct2 = await createEntry('api::product.product', { name: 'Candle' });
|
||||||
|
|
||||||
|
if (withDraftAndPublish) {
|
||||||
|
await rq({
|
||||||
|
url: `/content-manager/collection-types/api::product.product/${createdProduct1.id}/actions/publish`,
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
data.products.push(createdProduct1);
|
||||||
|
data.products.push(createdProduct2);
|
||||||
|
|
||||||
|
const createdShop = await createEntry('api::shop.shop', {
|
||||||
|
name: 'Cazotte Shop',
|
||||||
|
products: [createdProduct1.id],
|
||||||
|
myCompo: { compoProducts: [createdProduct2.id] },
|
||||||
|
});
|
||||||
|
|
||||||
|
data.shops.push(createdShop);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await strapi.destroy();
|
||||||
|
await builder.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findAvailable', () => {
|
||||||
|
test('relation not in a component && no entity', async () => {
|
||||||
|
let res = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/content-manager/relations/api::shop.shop/products',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.results).toMatchObject([
|
||||||
|
{
|
||||||
|
id: expect.any(Number),
|
||||||
|
name: 'Candle',
|
||||||
|
...addPublishedAtCheck(null),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: expect.any(Number),
|
||||||
|
name: 'Skate',
|
||||||
|
...addPublishedAtCheck(expect.any(String)),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// can omitIds
|
||||||
|
res = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/content-manager/relations/api::shop.shop/products',
|
||||||
|
qs: {
|
||||||
|
idsToOmit: [data.products[0].id],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.body.results).toMatchObject([
|
||||||
|
{
|
||||||
|
id: expect.any(Number),
|
||||||
|
name: 'Candle',
|
||||||
|
...addPublishedAtCheck(null),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('relation not in a component && on an entity', async () => {
|
||||||
|
let res = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/content-manager/relations/api::shop.shop/products',
|
||||||
|
qs: {
|
||||||
|
entityId: data.shops[0].id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.results).toMatchObject([
|
||||||
|
{
|
||||||
|
id: expect.any(Number),
|
||||||
|
name: 'Candle',
|
||||||
|
...addPublishedAtCheck(null),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// can omitIds
|
||||||
|
res = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/content-manager/relations/api::shop.shop/products',
|
||||||
|
qs: {
|
||||||
|
entityId: data.shops[0].id,
|
||||||
|
idsToOmit: [data.products[1].id],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.body.results).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('relation in a component && no entity', async () => {
|
||||||
|
let res = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/content-manager/relations/api::shop.shop/compoProducts',
|
||||||
|
qs: {
|
||||||
|
component: 'default.compo',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.results).toMatchObject([
|
||||||
|
{
|
||||||
|
id: expect.any(Number),
|
||||||
|
name: 'Candle',
|
||||||
|
...addPublishedAtCheck(null),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: expect.any(Number),
|
||||||
|
name: 'Skate',
|
||||||
|
...addPublishedAtCheck(expect.any(String)),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// can omitIds
|
||||||
|
res = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/content-manager/relations/api::shop.shop/compoProducts',
|
||||||
|
qs: {
|
||||||
|
component: 'default.compo',
|
||||||
|
idsToOmit: [data.products[0].id],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.body.results).toMatchObject([
|
||||||
|
{
|
||||||
|
id: expect.any(Number),
|
||||||
|
name: 'Candle',
|
||||||
|
...addPublishedAtCheck(null),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('relation in a component && on an entity', async () => {
|
||||||
|
let res = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/content-manager/relations/api::shop.shop/compoProducts',
|
||||||
|
qs: {
|
||||||
|
entityId: data.shops[0].myCompo.id,
|
||||||
|
component: 'default.compo',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.results).toMatchObject([
|
||||||
|
{
|
||||||
|
id: expect.any(Number),
|
||||||
|
name: 'Skate',
|
||||||
|
...addPublishedAtCheck(expect.any(String)),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// can omitIds
|
||||||
|
res = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/content-manager/relations/api::shop.shop/compoProducts',
|
||||||
|
qs: {
|
||||||
|
entityId: data.shops[0].myCompo.id,
|
||||||
|
component: 'default.compo',
|
||||||
|
idsToOmit: [data.products[0].id],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.body.results).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can search', async () => {
|
||||||
|
const res = await rq({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/content-manager/relations/api::shop.shop/products',
|
||||||
|
qs: {
|
||||||
|
_q: 'Can',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.results).toMatchObject([
|
||||||
|
{
|
||||||
|
id: expect.any(Number),
|
||||||
|
name: 'Candle',
|
||||||
|
...addPublishedAtCheck(null),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const createPivotJoin = (qb, joinTable, alias, tragetMeta) => {
|
const createPivotJoin = (ctx, { alias, refAlias, joinTable, targetMeta }) => {
|
||||||
|
const { qb } = ctx;
|
||||||
const joinAlias = qb.getAlias();
|
const joinAlias = qb.getAlias();
|
||||||
qb.join({
|
qb.join({
|
||||||
alias: joinAlias,
|
alias: joinAlias,
|
||||||
@ -11,10 +12,10 @@ const createPivotJoin = (qb, joinTable, alias, tragetMeta) => {
|
|||||||
on: joinTable.on,
|
on: joinTable.on,
|
||||||
});
|
});
|
||||||
|
|
||||||
const subAlias = qb.getAlias();
|
const subAlias = refAlias || qb.getAlias();
|
||||||
qb.join({
|
qb.join({
|
||||||
alias: subAlias,
|
alias: subAlias,
|
||||||
referencedTable: tragetMeta.tableName,
|
referencedTable: targetMeta.tableName,
|
||||||
referencedColumn: joinTable.inverseJoinColumn.referencedColumn,
|
referencedColumn: joinTable.inverseJoinColumn.referencedColumn,
|
||||||
rootColumn: joinTable.inverseJoinColumn.name,
|
rootColumn: joinTable.inverseJoinColumn.name,
|
||||||
rootTable: joinAlias,
|
rootTable: joinAlias,
|
||||||
@ -23,22 +24,22 @@ const createPivotJoin = (qb, joinTable, alias, tragetMeta) => {
|
|||||||
return subAlias;
|
return subAlias;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createJoin = (ctx, { alias, attributeName, attribute }) => {
|
const createJoin = (ctx, { alias, refAlias, attributeName, attribute }) => {
|
||||||
const { db, qb } = ctx;
|
const { db, qb } = ctx;
|
||||||
|
|
||||||
if (attribute.type !== 'relation') {
|
if (attribute.type !== 'relation') {
|
||||||
throw new Error(`Cannot join on non relational field ${attributeName}`);
|
throw new Error(`Cannot join on non relational field ${attributeName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tragetMeta = db.metadata.get(attribute.target);
|
const targetMeta = db.metadata.get(attribute.target);
|
||||||
|
|
||||||
const { joinColumn } = attribute;
|
const { joinColumn } = attribute;
|
||||||
|
|
||||||
if (joinColumn) {
|
if (joinColumn) {
|
||||||
const subAlias = qb.getAlias();
|
const subAlias = refAlias || qb.getAlias();
|
||||||
qb.join({
|
qb.join({
|
||||||
alias: subAlias,
|
alias: subAlias,
|
||||||
referencedTable: tragetMeta.tableName,
|
referencedTable: targetMeta.tableName,
|
||||||
referencedColumn: joinColumn.referencedColumn,
|
referencedColumn: joinColumn.referencedColumn,
|
||||||
rootColumn: joinColumn.name,
|
rootColumn: joinColumn.name,
|
||||||
rootTable: alias,
|
rootTable: alias,
|
||||||
@ -48,7 +49,7 @@ const createJoin = (ctx, { alias, attributeName, attribute }) => {
|
|||||||
|
|
||||||
const { joinTable } = attribute;
|
const { joinTable } = attribute;
|
||||||
if (joinTable) {
|
if (joinTable) {
|
||||||
return createPivotJoin(qb, joinTable, alias, tragetMeta);
|
return createPivotJoin(ctx, { alias, refAlias, joinTable, targetMeta });
|
||||||
}
|
}
|
||||||
|
|
||||||
return alias;
|
return alias;
|
||||||
|
|||||||
@ -4,36 +4,43 @@ const _ = require('lodash/fp');
|
|||||||
|
|
||||||
const helpers = require('./helpers');
|
const helpers = require('./helpers');
|
||||||
|
|
||||||
const createQueryBuilder = (uid, db) => {
|
const createQueryBuilder = (uid, db, initialState = {}) => {
|
||||||
const meta = db.metadata.get(uid);
|
const meta = db.metadata.get(uid);
|
||||||
const { tableName } = meta;
|
const { tableName } = meta;
|
||||||
|
|
||||||
const state = {
|
const state = _.defaults(
|
||||||
type: 'select',
|
{
|
||||||
select: [],
|
type: 'select',
|
||||||
count: null,
|
select: [],
|
||||||
max: null,
|
count: null,
|
||||||
first: false,
|
max: null,
|
||||||
data: null,
|
first: false,
|
||||||
where: [],
|
data: null,
|
||||||
joins: [],
|
where: [],
|
||||||
populate: null,
|
joins: [],
|
||||||
limit: null,
|
populate: null,
|
||||||
offset: null,
|
limit: null,
|
||||||
transaction: null,
|
offset: null,
|
||||||
forUpdate: false,
|
transaction: null,
|
||||||
orderBy: [],
|
forUpdate: false,
|
||||||
groupBy: [],
|
orderBy: [],
|
||||||
};
|
groupBy: [],
|
||||||
|
aliasCounter: 0,
|
||||||
|
},
|
||||||
|
initialState
|
||||||
|
);
|
||||||
|
|
||||||
let counter = 0;
|
const getAlias = () => `t${state.aliasCounter++}`;
|
||||||
const getAlias = () => `t${counter++}`;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
alias: getAlias(),
|
alias: getAlias(),
|
||||||
getAlias,
|
getAlias,
|
||||||
state,
|
state,
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return createQueryBuilder(uid, db, state);
|
||||||
|
},
|
||||||
|
|
||||||
select(args) {
|
select(args) {
|
||||||
state.type = 'select';
|
state.type = 'select';
|
||||||
state.select = _.uniq(_.castArray(args));
|
state.select = _.uniq(_.castArray(args));
|
||||||
@ -189,7 +196,24 @@ const createQueryBuilder = (uid, db) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
join(join) {
|
join(join) {
|
||||||
state.joins.push(join);
|
if (!join.targetField) {
|
||||||
|
state.joins.push(join);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = db.metadata.get(uid);
|
||||||
|
const attribute = model.attributes[join.targetField];
|
||||||
|
|
||||||
|
helpers.createJoin(
|
||||||
|
{ db, qb: this },
|
||||||
|
{
|
||||||
|
alias: this.alias,
|
||||||
|
refAlias: join.alias,
|
||||||
|
attributeName: join.targetField,
|
||||||
|
attribute,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ const {
|
|||||||
sanitize,
|
sanitize,
|
||||||
} = require('@strapi/utils');
|
} = require('@strapi/utils');
|
||||||
const { ValidationError } = require('@strapi/utils').errors;
|
const { ValidationError } = require('@strapi/utils').errors;
|
||||||
|
const { transformParamsToQuery } = require('@strapi/utils').convertQueryParams;
|
||||||
const uploadFiles = require('../utils/upload-files');
|
const uploadFiles = require('../utils/upload-files');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -18,7 +19,7 @@ const {
|
|||||||
updateComponents,
|
updateComponents,
|
||||||
deleteComponents,
|
deleteComponents,
|
||||||
} = require('./components');
|
} = require('./components');
|
||||||
const { transformParamsToQuery, pickSelectionParams } = require('./params');
|
const { pickSelectionParams } = require('./params');
|
||||||
const { applyTransforms } = require('./attributes');
|
const { applyTransforms } = require('./attributes');
|
||||||
|
|
||||||
// TODO: those should be strapi events used by the webhooks not the other way arround
|
// TODO: those should be strapi events used by the webhooks not the other way arround
|
||||||
|
|||||||
@ -1,95 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { pick, isNil, toNumber, isInteger } = require('lodash/fp');
|
const { pick } = require('lodash/fp');
|
||||||
const { PaginationError } = require('@strapi/utils').errors;
|
|
||||||
|
|
||||||
const {
|
|
||||||
convertSortQueryParams,
|
|
||||||
convertLimitQueryParams,
|
|
||||||
convertStartQueryParams,
|
|
||||||
convertPopulateQueryParams,
|
|
||||||
convertFiltersQueryParams,
|
|
||||||
convertFieldsQueryParams,
|
|
||||||
convertPublicationStateParams,
|
|
||||||
} = require('@strapi/utils/lib/convert-query-params');
|
|
||||||
|
|
||||||
const pickSelectionParams = pick(['fields', 'populate']);
|
const pickSelectionParams = pick(['fields', 'populate']);
|
||||||
|
|
||||||
const transformParamsToQuery = (uid, params) => {
|
|
||||||
// NOTE: can be a CT, a Compo or nothing in the case of polymorphism (DZ & morph relations)
|
|
||||||
const schema = strapi.getModel(uid);
|
|
||||||
|
|
||||||
const query = {};
|
|
||||||
|
|
||||||
const { _q, sort, filters, fields, populate, page, pageSize, start, limit } = params;
|
|
||||||
|
|
||||||
if (!isNil(_q)) {
|
|
||||||
query._q = _q;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNil(sort)) {
|
|
||||||
query.orderBy = convertSortQueryParams(sort);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNil(filters)) {
|
|
||||||
query.where = convertFiltersQueryParams(filters, schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNil(fields)) {
|
|
||||||
query.select = convertFieldsQueryParams(fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNil(populate)) {
|
|
||||||
query.populate = convertPopulateQueryParams(populate, schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPagePagination = !isNil(page) || !isNil(pageSize);
|
|
||||||
const isOffsetPagination = !isNil(start) || !isNil(limit);
|
|
||||||
|
|
||||||
if (isPagePagination && isOffsetPagination) {
|
|
||||||
throw new PaginationError(
|
|
||||||
'Invalid pagination attributes. You cannot use page and offset pagination in the same query'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNil(page)) {
|
|
||||||
const pageVal = toNumber(page);
|
|
||||||
|
|
||||||
if (!isInteger(pageVal) || pageVal <= 0) {
|
|
||||||
throw new PaginationError(
|
|
||||||
`Invalid 'page' parameter. Expected an integer > 0, received: ${page}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
query.page = pageVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNil(pageSize)) {
|
|
||||||
const pageSizeVal = toNumber(pageSize);
|
|
||||||
|
|
||||||
if (!isInteger(pageSizeVal) || pageSizeVal <= 0) {
|
|
||||||
throw new PaginationError(
|
|
||||||
`Invalid 'pageSize' parameter. Expected an integer > 0, received: ${page}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
query.pageSize = pageSizeVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNil(start)) {
|
|
||||||
query.offset = convertStartQueryParams(start);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNil(limit)) {
|
|
||||||
query.limit = convertLimitQueryParams(limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
convertPublicationStateParams(schema, params, query);
|
|
||||||
|
|
||||||
return query;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
transformParamsToQuery,
|
|
||||||
pickSelectionParams,
|
pickSelectionParams,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,10 +6,22 @@
|
|||||||
* Converts the standard Strapi REST query params to a more usable format for querying
|
* Converts the standard Strapi REST query params to a more usable format for querying
|
||||||
* You can read more here: https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest-api.html#filters
|
* You can read more here: https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest-api.html#filters
|
||||||
*/
|
*/
|
||||||
const { has, isEmpty, isObject, isPlainObject, cloneDeep, get, mergeAll } = require('lodash/fp');
|
const {
|
||||||
|
has,
|
||||||
|
isEmpty,
|
||||||
|
isObject,
|
||||||
|
isPlainObject,
|
||||||
|
cloneDeep,
|
||||||
|
get,
|
||||||
|
mergeAll,
|
||||||
|
isNil,
|
||||||
|
toNumber,
|
||||||
|
isInteger,
|
||||||
|
} = require('lodash/fp');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const parseType = require('./parse-type');
|
const parseType = require('./parse-type');
|
||||||
const contentTypesUtils = require('./content-types');
|
const contentTypesUtils = require('./content-types');
|
||||||
|
const { PaginationError } = require('./errors');
|
||||||
|
|
||||||
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
||||||
|
|
||||||
@ -389,6 +401,80 @@ const convertPublicationStateParams = (type, params = {}, query = {}) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const transformParamsToQuery = (uid, params) => {
|
||||||
|
// NOTE: can be a CT, a Compo or nothing in the case of polymorphism (DZ & morph relations)
|
||||||
|
const schema = strapi.getModel(uid);
|
||||||
|
|
||||||
|
const query = {};
|
||||||
|
|
||||||
|
const { _q, sort, filters, fields, populate, page, pageSize, start, limit } = params;
|
||||||
|
|
||||||
|
if (!isNil(_q)) {
|
||||||
|
query._q = _q;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNil(sort)) {
|
||||||
|
query.orderBy = convertSortQueryParams(sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNil(filters)) {
|
||||||
|
query.where = convertFiltersQueryParams(filters, schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNil(fields)) {
|
||||||
|
query.select = convertFieldsQueryParams(fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNil(populate)) {
|
||||||
|
query.populate = convertPopulateQueryParams(populate, schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPagePagination = !isNil(page) || !isNil(pageSize);
|
||||||
|
const isOffsetPagination = !isNil(start) || !isNil(limit);
|
||||||
|
|
||||||
|
if (isPagePagination && isOffsetPagination) {
|
||||||
|
throw new PaginationError(
|
||||||
|
'Invalid pagination attributes. You cannot use page and offset pagination in the same query'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNil(page)) {
|
||||||
|
const pageVal = toNumber(page);
|
||||||
|
|
||||||
|
if (!isInteger(pageVal) || pageVal <= 0) {
|
||||||
|
throw new PaginationError(
|
||||||
|
`Invalid 'page' parameter. Expected an integer > 0, received: ${page}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
query.page = pageVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNil(pageSize)) {
|
||||||
|
const pageSizeVal = toNumber(pageSize);
|
||||||
|
|
||||||
|
if (!isInteger(pageSizeVal) || pageSizeVal <= 0) {
|
||||||
|
throw new PaginationError(
|
||||||
|
`Invalid 'pageSize' parameter. Expected an integer > 0, received: ${page}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
query.pageSize = pageSizeVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNil(start)) {
|
||||||
|
query.offset = convertStartQueryParams(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNil(limit)) {
|
||||||
|
query.limit = convertLimitQueryParams(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertPublicationStateParams(schema, params, query);
|
||||||
|
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
convertSortQueryParams,
|
convertSortQueryParams,
|
||||||
convertStartQueryParams,
|
convertStartQueryParams,
|
||||||
@ -397,4 +483,5 @@ module.exports = {
|
|||||||
convertFiltersQueryParams,
|
convertFiltersQueryParams,
|
||||||
convertFieldsQueryParams,
|
convertFieldsQueryParams,
|
||||||
convertPublicationStateParams,
|
convertPublicationStateParams,
|
||||||
|
transformParamsToQuery,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -38,6 +38,7 @@ const pagination = require('./pagination');
|
|||||||
const sanitize = require('./sanitize');
|
const sanitize = require('./sanitize');
|
||||||
const traverseEntity = require('./traverse-entity');
|
const traverseEntity = require('./traverse-entity');
|
||||||
const pipeAsync = require('./pipe-async');
|
const pipeAsync = require('./pipe-async');
|
||||||
|
const convertQueryParams = require('./convert-query-params');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
yup,
|
yup,
|
||||||
@ -79,4 +80,5 @@ module.exports = {
|
|||||||
errors,
|
errors,
|
||||||
validateYupSchema,
|
validateYupSchema,
|
||||||
validateYupSchemaSync,
|
validateYupSchemaSync,
|
||||||
|
convertQueryParams,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,11 +9,51 @@ const enableContentType = require('./migrations/content-type/enable');
|
|||||||
const disableContentType = require('./migrations/content-type/disable');
|
const disableContentType = require('./migrations/content-type/disable');
|
||||||
|
|
||||||
module.exports = ({ strapi }) => {
|
module.exports = ({ strapi }) => {
|
||||||
|
decorateRelations();
|
||||||
extendLocalizedContentTypes(strapi);
|
extendLocalizedContentTypes(strapi);
|
||||||
addContentManagerLocaleMiddleware(strapi);
|
addContentManagerLocaleMiddleware(strapi);
|
||||||
addContentTypeSyncHooks(strapi);
|
addContentTypeSyncHooks(strapi);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the /relations controller to handle locale parameter
|
||||||
|
*/
|
||||||
|
const decorateRelations = () => {
|
||||||
|
const { wrapParams } = getService('entity-service-decorator');
|
||||||
|
|
||||||
|
strapi.container.get('controllers').extend('plugin::content-manager.relations', (controller) => {
|
||||||
|
const oldFindAvailable = controller.findAvailable;
|
||||||
|
return Object.assign(controller, {
|
||||||
|
async findAvailable(ctx, next) {
|
||||||
|
const { model, targetField } = ctx.params;
|
||||||
|
const { component } = ctx.request.query;
|
||||||
|
|
||||||
|
const sourceModelUid = component || model;
|
||||||
|
|
||||||
|
const sourceModel = strapi.getModel(sourceModelUid);
|
||||||
|
if (!sourceModel) {
|
||||||
|
return ctx.badRequest("The model doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
const attribute = sourceModel.attributes[targetField];
|
||||||
|
if (!attribute || attribute.type !== 'relation') {
|
||||||
|
return ctx.badRequest("This relational field doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetedModel = strapi.getModel(attribute.target);
|
||||||
|
|
||||||
|
const { isLocalizedContentType } = getService('content-types');
|
||||||
|
|
||||||
|
if (isLocalizedContentType(targetedModel)) {
|
||||||
|
ctx.request.query = await wrapParams(ctx.request.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldFindAvailable(ctx, next);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds middleware on CM creation routes to use i18n locale passed in a specific param
|
* Adds middleware on CM creation routes to use i18n locale passed in a specific param
|
||||||
* @param {Strapi} strapi
|
* @param {Strapi} strapi
|
||||||
|
|||||||
@ -109,22 +109,22 @@ describe('i18n - Relation-list route', () => {
|
|||||||
|
|
||||||
test('Can filter on default locale', async () => {
|
test('Can filter on default locale', async () => {
|
||||||
const res = await rq({
|
const res = await rq({
|
||||||
method: 'POST',
|
method: 'GET',
|
||||||
url: '/content-manager/relations/api::shop.shop/products',
|
url: '/content-manager/relations/api::shop.shop/products',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.body).toHaveLength(1);
|
expect(res.body.results).toHaveLength(1);
|
||||||
expect(res.body[0]).toStrictEqual(pick(['id', 'name'], data.products[1]));
|
expect(res.body.results[0]).toStrictEqual(pick(['id', 'name'], data.products[1]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can filter on any locale', async () => {
|
test('Can filter on any locale', async () => {
|
||||||
const res = await rq({
|
const res = await rq({
|
||||||
method: 'POST',
|
method: 'GET',
|
||||||
url: '/content-manager/relations/api::shop.shop/products',
|
url: '/content-manager/relations/api::shop.shop/products',
|
||||||
qs: { locale: 'it' },
|
qs: { locale: 'it' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.body).toHaveLength(1);
|
expect(res.body.results).toHaveLength(1);
|
||||||
expect(res.body[0]).toStrictEqual(pick(['id', 'name'], data.products[0]));
|
expect(res.body.results[0]).toStrictEqual(pick(['id', 'name'], data.products[0]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user