Merge pull request #15363 from strapi/fix/webhook-with-count-relations

This commit is contained in:
Marc 2023-02-01 12:22:42 +01:00 committed by GitHub
commit 786e476209
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 725 additions and 331 deletions

View File

@ -13,4 +13,10 @@ module.exports = ({ env }) => ({
app: {
keys: env.array('APP_KEYS', ['toBeModified1', 'toBeModified2']),
},
webhooks: {
// TODO: V5, set to false by default
// Receive populated relations in webhook and db lifecycle payloads
// This only populates relations in all content-manager endpoints
populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', true),
},
});

View File

@ -1,5 +1,6 @@
'use strict';
const _ = require('lodash');
const entityManagerLoader = require('../entity-manager');
let entityManager;
@ -11,16 +12,20 @@ describe('Content-Manager', () => {
};
describe('Publish', () => {
const defaultConfig = {};
beforeEach(() => {
global.strapi = {
entityService: {
update: jest.fn(),
update: jest.fn().mockReturnValue({ id: 1, publishedAt: new Date() }),
},
entityValidator: {
validateEntityCreation() {},
},
eventHub: { emit: jest.fn(), sanitizeEntity: (entity) => entity },
getModel: jest.fn(() => fakeModel),
config: {
get: (path, defaultValue) => _.get(defaultConfig, path, defaultValue),
},
};
entityManager = entityManagerLoader({ strapi });
});
@ -42,13 +47,17 @@ describe('Content-Manager', () => {
});
describe('Unpublish', () => {
const defaultConfig = {};
beforeEach(() => {
global.strapi = {
entityService: {
update: jest.fn(),
update: jest.fn().mockReturnValue({ id: 1, publishedAt: null }),
},
eventHub: { emit: jest.fn(), sanitizeEntity: (entity) => entity },
getModel: jest.fn(() => fakeModel),
config: {
get: (path, defaultValue) => _.get(defaultConfig, path, defaultValue),
},
};
entityManager = entityManagerLoader({ strapi });
});

View File

@ -4,6 +4,7 @@ const { assoc, has, prop, omit } = require('lodash/fp');
const strapiUtils = require('@strapi/utils');
const { ApplicationError } = require('@strapi/utils').errors;
const { getDeepPopulate, getDeepPopulateDraftCount } = require('./utils/populate');
const { getDeepRelationsCount } = require('./utils/count');
const { sumDraftCounts } = require('./utils/draft');
const { hasDraftAndPublish } = strapiUtils.contentTypes;
@ -12,10 +13,8 @@ const { ENTRY_PUBLISH, ENTRY_UNPUBLISH } = strapiUtils.webhook.webhookEvents;
const omitPublishedAtField = omit(PUBLISHED_AT_ATTRIBUTE);
const wrapWithEmitEvent = (event, fn) => async (entity, body, model) => {
const result = await fn(entity, body, model);
const modelDef = strapi.getModel(model);
const emitEvent = async (event, entity, modelUid) => {
const modelDef = strapi.getModel(modelUid);
const sanitizedEntity = await strapiUtils.sanitize.sanitizers.defaultSanitizeOutput(
modelDef,
entity
@ -25,8 +24,6 @@ const wrapWithEmitEvent = (event, fn) => async (entity, body, model) => {
model: modelDef.modelName,
entry: sanitizedEntity,
});
return result;
};
const findCreatorRoles = (entity) => {
@ -49,6 +46,19 @@ const addCreatedByRolesPopulate = (populate) => {
};
};
/**
* When webhooks.populateRelations is set to true, populated relations
* will be passed to any webhook event. The entity-manager
* response will not have the populated relations though.
* For performance reasons, it is recommended to set it to false,
*
* TODO V5: Set to false by default.
* TODO V5: Make webhooks always send the same entity data.
*/
const isRelationsPopulateEnabled = () => {
return strapi.config.get('server.webhooks.populateRelations', true);
};
/**
* @type {import('./entity-manager').default}
*/
@ -107,6 +117,7 @@ module.exports = ({ strapi }) => ({
async create(body, uid) {
const modelDef = strapi.getModel(uid);
const publishData = { ...body };
const populateRelations = isRelationsPopulateEnabled(uid);
if (hasDraftAndPublish(modelDef)) {
publishData[PUBLISHED_AT_ATTRIBUTE] = null;
@ -114,27 +125,59 @@ module.exports = ({ strapi }) => ({
const params = {
data: publishData,
populate: getDeepPopulate(uid, { countMany: true, countOne: true }),
populate: populateRelations
? getDeepPopulate(uid, {})
: getDeepPopulate(uid, { countMany: true, countOne: true }),
};
return strapi.entityService.create(uid, params);
const entity = await strapi.entityService.create(uid, params);
// If relations were populated, relations count will be returned instead of the array of relations.
if (populateRelations) {
return getDeepRelationsCount(entity, uid);
}
return entity;
},
update(entity, body, uid) {
async update(entity, body, uid) {
const publishData = omitPublishedAtField(body);
const populateRelations = isRelationsPopulateEnabled(uid);
const params = {
data: publishData,
populate: getDeepPopulate(uid, { countMany: true, countOne: true }),
populate: populateRelations
? getDeepPopulate(uid, {})
: getDeepPopulate(uid, { countMany: true, countOne: true }),
};
return strapi.entityService.update(uid, entity.id, params);
const updatedEntity = await strapi.entityService.update(uid, entity.id, params);
// If relations were populated, relations count will be returned instead of the array of relations.
if (populateRelations) {
return getDeepRelationsCount(updatedEntity, uid);
}
return updatedEntity;
},
delete(entity, uid) {
const params = { populate: getDeepPopulate(uid, { countMany: true, countOne: true }) };
async delete(entity, uid) {
const populateRelations = isRelationsPopulateEnabled(uid);
return strapi.entityService.delete(uid, entity.id, params);
const params = {
populate: populateRelations
? getDeepPopulate(uid, {})
: getDeepPopulate(uid, { countMany: true, countOne: true }),
};
const deletedEntity = await strapi.entityService.delete(uid, entity.id, params);
// If relations were populated, relations count will be returned instead of the array of relations.
if (populateRelations) {
return getDeepRelationsCount(deletedEntity, uid);
}
return deletedEntity;
},
// FIXME: handle relations
@ -144,7 +187,7 @@ module.exports = ({ strapi }) => ({
return strapi.entityService.deleteMany(uid, params);
},
publish: wrapWithEmitEvent(ENTRY_PUBLISH, async (entity, body = {}, uid) => {
async publish(entity, body = {}, uid) {
if (entity[PUBLISHED_AT_ATTRIBUTE]) {
throw new ApplicationError('already.published');
}
@ -158,29 +201,53 @@ module.exports = ({ strapi }) => ({
);
const data = { ...body, [PUBLISHED_AT_ATTRIBUTE]: new Date() };
const populateRelations = isRelationsPopulateEnabled(uid);
const params = {
data,
populate: getDeepPopulate(uid, { countMany: true, countOne: true }),
populate: populateRelations
? getDeepPopulate(uid, {})
: getDeepPopulate(uid, { countMany: true, countOne: true }),
};
return strapi.entityService.update(uid, entity.id, params);
}),
const updatedEntity = await strapi.entityService.update(uid, entity.id, params);
unpublish: wrapWithEmitEvent(ENTRY_UNPUBLISH, (entity, body = {}, uid) => {
await emitEvent(ENTRY_PUBLISH, entity, uid);
// If relations were populated, relations count will be returned instead of the array of relations.
if (isRelationsPopulateEnabled(uid)) {
return getDeepRelationsCount(updatedEntity, uid);
}
return updatedEntity;
},
async unpublish(entity, body = {}, uid) {
if (!entity[PUBLISHED_AT_ATTRIBUTE]) {
throw new ApplicationError('already.draft');
}
const data = { ...body, [PUBLISHED_AT_ATTRIBUTE]: null };
const populateRelations = isRelationsPopulateEnabled(uid);
const params = {
data,
populate: getDeepPopulate(uid, { countMany: true, countOne: true }),
populate: populateRelations
? getDeepPopulate(uid, {})
: getDeepPopulate(uid, { countMany: true, countOne: true }),
};
return strapi.entityService.update(uid, entity.id, params);
}),
const updatedEntity = await strapi.entityService.update(uid, entity.id, params);
await emitEvent(ENTRY_UNPUBLISH, entity, uid);
// If relations were populated, relations count will be returned instead of the array of relations.
if (isRelationsPopulateEnabled(uid)) {
return getDeepRelationsCount(updatedEntity, uid);
}
return updatedEntity;
},
async getNumberOfDraftRelations(id, uid) {
const { populate, hasRelations } = getDeepPopulateDraftCount(uid);

View File

@ -0,0 +1,242 @@
'use strict';
const { getDeepRelationsCount } = require('../count');
const fakeModels = {
component: {
modelName: 'Fake component model',
attributes: {
componentAttrName: {
type: 'component',
component: 'relationMTM',
},
},
},
repeatableComponent: {
modelName: 'Fake repeatable component model',
attributes: {
repeatableComponentAttrName: {
type: 'component',
repeatable: true,
component: 'relationMTM',
},
},
},
dynZone: {
modelName: 'Fake dynamic zone model',
attributes: {
dynZoneAttrName: {
type: 'dynamiczone',
components: ['component'],
},
},
},
relationMTM: {
modelName: 'Fake relation manyToMany model',
attributes: {
relationAttrName: {
type: 'relation',
relation: 'oneToMany',
},
},
},
relationOTO: {
modelName: 'Fake relation oneToOne model',
attributes: {
relationAttrName: {
type: 'relation',
relation: 'oneToOne',
},
},
},
media: {
modelName: 'Fake media model',
attributes: {
mediaAttrName: {
type: 'media',
},
},
},
};
describe('getDeepRelationsCount', () => {
beforeEach(() => {
global.strapi = {
getModel: jest.fn((uid) => fakeModels[uid]),
};
});
afterEach(() => {
jest.clearAllMocks();
});
describe('relation fields', () => {
test('with many to many', () => {
const count = getDeepRelationsCount(
{
relationAttrName: [
{
id: 2,
name: 'rel1',
},
{
id: 7,
name: 'rel2',
},
],
},
'relationMTM'
);
expect(count).toEqual({
relationAttrName: {
count: 2,
},
});
});
test('with one to one', () => {
const count = getDeepRelationsCount(
{
relationAttrName: {
id: 2,
name: 'rel1',
},
},
'relationOTO'
);
expect(count).toEqual({
relationAttrName: {
count: 1,
},
});
});
});
describe('media fields', () => {
test('with media', () => {
const mediaEntity = {
mediaAttrName: { id: 1, name: 'img1' },
};
const count = getDeepRelationsCount(mediaEntity, 'media');
expect(count).toEqual(mediaEntity);
});
});
describe('component fields', () => {
test('with component', () => {
const count = getDeepRelationsCount(
{
componentAttrName: {
relationAttrName: [
{
id: 2,
name: 'rel1',
},
{
id: 7,
name: 'rel2',
},
],
},
},
'component'
);
expect(count).toEqual({
componentAttrName: {
relationAttrName: {
count: 2,
},
},
});
});
test('with empty component', () => {
const count = getDeepRelationsCount(
{
componentAttrName: null,
},
'component'
);
expect(count).toEqual({
componentAttrName: null,
});
});
test('with repeatable component', () => {
const count = getDeepRelationsCount(
{
repeatableComponentAttrName: [
{
relationAttrName: [
{
id: 2,
name: 'rel1',
},
{
id: 7,
name: 'rel2',
},
],
},
],
},
'repeatableComponent'
);
expect(count).toEqual({
repeatableComponentAttrName: [
{
relationAttrName: {
count: 2,
},
},
],
});
});
});
describe('dynamic zone fields', () => {
test('with dynamic zone', () => {
const count = getDeepRelationsCount(
{
dynZoneAttrName: [
{
__component: 'component',
componentAttrName: {
relationAttrName: [
{
id: 2,
name: 'rel1',
},
{
id: 7,
name: 'rel2',
},
],
},
},
],
},
'dynZone'
);
expect(count).toEqual({
dynZoneAttrName: [
{
__component: 'component',
componentAttrName: {
relationAttrName: {
count: 2,
},
},
},
],
});
});
});
});

View File

@ -0,0 +1,57 @@
'use strict';
const { isVisibleAttribute } = require('@strapi/utils').contentTypes;
function getCountForRelation(attributeName, entity, model) {
// do not count createdBy, updatedBy, localizations etc.
if (!isVisibleAttribute(model, attributeName)) {
return entity;
}
if (Array.isArray(entity)) {
return { count: entity.length };
}
return entity ? { count: 1 } : { count: 0 };
}
function getCountForDZ(entity) {
return entity.map((component) => {
return getDeepRelationsCount(component, component.__component);
});
}
function getCountFor(attributeName, entity, model) {
const attribute = model.attributes[attributeName];
switch (attribute?.type) {
case 'relation':
return getCountForRelation(attributeName, entity, model);
case 'component':
if (!entity) return null;
if (attribute.repeatable) {
return entity.map((component) => getDeepRelationsCount(component, attribute.component));
}
return getDeepRelationsCount(entity, attribute.component);
case 'dynamiczone':
return getCountForDZ(entity);
default:
return entity;
}
}
const getDeepRelationsCount = (entity, uid) => {
const model = strapi.getModel(uid);
return Object.keys(entity).reduce(
(relationCountEntity, attributeName) =>
Object.assign(relationCountEntity, {
[attributeName]: getCountFor(attributeName, entity[attributeName], model),
}),
{}
);
};
module.exports = {
getDeepRelationsCount,
};

View File

@ -114,324 +114,331 @@ const getCollectorByName = (collectors, name) => collectors.find((c) => c.name =
const getStampByName = (stamps, name) => stamps.find((s) => s.name === name);
describe('CM API', () => {
beforeAll(async () => {
await builder
.addContentTypes([stamp, collector])
.addFixtures(stamp.singularName, stampFixtures)
.addFixtures(collector.singularName, collectorFixtures)
.build();
describe.each([{ populateRelations: false }, { populateRelations: true }])(
`populate relations in webhooks ($populateRelations)`,
({ populateRelations }) => {
beforeAll(async () => {
await builder
.addContentTypes([stamp, collector])
.addFixtures(stamp.singularName, stampFixtures)
.addFixtures(collector.singularName, collectorFixtures)
.build();
strapi = await createStrapiInstance();
rq = await createAuthRequest({ strapi });
strapi = await createStrapiInstance();
rq = await createAuthRequest({ strapi });
data.collectors = await builder.sanitizedFixturesFor(collector.singularName, strapi);
data.stamps = await builder.sanitizedFixturesFor(stamp.singularName, strapi);
});
strapi.config.set('server.webhooks.populateRelations', populateRelations);
afterAll(async () => {
await strapi.destroy();
await builder.cleanup();
});
describe('Automatic count and populate relations', () => {
test('many-way', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
sort: 'name:ASC',
},
data.collectors = await builder.sanitizedFixturesFor(collector.singularName, strapi);
data.stamps = await builder.sanitizedFixturesFor(stamp.singularName, strapi);
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
// all relations are populated and xToMany relations are counted
expect(res.body.results).toMatchObject([
{
age: 25,
createdBy: null,
id: 1,
name: 'Bernard',
stamps: { count: 2 },
stamps_m2m: { count: 1 },
stamps_one_many: { count: 0 },
stamps_one_one: {
afterAll(async () => {
await strapi.destroy();
await builder.cleanup();
});
describe('Automatic count and populate relations', () => {
test('many-way', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
sort: 'name:ASC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
// all relations are populated and xToMany relations are counted
expect(res.body.results).toMatchObject([
{
age: 25,
createdBy: null,
id: 1,
name: 'Bernard',
stamps: { count: 2 },
stamps_m2m: { count: 1 },
stamps_one_many: { count: 0 },
stamps_one_one: {
id: 1,
name: '1946',
},
stamps_one_way: {
id: 1,
name: '1946',
},
updatedBy: null,
},
{
age: 23,
createdBy: null,
id: 3,
name: 'Emma',
stamps: { count: 0 },
stamps_m2m: { count: 2 },
stamps_one_many: { count: 1 },
stamps_one_one: {
id: 3,
name: '1948',
},
stamps_one_way: {
id: 3,
name: '1948',
},
updatedBy: null,
},
{
age: 55,
createdBy: null,
id: 2,
name: 'Isabelle',
stamps: { count: 1 },
stamps_m2m: { count: 0 },
stamps_one_many: { count: 2 },
stamps_one_one: {
id: 2,
name: '1947',
},
stamps_one_way: {
id: 2,
name: '1947',
},
updatedBy: null,
},
]);
});
test('findOne', async () => {
const res = await rq({
method: 'GET',
url: `/content-manager/collection-types/api::collector.collector/${data.collectors[0].id}`,
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
age: 25,
id: 1,
name: '1946',
},
stamps_one_way: {
id: 1,
name: '1946',
},
updatedBy: null,
},
{
age: 23,
createdBy: null,
id: 3,
name: 'Emma',
stamps: { count: 0 },
stamps_m2m: { count: 2 },
stamps_one_many: { count: 1 },
stamps_one_one: {
id: 3,
name: '1948',
},
stamps_one_way: {
id: 3,
name: '1948',
},
updatedBy: null,
},
{
age: 55,
createdBy: null,
id: 2,
name: 'Isabelle',
stamps: { count: 1 },
stamps_m2m: { count: 0 },
stamps_one_many: { count: 2 },
stamps_one_one: {
id: 2,
name: '1947',
},
stamps_one_way: {
id: 2,
name: '1947',
},
updatedBy: null,
},
]);
});
test('findOne', async () => {
const res = await rq({
method: 'GET',
url: `/content-manager/collection-types/api::collector.collector/${data.collectors[0].id}`,
name: 'Bernard',
stamps: { count: 2 },
stamps_m2m: { count: 1 },
stamps_one_many: { count: 0 },
stamps_one_one: { count: 1 },
stamps_one_way: { count: 1 },
createdBy: null,
updatedBy: null,
});
});
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
age: 25,
id: 1,
name: 'Bernard',
stamps: { count: 2 },
stamps_m2m: { count: 1 },
stamps_one_many: { count: 0 },
stamps_one_one: { count: 1 },
stamps_one_way: { count: 1 },
createdBy: null,
updatedBy: null,
});
});
});
describe('Filter relations', () => {
test('many-way', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
filters: { stamps: { name: '1946' } },
},
});
describe('Filter relations', () => {
test('many-way', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
filters: { stamps: { name: '1946' } },
},
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(2);
expect(res.body.results[0].name).toBe('Bernard');
expect(res.body.results[1].name).toBe('Isabelle');
});
test('many-to-many (collector -> stamps)', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
filters: { stamps_m2m: { name: '1946' } },
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(2);
expect(getCollectorByName(res.body.results, 'Bernard')).toBeDefined();
expect(getCollectorByName(res.body.results, 'Emma')).toBeDefined();
});
test('many-to-many (stamp -> collectors)', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::stamp.stamp',
qs: {
filters: { collectors: { name: 'Emma' } },
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(2);
expect(getStampByName(res.body.results, '1946')).toBeDefined();
expect(getStampByName(res.body.results, '1947')).toBeDefined();
});
test('one-to-many', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
filters: { stamps_one_many: { name: '1947' } },
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(1);
expect(res.body.results[0].name).toBe('Isabelle');
});
test('many-to-one', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::stamp.stamp',
qs: {
filters: { collector: { name: 'Isabelle' } },
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(2);
expect(getStampByName(res.body.results, '1947')).toBeDefined();
expect(getStampByName(res.body.results, '1948')).toBeDefined();
});
test('one-way', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
filters: { stamps_one_way: { name: '1947' } },
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(1);
expect(getCollectorByName(res.body.results, 'Isabelle')).toBeDefined();
});
test('one-one', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
filters: { stamps_one_one: { name: '1947' } },
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(1);
expect(getCollectorByName(res.body.results, 'Isabelle')).toBeDefined();
});
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(2);
expect(res.body.results[0].name).toBe('Bernard');
expect(res.body.results[1].name).toBe('Isabelle');
});
describe('Sort relations', () => {
test('many-to-one', async () => {
let res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::stamp.stamp',
qs: {
sort: 'collector.name:ASC',
},
});
test('many-to-many (collector -> stamps)', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
filters: { stamps_m2m: { name: '1946' } },
},
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].collector.name).toBe('Emma');
expect(res.body.results[1].collector.name).toBe('Isabelle');
expect(res.body.results[2].collector.name).toBe('Isabelle');
res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::stamp.stamp',
qs: {
sort: 'collector.name:DESC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].collector.name).toBe('Isabelle');
expect(res.body.results[1].collector.name).toBe('Isabelle');
expect(res.body.results[2].collector.name).toBe('Emma');
});
test('one-way', async () => {
let res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
sort: 'stamps_one_way.name:ASC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].stamps_one_way.name).toBe('1946');
expect(res.body.results[1].stamps_one_way.name).toBe('1947');
expect(res.body.results[2].stamps_one_way.name).toBe('1948');
res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
sort: 'stamps_one_way.name:DESC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].stamps_one_way.name).toBe('1948');
expect(res.body.results[1].stamps_one_way.name).toBe('1947');
expect(res.body.results[2].stamps_one_way.name).toBe('1946');
});
test('one-one', async () => {
let res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
sort: 'stamps_one_one.name:ASC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].stamps_one_one.name).toBe('1946');
expect(res.body.results[1].stamps_one_one.name).toBe('1947');
expect(res.body.results[2].stamps_one_one.name).toBe('1948');
res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
sort: 'stamps_one_one.name:DESC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].stamps_one_one.name).toBe('1948');
expect(res.body.results[1].stamps_one_one.name).toBe('1947');
expect(res.body.results[2].stamps_one_one.name).toBe('1946');
});
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(2);
expect(getCollectorByName(res.body.results, 'Bernard')).toBeDefined();
expect(getCollectorByName(res.body.results, 'Emma')).toBeDefined();
});
test('many-to-many (stamp -> collectors)', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::stamp.stamp',
qs: {
filters: { collectors: { name: 'Emma' } },
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(2);
expect(getStampByName(res.body.results, '1946')).toBeDefined();
expect(getStampByName(res.body.results, '1947')).toBeDefined();
});
test('one-to-many', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
filters: { stamps_one_many: { name: '1947' } },
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(1);
expect(res.body.results[0].name).toBe('Isabelle');
});
test('many-to-one', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::stamp.stamp',
qs: {
filters: { collector: { name: 'Isabelle' } },
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(2);
expect(getStampByName(res.body.results, '1947')).toBeDefined();
expect(getStampByName(res.body.results, '1948')).toBeDefined();
});
test('one-way', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
filters: { stamps_one_way: { name: '1947' } },
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(1);
expect(getCollectorByName(res.body.results, 'Isabelle')).toBeDefined();
});
test('one-one', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
filters: { stamps_one_one: { name: '1947' } },
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(1);
expect(getCollectorByName(res.body.results, 'Isabelle')).toBeDefined();
});
});
describe('Sort relations', () => {
test('many-to-one', async () => {
let res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::stamp.stamp',
qs: {
sort: 'collector.name:ASC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].collector.name).toBe('Emma');
expect(res.body.results[1].collector.name).toBe('Isabelle');
expect(res.body.results[2].collector.name).toBe('Isabelle');
res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::stamp.stamp',
qs: {
sort: 'collector.name:DESC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].collector.name).toBe('Isabelle');
expect(res.body.results[1].collector.name).toBe('Isabelle');
expect(res.body.results[2].collector.name).toBe('Emma');
});
test('one-way', async () => {
let res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
sort: 'stamps_one_way.name:ASC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].stamps_one_way.name).toBe('1946');
expect(res.body.results[1].stamps_one_way.name).toBe('1947');
expect(res.body.results[2].stamps_one_way.name).toBe('1948');
res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
sort: 'stamps_one_way.name:DESC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].stamps_one_way.name).toBe('1948');
expect(res.body.results[1].stamps_one_way.name).toBe('1947');
expect(res.body.results[2].stamps_one_way.name).toBe('1946');
});
test('one-one', async () => {
let res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
sort: 'stamps_one_one.name:ASC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].stamps_one_one.name).toBe('1946');
expect(res.body.results[1].stamps_one_one.name).toBe('1947');
expect(res.body.results[2].stamps_one_one.name).toBe('1948');
res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
sort: 'stamps_one_one.name:DESC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(res.body.results[0].stamps_one_one.name).toBe('1948');
expect(res.body.results[1].stamps_one_one.name).toBe('1947');
expect(res.body.results[2].stamps_one_one.name).toBe('1946');
});
});
}
);
});

View File

@ -4,4 +4,7 @@ module.exports = ({ env }) => ({
app: {
keys: env.array('APP_KEYS'),
},
webhooks: {
populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false),
},
});

View File

@ -4,4 +4,7 @@ export default ({ env }) => ({
app: {
keys: env.array('APP_KEYS'),
},
webhooks: {
populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false),
},
});