Merge branch 'features/api-token-v2' into api-token-v2/select-all-bug

This commit is contained in:
Simone Taeggi 2022-09-14 14:48:04 +02:00
commit a1ce4d8fa6
7 changed files with 180 additions and 42 deletions

View File

@ -80,7 +80,7 @@ Complete installation requirements can be found in the documentation under <a hr
- CentOS/RHEL 8 - CentOS/RHEL 8
- macOS Mojave - macOS Mojave
- Windows 10 - Windows 10
- Docker - [Docker-Repo](https://github.com/strapi/strapi-docker) - Docker
(Please note that Strapi may work on other operating systems, but these are not tested nor officially supported at this time.) (Please note that Strapi may work on other operating systems, but these are not tested nor officially supported at this time.)

View File

@ -47,7 +47,7 @@ const DynamicTable = ({
...metadatas, ...metadatas,
label: formatMessage({ label: formatMessage({
id: getTrad(`containers.ListPage.table-headers.${header.name}`), id: getTrad(`containers.ListPage.table-headers.${header.name}`),
defaultMessage: header.name, defaultMessage: metadatas.label,
}), }),
}, },
name: sortFieldValue, name: sortFieldValue,
@ -60,7 +60,7 @@ const DynamicTable = ({
...metadatas, ...metadatas,
label: formatMessage({ label: formatMessage({
id: getTrad(`containers.ListPage.table-headers.${header.name}`), id: getTrad(`containers.ListPage.table-headers.${header.name}`),
defaultMessage: header.name, defaultMessage: metadatas.label,
}), }),
}, },
}; };

View File

@ -1,19 +1,32 @@
'use strict'; 'use strict';
const _ = require('lodash/fp'); const {
const types = require('./types'); isUndefined,
const { createField } = require('./fields'); castArray,
const { createQueryBuilder } = require('./query'); isNil,
has,
isString,
isInteger,
pick,
isPlainObject,
isEmpty,
isArray,
isNull,
} = require('lodash/fp');
const types = require('../types');
const { createField } = require('../fields');
const { createQueryBuilder } = require('../query');
const { createRepository } = require('./entity-repository'); const { createRepository } = require('./entity-repository');
const { isBidirectional, isOneToAny } = require('./metadata/relations'); const { isBidirectional, isOneToAny } = require('../metadata/relations');
const { deleteRelatedMorphOneRelationsAfterMorphToManyUpdate } = require('./morph-relations');
const toId = (value) => value.id || value; const toId = (value) => value.id || value;
const toIds = (value) => _.castArray(value || []).map(toId); const toIds = (value) => castArray(value || []).map(toId);
const isValidId = (value) => _.isString(value) || _.isInteger(value); const isValidId = (value) => isString(value) || isInteger(value);
const toAssocs = (data) => { const toAssocs = (data) => {
return _.castArray(data) return castArray(data)
.filter((datum) => !_.isNil(datum)) .filter((datum) => !isNil(datum))
.map((datum) => { .map((datum) => {
// if it is a string or an integer return an obj with id = to datum // if it is a string or an integer return an obj with id = to datum
if (isValidId(datum)) { if (isValidId(datum)) {
@ -21,7 +34,7 @@ const toAssocs = (data) => {
} }
// if it is an object check it has at least a valid id // if it is an object check it has at least a valid id
if (!_.has('id', datum) || !isValidId(datum.id)) { if (!has('id', datum) || !isValidId(datum.id)) {
throw new Error(`Invalid id, expected a string or integer, got ${datum}`); throw new Error(`Invalid id, expected a string or integer, got ${datum}`);
} }
@ -40,8 +53,8 @@ const processData = (metadata, data = {}, { withDefaults = false } = {}) => {
if (types.isScalar(attribute.type)) { if (types.isScalar(attribute.type)) {
const field = createField(attribute); const field = createField(attribute);
if (_.isUndefined(data[attributeName])) { if (isUndefined(data[attributeName])) {
if (!_.isUndefined(attribute.default) && withDefaults) { if (!isUndefined(attribute.default) && withDefaults) {
if (typeof attribute.default === 'function') { if (typeof attribute.default === 'function') {
obj[attributeName] = attribute.default(); obj[attributeName] = attribute.default();
} else { } else {
@ -66,11 +79,11 @@ const processData = (metadata, data = {}, { withDefaults = false } = {}) => {
const joinColumnName = attribute.joinColumn.name; const joinColumnName = attribute.joinColumn.name;
// allow setting to null // allow setting to null
const attrValue = !_.isUndefined(data[attributeName]) const attrValue = !isUndefined(data[attributeName])
? data[attributeName] ? data[attributeName]
: data[joinColumnName]; : data[joinColumnName];
if (!_.isUndefined(attrValue)) { if (!isUndefined(attrValue)) {
obj[joinColumnName] = attrValue; obj[joinColumnName] = attrValue;
} }
@ -91,8 +104,8 @@ const processData = (metadata, data = {}, { withDefaults = false } = {}) => {
continue; continue;
} }
if (!_.isUndefined(value)) { if (!isUndefined(value)) {
if (!_.has('id', value) || !_.has(typeField, value)) { if (!has('id', value) || !has(typeField, value)) {
throw new Error(`Expects properties ${typeField} an id to make a morph association`); throw new Error(`Expects properties ${typeField} an id to make a morph association`);
} }
@ -137,7 +150,7 @@ const createEntityManager = (db) => {
const states = await db.lifecycles.run('beforeCount', uid, { params }); const states = await db.lifecycles.run('beforeCount', uid, { params });
const res = await this.createQueryBuilder(uid) const res = await this.createQueryBuilder(uid)
.init(_.pick(['_q', 'where', 'filters'], params)) .init(pick(['_q', 'where', 'filters'], params))
.count() .count()
.first() .first()
.execute(); .execute();
@ -155,7 +168,7 @@ const createEntityManager = (db) => {
const metadata = db.metadata.get(uid); const metadata = db.metadata.get(uid);
const { data } = params; const { data } = params;
if (!_.isPlainObject(data)) { if (!isPlainObject(data)) {
throw new Error('Create expects a data object'); throw new Error('Create expects a data object');
} }
@ -187,7 +200,7 @@ const createEntityManager = (db) => {
const metadata = db.metadata.get(uid); const metadata = db.metadata.get(uid);
const { data } = params; const { data } = params;
if (!_.isArray(data)) { if (!isArray(data)) {
throw new Error('CreateMany expects data to be an array'); throw new Error('CreateMany expects data to be an array');
} }
@ -195,7 +208,7 @@ const createEntityManager = (db) => {
processData(metadata, datum, { withDefaults: true }) processData(metadata, datum, { withDefaults: true })
); );
if (_.isEmpty(dataToInsert)) { if (isEmpty(dataToInsert)) {
throw new Error('Nothing to insert'); throw new Error('Nothing to insert');
} }
@ -214,11 +227,11 @@ const createEntityManager = (db) => {
const metadata = db.metadata.get(uid); const metadata = db.metadata.get(uid);
const { where, data } = params; const { where, data } = params;
if (!_.isPlainObject(data)) { if (!isPlainObject(data)) {
throw new Error('Update requires a data object'); throw new Error('Update requires a data object');
} }
if (_.isEmpty(where)) { if (isEmpty(where)) {
throw new Error('Update requires a where parameter'); throw new Error('Update requires a where parameter');
} }
@ -232,7 +245,7 @@ const createEntityManager = (db) => {
const dataToUpdate = processData(metadata, data); const dataToUpdate = processData(metadata, data);
if (!_.isEmpty(dataToUpdate)) { if (!isEmpty(dataToUpdate)) {
await this.createQueryBuilder(uid).where({ id }).update(dataToUpdate).execute(); await this.createQueryBuilder(uid).where({ id }).update(dataToUpdate).execute();
} }
@ -259,7 +272,7 @@ const createEntityManager = (db) => {
const dataToUpdate = processData(metadata, data); const dataToUpdate = processData(metadata, data);
if (_.isEmpty(dataToUpdate)) { if (isEmpty(dataToUpdate)) {
throw new Error('Update requires data'); throw new Error('Update requires data');
} }
@ -280,7 +293,7 @@ const createEntityManager = (db) => {
const { where, select, populate } = params; const { where, select, populate } = params;
if (_.isEmpty(where)) { if (isEmpty(where)) {
throw new Error('Delete requires a where parameter'); throw new Error('Delete requires a where parameter');
} }
@ -336,7 +349,7 @@ const createEntityManager = (db) => {
for (const attributeName of Object.keys(attributes)) { for (const attributeName of Object.keys(attributes)) {
const attribute = attributes[attributeName]; const attribute = attributes[attributeName];
const isValidLink = _.has(attributeName, data) && !_.isNil(data[attributeName]); const isValidLink = has(attributeName, data) && !isNil(data[attributeName]);
if (attribute.type !== 'relation' || !isValidLink) { if (attribute.type !== 'relation' || !isValidLink) {
continue; continue;
@ -373,7 +386,7 @@ const createEntityManager = (db) => {
}; };
}); });
if (_.isEmpty(rows)) { if (isEmpty(rows)) {
continue; continue;
} }
@ -398,10 +411,18 @@ const createEntityManager = (db) => {
...(data.__pivot || {}), ...(data.__pivot || {}),
})); }));
if (_.isEmpty(rows)) { if (isEmpty(rows)) {
continue; continue;
} }
// delete previous relations
await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
uid,
attributeName,
joinTable,
db,
});
await this.createQueryBuilder(joinTable.name).insert(rows).execute(); await this.createQueryBuilder(joinTable.name).insert(rows).execute();
continue; continue;
@ -450,7 +471,7 @@ const createEntityManager = (db) => {
if (isOneToAny(attribute) && isBidirectional(attribute)) { if (isOneToAny(attribute) && isBidirectional(attribute)) {
await this.createQueryBuilder(joinTable.name) await this.createQueryBuilder(joinTable.name)
.delete() .delete()
.where({ [inverseJoinColumn.name]: _.castArray(data[attributeName]) }) .where({ [inverseJoinColumn.name]: castArray(data[attributeName]) })
.where(joinTable.on || {}) .where(joinTable.on || {})
.execute(); .execute();
} }
@ -490,7 +511,7 @@ const createEntityManager = (db) => {
for (const attributeName of Object.keys(attributes)) { for (const attributeName of Object.keys(attributes)) {
const attribute = attributes[attributeName]; const attribute = attributes[attributeName];
if (attribute.type !== 'relation' || !_.has(attributeName, data)) { if (attribute.type !== 'relation' || !has(attributeName, data)) {
continue; continue;
} }
@ -503,12 +524,14 @@ const createEntityManager = (db) => {
// set columns // set columns
const { idColumn, typeColumn } = targetAttribute.morphColumn; const { idColumn, typeColumn } = targetAttribute.morphColumn;
// update instead of deleting because the relation is directly on the entity table
// and not in a join table
await this.createQueryBuilder(target) await this.createQueryBuilder(target)
.update({ [idColumn.name]: null, [typeColumn.name]: null }) .update({ [idColumn.name]: null, [typeColumn.name]: null })
.where({ [idColumn.name]: id, [typeColumn.name]: uid }) .where({ [idColumn.name]: id, [typeColumn.name]: uid })
.execute(); .execute();
if (!_.isNull(data[attributeName])) { if (!isNull(data[attributeName])) {
await this.createQueryBuilder(target) await this.createQueryBuilder(target)
.update({ [idColumn.name]: id, [typeColumn.name]: uid }) .update({ [idColumn.name]: id, [typeColumn.name]: uid })
.where({ id: toId(data[attributeName]) }) .where({ id: toId(data[attributeName]) })
@ -540,7 +563,7 @@ const createEntityManager = (db) => {
field: attributeName, field: attributeName,
})); }));
if (_.isEmpty(rows)) { if (isEmpty(rows)) {
continue; continue;
} }
@ -577,10 +600,18 @@ const createEntityManager = (db) => {
...(data.__pivot || {}), ...(data.__pivot || {}),
})); }));
if (_.isEmpty(rows)) { if (isEmpty(rows)) {
continue; continue;
} }
// delete previous relations
await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
uid,
attributeName,
joinTable,
db,
});
await this.createQueryBuilder(joinTable.name).insert(rows).execute(); await this.createQueryBuilder(joinTable.name).insert(rows).execute();
continue; continue;
@ -602,7 +633,7 @@ const createEntityManager = (db) => {
.update({ [attribute.joinColumn.referencedColumn]: null }) .update({ [attribute.joinColumn.referencedColumn]: null })
.execute(); .execute();
if (!_.isNull(data[attributeName])) { if (!isNull(data[attributeName])) {
await this.createQueryBuilder(target) await this.createQueryBuilder(target)
// NOTE: works if it is an array or a single id // NOTE: works if it is an array or a single id
.where({ id: data[attributeName] }) .where({ id: data[attributeName] })
@ -633,7 +664,7 @@ const createEntityManager = (db) => {
.execute(); .execute();
} }
if (!_.isNull(data[attributeName])) { if (!isNull(data[attributeName])) {
const insert = toAssocs(data[attributeName]).map((data) => { const insert = toAssocs(data[attributeName]).map((data) => {
return { return {
[joinColumn.name]: id, [joinColumn.name]: id,
@ -791,7 +822,7 @@ const createEntityManager = (db) => {
async load(uid, entity, fields, params) { async load(uid, entity, fields, params) {
const { attributes } = db.metadata.get(uid); const { attributes } = db.metadata.get(uid);
const fieldsArr = _.castArray(fields); const fieldsArr = castArray(fields);
fieldsArr.forEach((field) => { fieldsArr.forEach((field) => {
const attribute = attributes[field]; const attribute = attributes[field];
@ -814,7 +845,7 @@ const createEntityManager = (db) => {
} }
if (Array.isArray(fields)) { if (Array.isArray(fields)) {
return _.pick(fields, entry); return pick(fields, entry);
} }
return entry[fields]; return entry[fields];

View File

@ -0,0 +1,59 @@
'use strict';
const { groupBy, pipe, mapValues, map, isEmpty } = require('lodash/fp');
const { createQueryBuilder } = require('../query');
const getMorphToManyRowsLinkedToMorphOne = (rows, { uid, attributeName, typeColumn, db }) =>
rows.filter((row) => {
const relatedType = row[typeColumn.name];
const field = row.field;
const targetAttribute = db.metadata.get(relatedType).attributes[field];
// ensure targeted field is the right one + check if it is a morphOne
return (
targetAttribute?.target === uid &&
targetAttribute?.morphBy === attributeName &&
targetAttribute?.relation === 'morphOne'
);
});
const deleteRelatedMorphOneRelationsAfterMorphToManyUpdate = async (
rows,
{ uid, attributeName, joinTable, db }
) => {
const { morphColumn } = joinTable;
const { idColumn, typeColumn } = morphColumn;
const morphOneRows = getMorphToManyRowsLinkedToMorphOne(rows, {
uid,
attributeName,
typeColumn,
db,
});
const groupByType = groupBy(typeColumn.name);
const groupByField = groupBy('field');
const typeAndFieldIdsGrouped = pipe(groupByType, mapValues(groupByField))(morphOneRows);
const orWhere = [];
for (const [type, v] of Object.entries(typeAndFieldIdsGrouped)) {
for (const [field, arr] of Object.entries(v)) {
orWhere.push({
[typeColumn.name]: type,
field,
[idColumn.name]: { $in: map(idColumn.name, arr) },
});
}
}
if (!isEmpty(orWhere)) {
await createQueryBuilder(joinTable.name, db).delete().where({ $or: orWhere }).execute();
}
};
module.exports = {
deleteRelatedMorphOneRelationsAfterMorphToManyUpdate,
};

View File

@ -42,7 +42,7 @@
"react-redux": "7.2.8", "react-redux": "7.2.8",
"react-router": "^5.2.0", "react-router": "^5.2.0",
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",
"sharp": "0.30.7" "sharp": "0.31.0"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/dom": "8.17.1", "@testing-library/dom": "8.17.1",

View File

@ -9,6 +9,7 @@ const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
const { createContentAPIRequest } = require('../../../../../test/helpers/request'); const { createContentAPIRequest } = require('../../../../../test/helpers/request');
const builder = createTestBuilder(); const builder = createTestBuilder();
const data = { dogs: [] };
let strapi; let strapi;
let rq; let rq;
@ -24,7 +25,7 @@ const dogModel = {
}, },
}; };
describe('Upload plugin end to end tests', () => { describe('Upload plugin', () => {
beforeAll(async () => { beforeAll(async () => {
await builder.addContentType(dogModel).build(); await builder.addContentType(dogModel).build();
strapi = await createStrapiInstance(); strapi = await createStrapiInstance();
@ -155,6 +156,8 @@ describe('Upload plugin end to end tests', () => {
id: expect.anything(), id: expect.anything(),
}, },
}); });
data.dogs.push(res.body);
}); });
test('With a pdf', async () => { test('With a pdf', async () => {
@ -183,6 +186,51 @@ describe('Upload plugin end to end tests', () => {
id: expect.anything(), id: expect.anything(),
}, },
}); });
data.dogs.push(res.body);
});
});
// see https://github.com/strapi/strapi/issues/14125
describe('File relations are correctly removed', () => {
test('Update an entity with a file correctly removes the relation between the entity and its old file', async () => {
const res = await rq({
method: 'PUT',
url: `/dogs/${data.dogs[0].data.id}?populate=*`,
formData: {
data: '{}',
'files.profilePicture': fs.createReadStream(path.join(__dirname, '../utils/strapi.jpg')),
},
});
expect(res.statusCode).toBe(200);
expect(res.body.data.attributes.profilePicture.data.id).not.toBe(
data.dogs[0].data.attributes.profilePicture.data.id
);
data.dogs[0] = res.body;
});
test('Update a file with an entity correctly removes the relation between the entity and its old file', async () => {
const fileId = data.dogs[1].data.attributes.profilePicture.data.id;
await strapi.entityService.update('plugin::upload.file', fileId, {
data: {
related: [
{
id: data.dogs[0].data.id,
__type: 'api::dog.dog',
__pivot: { field: 'profilePicture' },
},
],
},
});
const res = await rq({
method: 'GET',
url: `/dogs/${data.dogs[0].data.id}?populate=*`,
});
expect(res.body.data.attributes.profilePicture.data.id).toBe(fileId);
data.dogs[0] = res.body;
}); });
}); });
}); });