Merge branch 'main' into enhancement/axios-refactoring

This commit is contained in:
Simone Taeggi 2022-11-29 17:35:58 +01:00
commit 20cbf0d630
10 changed files with 191 additions and 50 deletions

View File

@ -21,7 +21,7 @@ permissions: {}
jobs: jobs:
deploy: deploy:
permissions: permissions:
contents: write # to push pages branch (peaceiris/actions-gh-pages) contents: write # to push pages branch (peaceiris/actions-gh-pages)
environment: environment:
name: github-pages name: github-pages

View File

@ -6,7 +6,7 @@ on:
workflow_dispatch: workflow_dispatch:
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
jobs: jobs:
publish: publish:

View File

@ -14,7 +14,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
jobs: jobs:
lint: lint:
@ -142,18 +142,18 @@ jobs:
node: [14, 16, 18] node: [14, 16, 18]
services: services:
mysql: mysql:
image: mysql image: bitnami/mysql:latest
env:
MYSQL_ROOT_PASSWORD: strapi
MYSQL_USER: strapi
MYSQL_PASSWORD: strapi
MYSQL_DATABASE: strapi_test
MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
options: >- options: >-
--health-cmd="mysqladmin ping" --health-cmd="mysqladmin ping"
--health-interval=10s --health-interval=10s
--health-timeout=5s --health-timeout=5s
--health-retries=3 --health-retries=3
-e MYSQL_ROOT_PASSWORD=strapi
-e MYSQL_ROOT_HOST="%"
-e MYSQL_USER=strapi
-e MYSQL_PASSWORD=strapi
-e MYSQL_DATABASE=strapi_test
--entrypoint sh mysql -c "exec docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password"
ports: ports:
# Maps tcp port 5432 on service container to the host # Maps tcp port 5432 on service container to the host
- 3306:3306 - 3306:3306
@ -180,18 +180,17 @@ jobs:
node: [14, 16, 18] node: [14, 16, 18]
services: services:
mysql: mysql:
image: mysql:5 image: bitnami/mysql:5.7
env:
MYSQL_ROOT_PASSWORD: strapi
MYSQL_USER: strapi
MYSQL_PASSWORD: strapi
MYSQL_DATABASE: strapi_test
options: >- options: >-
--health-cmd="mysqladmin ping" --health-cmd="mysqladmin ping"
--health-interval=10s --health-interval=10s
--health-timeout=5s --health-timeout=5s
--health-retries=3 --health-retries=3
-e MYSQL_ROOT_PASSWORD=strapi
-e MYSQL_ROOT_HOST="%"
-e MYSQL_USER=strapi
-e MYSQL_PASSWORD=strapi
-e MYSQL_DATABASE=strapi_test
--entrypoint sh mysql -c "exec docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password"
ports: ports:
# Maps tcp port 5432 on service container to the host # Maps tcp port 5432 on service container to the host
- 3306:3306 - 3306:3306
@ -288,18 +287,18 @@ jobs:
node: [14, 16, 18] node: [14, 16, 18]
services: services:
mysql: mysql:
image: mysql image: bitnami/mysql:latest
env:
MYSQL_ROOT_PASSWORD: strapi
MYSQL_USER: strapi
MYSQL_PASSWORD: strapi
MYSQL_DATABASE: strapi_test
MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
options: >- options: >-
--health-cmd="mysqladmin ping" --health-cmd="mysqladmin ping"
--health-interval=10s --health-interval=10s
--health-timeout=5s --health-timeout=5s
--health-retries=3 --health-retries=3
-e MYSQL_ROOT_PASSWORD=strapi
-e MYSQL_ROOT_HOST="%"
-e MYSQL_USER=strapi
-e MYSQL_PASSWORD=strapi
-e MYSQL_DATABASE=strapi_test
--entrypoint sh mysql -c "exec docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password"
ports: ports:
# Maps tcp port 5432 on service container to the host # Maps tcp port 5432 on service container to the host
- 3306:3306 - 3306:3306

View File

@ -436,7 +436,7 @@ describe('Admin API Token v2 CRUD (api)', () => {
expect(res.body.data).toMatchObject({ expect(res.body.data).toMatchObject({
accessKey: expect.any(String), accessKey: expect.any(String),
name: body.name, name: body.name,
permissions: body.permissions, permissions: expect.arrayContaining(body.permissions),
description: body.description, description: body.description,
type: body.type, type: body.type,
id: expect.any(Number), id: expect.any(Number),

View File

@ -446,6 +446,11 @@ const morphToMany = async (input, ctx) => {
.where({ .where({
[joinColumn.name]: referencedValues, [joinColumn.name]: referencedValues,
...(joinTable.on || {}), ...(joinTable.on || {}),
// If the populateValue contains an "on" property,
// only populate the types defined in it
...('on' in populateValue
? { [morphColumn.typeColumn.name]: Object.keys(populateValue.on) }
: {}),
}) })
.orderBy([joinColumn.name, 'order']) .orderBy([joinColumn.name, 'order'])
.execute({ mapResults: false }); .execute({ mapResults: false });
@ -470,6 +475,8 @@ const morphToMany = async (input, ctx) => {
}, {}); }, {});
const map = {}; const map = {};
const { on, ...typePopulate } = populateValue;
for (const type of Object.keys(idsByType)) { for (const type of Object.keys(idsByType)) {
const ids = idsByType[type]; const ids = idsByType[type];
@ -483,7 +490,7 @@ const morphToMany = async (input, ctx) => {
const qb = db.entityManager.createQueryBuilder(type); const qb = db.entityManager.createQueryBuilder(type);
const rows = await qb const rows = await qb
.init(populateValue) .init(on?.[type] ?? typePopulate)
.addSelect(`${qb.alias}.${idColumn.referencedColumn}`) .addSelect(`${qb.alias}.${idColumn.referencedColumn}`)
.where({ [idColumn.referencedColumn]: ids }) .where({ [idColumn.referencedColumn]: ids })
.execute({ mapResults: false }); .execute({ mapResults: false });
@ -540,6 +547,8 @@ const morphToOne = async (input, ctx) => {
}, {}); }, {});
const map = {}; const map = {};
const { on, ...typePopulate } = populateValue;
for (const type of Object.keys(idsByType)) { for (const type of Object.keys(idsByType)) {
const ids = idsByType[type]; const ids = idsByType[type];
@ -552,7 +561,7 @@ const morphToOne = async (input, ctx) => {
const qb = db.entityManager.createQueryBuilder(type); const qb = db.entityManager.createQueryBuilder(type);
const rows = await qb const rows = await qb
.init(populateValue) .init(on?.[type] ?? typePopulate)
.addSelect(`${qb.alias}.${idColumn.referencedColumn}`) .addSelect(`${qb.alias}.${idColumn.referencedColumn}`)
.where({ [idColumn.referencedColumn]: ids }) .where({ [idColumn.referencedColumn]: ids })
.execute({ mapResults: false }); .execute({ mapResults: false });
@ -579,7 +588,16 @@ const morphToOne = async (input, ctx) => {
// TODO: Omit limit & offset to avoid needing a query per result to avoid making too many queries // TODO: Omit limit & offset to avoid needing a query per result to avoid making too many queries
const pickPopulateParams = (populate) => { const pickPopulateParams = (populate) => {
const fieldsToPick = ['select', 'count', 'where', 'populate', 'orderBy', 'filters', 'ordering']; const fieldsToPick = [
'select',
'count',
'where',
'populate',
'orderBy',
'filters',
'ordering',
'on',
];
if (populate.count !== true) { if (populate.count !== true) {
fieldsToPick.push('limit', 'offset'); fieldsToPick.push('limit', 'offset');

View File

@ -108,6 +108,11 @@ const fixtures = {
number: 2, number: 2,
field: 'short string', field: 'short string',
}, },
{
__component: 'default.foo',
number: 3,
field: 'long string',
},
{ {
__component: 'default.bar', __component: 'default.bar',
title: 'this is a title', title: 'this is a title',
@ -315,4 +320,100 @@ describe('Populate filters', () => {
expect(body.data[0].attributes.third).toBeUndefined(); expect(body.data[0].attributes.third).toBeUndefined();
}); });
}); });
describe('Populate a dynamic zone', () => {
test('Populate every component in the dynamic zone', async () => {
const qs = {
populate: {
dz: '*',
},
};
const { status, body } = await rq.get(`/${schemas.contentTypes.b.pluralName}`, { qs });
expect(status).toBe(200);
expect(body.data).toHaveLength(2);
fixtures.b.forEach((fixture, i) => {
const res = body.data[i];
const { dz } = res.attributes;
expect(dz).toHaveLength(fixture.dz.length);
expect(dz).toMatchObject(
fixture.dz.map((component) => ({
...omit('field', component),
id: expect.any(Number),
}))
);
});
});
test('Populate only one component type using fragment', async () => {
const qs = {
populate: {
dz: {
on: {
'default.foo': true,
},
},
},
};
const { status, body } = await rq.get(`/${schemas.contentTypes.b.pluralName}`, { qs });
expect(status).toBe(200);
expect(body.data).toHaveLength(2);
expect(body.data[0].attributes.dz).toHaveLength(3);
expect(body.data[1].attributes.dz).toHaveLength(0);
const expected = fixtures.b[0].dz
.filter(({ __component }) => __component === 'default.foo')
.map((component) => ({
...component,
id: expect.any(Number),
}));
expect(body.data[0].attributes.dz).toMatchObject(expected);
});
test('Populate the dynamic zone with filters in fragments', async () => {
const qs = {
populate: {
dz: {
on: {
'default.foo': {
filters: { number: { $lt: 3 } },
},
'default.bar': {
filters: { title: { $contains: 'another' } },
},
},
},
},
};
const { status, body } = await rq.get(`/${schemas.contentTypes.b.pluralName}`, { qs });
expect(status).toBe(200);
expect(body.data).toHaveLength(2);
expect(body.data[0].attributes.dz).toHaveLength(2);
expect(body.data[1].attributes.dz).toHaveLength(1);
const filter = (data = []) =>
data
.filter(({ __component, number, title }) => {
if (__component === 'default.foo') return number < 3;
if (__component === 'default.bar') return title.includes('another');
return false;
})
.map((component) => ({
...(component.__component === 'default.foo' ? component : omit('field', component)),
id: expect.any(Number),
}));
expect(body.data[0].attributes.dz).toMatchObject(filter(fixtures.b[0].dz));
expect(body.data[1].attributes.dz).toMatchObject(filter(fixtures.b[1].dz));
});
});
}); });

View File

@ -115,7 +115,6 @@ describe('Uploads folder (GraphQL)', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
uploadFolder = file.folder; uploadFolder = file.folder;
}); });
@ -170,7 +169,6 @@ describe('Uploads folder (GraphQL)', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
uploadFolder = file.folder; uploadFolder = file.folder;
}); });
@ -255,7 +253,6 @@ describe('Uploads folder (GraphQL)', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
uploadFolder = file.folder; uploadFolder = file.folder;
}); });
@ -310,7 +307,6 @@ describe('Uploads folder (GraphQL)', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
uploadFolder = file.folder; uploadFolder = file.folder;
}); });

View File

@ -118,7 +118,6 @@ describe('Uploads folder', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
uploadFolder = file.folder; uploadFolder = file.folder;
}); });
@ -163,7 +162,6 @@ describe('Uploads folder', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
uploadFolder = file.folder; uploadFolder = file.folder;
}); });
@ -230,7 +228,6 @@ describe('Uploads folder', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
uploadFolder = file.folder; uploadFolder = file.folder;
}); });
@ -276,7 +273,6 @@ describe('Uploads folder', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
uploadFolder = file.folder; uploadFolder = file.folder;
}); });
@ -360,7 +356,6 @@ describe('Uploads folder', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
uploadFolder = file.folder; uploadFolder = file.folder;
}); });
@ -408,7 +403,6 @@ describe('Uploads folder', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
uploadFolder = file.folder; uploadFolder = file.folder;
}); });
@ -563,7 +557,6 @@ describe('Uploads folder', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
}); });
expect(files.every((file) => file.folder.id === files[0].folder.id)).toBe(true); expect(files.every((file) => file.folder.id === files[0].folder.id)).toBe(true);
@ -625,7 +618,6 @@ describe('Uploads folder', () => {
}, },
folderPath: `/${file.folder.pathId}`, folderPath: `/${file.folder.pathId}`,
}); });
expect(file.folder.id).not.toBe(uploadFolder.id);
}); });
expect(files.every((file) => file.folder.id === files[0].folder.id)).toBe(true); expect(files.every((file) => file.folder.id === files[0].folder.id)).toBe(true);

View File

@ -104,12 +104,16 @@ const isPrivateAttribute = (model = {}, attributeName) => {
}; };
const isScalarAttribute = (attribute) => { const isScalarAttribute = (attribute) => {
return !['media', 'component', 'relation', 'dynamiczone'].includes(attribute.type); return !['media', 'component', 'relation', 'dynamiczone'].includes(attribute?.type);
};
const isMediaAttribute = (attribute) => attribute?.type === 'media';
const isRelationalAttribute = (attribute) => attribute?.type === 'relation';
const isComponentAttribute = (attribute) => ['component', 'dynamiczone'].includes(attribute?.type);
const isDynamicZoneAttribute = (attribute) => attribute?.type === 'dynamiczone';
const isMorphToRelationalAttribute = (attribute) => {
return isRelationalAttribute(attribute) && attribute?.relation?.startsWith?.('morphTo');
}; };
const isMediaAttribute = (attribute) => attribute && attribute.type === 'media';
const isRelationalAttribute = (attribute) => attribute && attribute.type === 'relation';
const isComponentAttribute = (attribute) =>
attribute && ['component', 'dynamiczone'].includes(attribute.type);
const getComponentAttributes = (schema) => { const getComponentAttributes = (schema) => {
return _.reduce( return _.reduce(
@ -158,6 +162,8 @@ module.exports = {
isMediaAttribute, isMediaAttribute,
isRelationalAttribute, isRelationalAttribute,
isComponentAttribute, isComponentAttribute,
isDynamicZoneAttribute,
isMorphToRelationalAttribute,
isTypedAttribute, isTypedAttribute,
getPrivateAttributes, getPrivateAttributes,
isPrivateAttribute, isPrivateAttribute,

View File

@ -6,7 +6,11 @@
* 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 { const {
isNil,
toNumber,
isInteger,
has, has,
isEmpty, isEmpty,
isObject, isObject,
@ -14,14 +18,16 @@ const {
cloneDeep, cloneDeep,
get, get,
mergeAll, mergeAll,
isNil,
toNumber,
isInteger,
} = require('lodash/fp'); } = 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 { PaginationError } = require('./errors');
const {
isMediaAttribute,
isDynamicZoneAttribute,
isMorphToRelationalAttribute,
} = require('./content-types');
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants; const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
@ -185,8 +191,31 @@ const convertPopulateObject = (populate, schema) => {
return acc; return acc;
} }
// FIXME: This is a temporary solution for dynamic zones that should be // Allow adding an 'on' strategy to populate queries for polymorphic relations, media and dynamic zones
// fixed when we'll implement a more accurate way to query them const isAllowedAttributeForFragmentPopulate =
isDynamicZoneAttribute(attribute) ||
isMediaAttribute(attribute) ||
isMorphToRelationalAttribute(attribute);
const hasFragmentPopulateDefined = typeof subPopulate === 'object' && 'on' in subPopulate;
if (isAllowedAttributeForFragmentPopulate && hasFragmentPopulateDefined) {
return {
...acc,
[key]: {
on: Object.entries(subPopulate.on).reduce(
(acc, [type, typeSubPopulate]) => ({
...acc,
[type]: convertNestedPopulate(typeSubPopulate, strapi.getModel(type)),
}),
{}
),
},
};
}
// TODO: This is a query's populate fallback for DynamicZone and is kept for legacy purpose.
// Removing it could break existing user queries but it should be removed in V5.
if (attribute.type === 'dynamiczone') { if (attribute.type === 'dynamiczone') {
const populates = attribute.components const populates = attribute.components
.map((uid) => strapi.getModel(uid)) .map((uid) => strapi.getModel(uid))