Use new filters format in the upload plugin

This commit is contained in:
Pierre Noël 2021-09-20 18:50:48 +02:00
parent 9ce23475bc
commit d02c7ab741
12 changed files with 179 additions and 209 deletions

View File

@ -6,17 +6,19 @@ module.exports = {
displayName: 'Is creator',
name: 'is-creator',
plugin: 'admin',
handler: user => ({ 'createdBy.id': user.id }),
handler: user => ({ created_by: { id: user.id } }),
},
{
displayName: 'Has same role as creator',
name: 'has-same-role-as-creator',
plugin: 'admin',
handler: user => ({
'createdBy.roles': {
$elemMatch: {
id: {
$in: user.roles.map(r => r.id),
created_by: {
roles: {
$elemMatch: {
id: {
$in: user.roles.map(r => r.id),
},
},
},
},

View File

@ -56,7 +56,7 @@ describe('Permissions Manager', () => {
model: 'foo',
});
const expected = [{ kai: 'doe' }];
const expected = { $and: [{ kai: 'doe' }] };
expect(pm.getQuery()).toStrictEqual(expected);
});
@ -179,7 +179,7 @@ describe('Permissions Manager', () => {
});
});
describe('queryFrom', () => {
describe('addPermissionsQueryTo', () => {
const ability = defineAbility(can =>
can('read', 'article', ['title'], { $and: [{ title: 'foo' }] })
);
@ -189,25 +189,27 @@ describe('Permissions Manager', () => {
model: 'article',
});
const pmQuery = [{ title: 'foo' }];
const pmQuery = { $and: [{ title: 'foo' }] };
test('Create query from simple object', () => {
const query = { _limit: 100 };
const expected = { _limit: 100, _where: pmQuery };
const query = { limit: 100 };
const expected = { limit: 100, filters: pmQuery };
const res = pm.queryFrom(query);
const res = pm.addPermissionsQueryTo(query);
expect(res).toStrictEqual(expected);
});
test('Create query from complex object', () => {
const query = { _limit: 100, _where: [{ a: 'b' }, { c: 'd' }] };
const query = { limit: 100, filters: { $and: [{ a: 'b' }, { c: 'd' }] } };
const expected = {
_limit: 100,
_where: [{ a: 'b' }, { c: 'd' }, ...pmQuery],
limit: 100,
filters: {
$and: [query.filters, pmQuery],
},
};
const res = pm.queryFrom(query);
const res = pm.addPermissionsQueryTo(query);
expect(res).toStrictEqual(expected);
});
@ -216,63 +218,75 @@ describe('Permissions Manager', () => {
describe('buildStrapiQuery', () => {
const tests = [
['No transform', { foo: 'bar' }, { foo: 'bar' }],
['Simple op', { foo: { $eq: 'bar' } }, { foo_eq: 'bar' }],
['Nested property', { foo: { nested: 'bar' } }, { 'foo.nested': 'bar' }],
['Simple op', { foo: { $eq: 'bar' } }, { foo: { $eq: 'bar' } }],
['Nested property', { foo: { nested: 'bar' } }, { foo: { nested: 'bar' } }],
[
'Deeply nested property',
{ foo: { nested: { again: 'bar' } } },
{ 'foo.nested.again': 'bar' },
{ foo: { nested: { again: 'bar' } } },
],
['Op with array', { foo: { $in: ['bar', 'rab'] } }, { foo_in: ['bar', 'rab'] }],
['Removable op', { foo: { $elemMatch: { a: 'b' } } }, { 'foo.a': 'b' }],
['Op with array', { foo: { $in: ['bar', 'rab'] } }, { foo: { $in: ['bar', 'rab'] } }],
['Removable op', { foo: { $elemMatch: { a: 'b' } } }, { foo: { $and: [{ a: 'b' }] } }],
[
'Combination of removable and basic ops',
{ foo: { $elemMatch: { a: { $in: [1, 2, 3] } } } },
{ 'foo.a_in': [1, 2, 3] },
{ foo: { $and: [{ a: { $in: [1, 2, 3] } }] } },
],
[
'Decoupling of nested properties with/without op',
{ foo: { $elemMatch: { a: { $in: [1, 2, 3] }, b: 'c' } } },
{ 'foo.a_in': [1, 2, 3], 'foo.b': 'c' },
{ foo: { $and: [{ a: { $in: [1, 2, 3] }, b: 'c' }] } },
],
[
'OR op and properties decoupling',
{ $or: [{ foo: { a: 2 } }, { foo: { b: 3 } }] },
{ _or: [{ 'foo.a': 2 }, { 'foo.b': 3 }] },
{ $or: [{ foo: { a: 2 } }, { foo: { b: 3 } }] },
],
[
'OR op with nested properties & ops',
{ $or: [{ foo: { a: 2 } }, { foo: { b: { $in: [1, 2, 3] } } }] },
{ _or: [{ 'foo.a': 2 }, { 'foo.b_in': [1, 2, 3] }] },
{ $or: [{ foo: { a: 2 } }, { foo: { b: { $in: [1, 2, 3] } } }] },
],
[
'Nested OR op',
{ $or: [{ $or: [{ a: 2 }, { a: 3 }] }] },
{ _or: [{ _or: [{ a: 2 }, { a: 3 }] }] },
{ $or: [{ $or: [{ a: 2 }, { a: 3 }] }] },
],
[
'OR op with nested AND op',
{ $or: [{ a: 2 }, [{ a: 3 }, { $or: [{ b: 1 }, { b: 4 }] }]] },
{ _or: [{ a: 2 }, [{ a: 3 }, { _or: [{ b: 1 }, { b: 4 }] }]] },
{ $or: [{ a: 2 }, [{ a: 3 }, { $or: [{ b: 1 }, { b: 4 }] }]] },
],
[
'OR op with nested AND op and nested properties',
{ _or: [{ a: 2 }, [{ a: 3 }, { b: { c: 'foo' } }]] },
{ _or: [{ a: 2 }, [{ a: 3 }, { 'b.c': 'foo' }]] },
{ $or: [{ a: 2 }, [{ a: 3 }, { b: { c: 'foo' } }]] },
{ $or: [{ a: 2 }, [{ a: 3 }, { b: { c: 'foo' } }]] },
],
[
'Literal nested property with removable op',
{
'createdBy.roles': {
$elemMatch: {
id: {
$in: [1, 2, 3],
created_by: {
roles: {
$elemMatch: {
id: {
$in: [1, 2, 3],
},
},
},
},
},
{
'createdBy.roles.id_in': [1, 2, 3],
created_by: {
roles: {
$and: [
{
id: {
$in: [1, 2, 3],
},
},
],
},
},
},
],
];

View File

@ -1,7 +1,7 @@
'use strict';
const _ = require('lodash');
const { cloneDeep, isObject, set, isArray } = require('lodash/fp');
const { cloneDeep, isPlainObject } = require('lodash/fp');
const { subject: asSubject } = require('@casl/ability');
const { permittedFieldsOf } = require('@casl/ability/extra');
const {
@ -35,22 +35,15 @@ module.exports = ({ ability, action, model }) => ({
return buildStrapiQuery(buildCaslQuery(ability, queryAction, model));
},
// FIXME:
queryFrom(query = {}, action) {
addPermissionsQueryTo(query = {}, action) {
const newQuery = cloneDeep(query);
const permissionQuery = this.getQuery(action);
const newQuery = cloneDeep(query);
const { _where } = query;
newQuery.filters = isPlainObject(query.filters)
? { $and: [query.filters, permissionQuery] }
: permissionQuery;
if (isObject(_where) && !isArray(_where)) {
Object.assign(newQuery, { _where: [_where] });
}
if (!_where) {
Object.assign(newQuery, { _where: [] });
}
return set('_where', newQuery._where.concat(permissionQuery), newQuery);
return newQuery;
},
sanitize(data, options = {}) {

View File

@ -13,69 +13,34 @@ const ops = {
const buildCaslQuery = (ability, action, model) => {
const query = rulesToQuery(ability, action, model, o => o.conditions);
return _.get(query, '$or[0].$and', {});
return _.get(query, '$or[0]', {});
};
const buildStrapiQuery = caslQuery => {
const transform = _.flow([flattenDeep, cleanupUnwantedProperties]);
return transform(caslQuery);
const transform = _.flow([removeCleanable]);
const res = transform(caslQuery);
return res;
};
const flattenDeep = condition => {
const removeCleanable = condition => {
if (_.isArray(condition)) {
return _.map(condition, flattenDeep);
return _.map(condition, removeCleanable);
}
if (!_.isObject(condition)) {
return condition;
}
const shouldIgnore = e => !!ops.common.includes(e);
const shouldPerformTransformation = (v, k) => _.isObject(v) && !_.isArray(v) && !shouldIgnore(k);
const result = {};
const set = (key, value) => (result[key] = value);
const getTransformParams = (prevKey, v, k) =>
shouldIgnore(k) ? [`${prevKey}_${k.replace('$', '')}`, v] : [`${prevKey}.${k}`, v];
_.each(condition, (value, key) => {
if (ops.boolean.includes(key)) {
set(key.replace('$', '_'), _.map(value, flattenDeep));
} else if (shouldPerformTransformation(value, key)) {
_.each(flattenDeep(value), (v, k) => {
set(...getTransformParams(key, v, k));
});
} else {
set(key, flattenDeep(value));
}
});
return result;
};
const cleanupUnwantedProperties = condition => {
if (!_.isObject(condition)) {
return condition;
}
if (_.isArray(condition)) {
return condition.map(cleanupUnwantedProperties);
}
const shouldClean = e =>
typeof e === 'string' ? ops.cleanable.find(o => e.includes(`.${o}`)) : undefined;
return _.reduce(
condition,
(acc, value, key) => {
const keyToClean = shouldClean(key);
const newKey = keyToClean ? key.split(`.${keyToClean}`).join('') : key;
(newCondition, value, key) => {
if (ops.cleanable.includes(key)) {
newCondition.$and = [removeCleanable(value)];
} else {
newCondition[key] = removeCleanable(value);
}
return {
...acc,
[newKey]: _.isArray(value) ? value.map(cleanupUnwantedProperties) : value,
};
return newCondition;
},
{}
);

View File

@ -43,7 +43,8 @@ const createPermissionChecker = strapi => ({ userAbility, model }) => {
const sanitizeCreateInput = data => sanitizeInput(ACTIONS.create, data);
const sanitizeUpdateInput = entity => data => sanitizeInput(ACTIONS.update, data, entity);
const buildPermissionQuery = (query, action) => permissionsManager.queryFrom(query, action);
const buildPermissionQuery = (query, action) =>
permissionsManager.addPermissionsQueryTo(query, action);
const buildReadQuery = query => buildPermissionQuery(query, ACTIONS.read);
const buildDeleteQuery = query => buildPermissionQuery(query, ACTIONS.delete);

View File

@ -1,10 +1,10 @@
'use strict';
const _ = require('lodash');
const { contentTypes: contentTypesUtils } = require('@strapi/utils');
const validateSettings = require('../validation/settings');
const validateUploadBody = require('../validation/upload');
const { getService } = require('../../utils');
const { contentTypes: contentTypesUtils, sanitizeEntity } = require('@strapi/utils');
const { getService } = require('../utils');
const validateSettings = require('./validation/settings');
const validateUploadBody = require('./validation/upload');
const { CREATED_BY_ATTRIBUTE } = contentTypesUtils.constants;
@ -35,7 +35,8 @@ module.exports = {
return ctx.forbidden();
}
const query = pm.queryFrom(ctx.query);
const query = pm.addPermissionsQueryTo(ctx.query);
const files = await getService('upload').fetchAll(query);
ctx.body = pm.sanitize(files, { withPrivate: false });
@ -68,7 +69,7 @@ module.exports = {
return ctx.forbidden();
}
const query = pm.queryFrom(ctx.query);
const query = pm.addPermissionsQueryTo(ctx.query);
const count = await getService('upload').count(query);
ctx.body = { count };
@ -185,6 +186,37 @@ module.exports = {
ctx.body = pm.sanitize(uploadedFiles, { action: ACTIONS.read, withPrivate: false });
},
async upload(ctx) {
const {
query: { id },
request: { files: { files } = {} },
} = ctx;
if (id && (_.isEmpty(files) || files.size === 0)) {
return this.updateFileInfo(ctx);
}
if (_.isEmpty(files) || files.size === 0) {
throw strapi.errors.badRequest(null, {
errors: [{ id: 'Upload.status.empty', message: 'Files are empty' }],
});
}
await (id ? this.replaceFile : this.uploadFiles)(ctx);
},
async search(ctx) {
const { id } = ctx.params;
const model = strapi.getModel('plugin::upload.file');
const entries = await strapi.query('plugin::upload.file').findMany({
where: {
$or: [{ hash: { $contains: id } }, { name: { $contains: id } }],
},
});
ctx.body = sanitizeEntity(entries, { model });
},
};
const findEntityAndCheckPermissions = async (ability, action, model, id) => {

View File

@ -1,9 +1,10 @@
'use strict';
const _ = require('lodash');
const { sanitizeEntity } = require('@strapi/utils');
const validateSettings = require('../validation/settings');
const validateUploadBody = require('../validation/upload');
const { getService } = require('../../utils');
const { getService } = require('../utils');
const validateSettings = require('./validation/settings');
const validateUploadBody = require('./validation/upload');
const sanitize = (data, options = {}) => {
return sanitizeEntity(data, {
@ -118,4 +119,35 @@ module.exports = {
ctx.body = sanitize(uploadedFiles);
},
async upload(ctx) {
const {
query: { id },
request: { files: { files } = {} },
} = ctx;
if (id && (_.isEmpty(files) || files.size === 0)) {
return this.updateFileInfo(ctx);
}
if (_.isEmpty(files) || files.size === 0) {
throw strapi.errors.badRequest(null, {
errors: [{ id: 'Upload.status.empty', message: 'Files are empty' }],
});
}
await (id ? this.replaceFile : this.uploadFiles)(ctx);
},
async search(ctx) {
const { id } = ctx.params;
const model = strapi.getModel('plugin::upload.file');
const entries = await strapi.query('plugin::upload.file').findMany({
where: {
$or: [{ hash: { $contains: id } }, { name: { $contains: id } }],
},
});
ctx.body = sanitizeEntity(entries, { model });
},
};

View File

@ -1,7 +1,9 @@
'use strict';
const upload = require('./upload');
const adminApi = require('./admin-api');
const contentApi = require('./content-api');
module.exports = {
upload,
'admin-api': adminApi,
'content-api': contentApi,
};

View File

@ -1,72 +0,0 @@
'use strict';
/**
* Upload.js controller
*
*/
const _ = require('lodash');
const { sanitizeEntity } = require('@strapi/utils');
const apiUploadController = require('./upload/api');
const adminUploadController = require('./upload/admin');
const resolveController = ctx => {
const {
state: { isAuthenticatedAdmin },
} = ctx;
return isAuthenticatedAdmin ? adminUploadController : apiUploadController;
};
const resolveControllerMethod = method => ctx => {
const controller = resolveController(ctx);
const callbackFn = controller[method];
if (!_.isFunction(callbackFn)) {
return ctx.notFound();
}
return callbackFn(ctx);
};
module.exports = {
find: resolveControllerMethod('find'),
findOne: resolveControllerMethod('findOne'),
count: resolveControllerMethod('count'),
destroy: resolveControllerMethod('destroy'),
updateSettings: resolveControllerMethod('updateSettings'),
getSettings: resolveControllerMethod('getSettings'),
async upload(ctx) {
const {
query: { id },
request: { files: { files } = {} },
} = ctx;
const controller = resolveController(ctx);
if (id && (_.isEmpty(files) || files.size === 0)) {
return controller.updateFileInfo(ctx);
}
if (_.isEmpty(files) || files.size === 0) {
throw strapi.errors.badRequest(null, {
errors: [{ id: 'Upload.status.empty', message: 'Files are empty' }],
});
}
await (id ? controller.replaceFile : controller.uploadFiles)(ctx);
},
async search(ctx) {
const { id } = ctx.params;
const model = strapi.getModel('plugin::upload.file');
const entries = await strapi.query('plugin::upload.file').findMany({
where: {
$or: [{ hash: { $contains: id } }, { name: { $contains: id } }],
},
});
ctx.body = sanitizeEntity(entries, { model });
},
};

View File

@ -6,7 +6,7 @@ module.exports = {
{
method: 'GET',
path: '/settings',
handler: 'upload.getSettings',
handler: 'admin-api.getSettings',
config: {
policies: [
'admin::isAuthenticatedAdmin',
@ -22,7 +22,7 @@ module.exports = {
{
method: 'PUT',
path: '/settings',
handler: 'upload.updateSettings',
handler: 'admin-api.updateSettings',
config: {
policies: [
'admin::isAuthenticatedAdmin',
@ -38,7 +38,7 @@ module.exports = {
{
method: 'POST',
path: '/',
handler: 'upload.upload',
handler: 'admin-api.upload',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
@ -46,7 +46,7 @@ module.exports = {
{
method: 'GET',
path: '/files/count',
handler: 'upload.count',
handler: 'admin-api.count',
config: {
policies: [
'admin::isAuthenticatedAdmin',
@ -62,7 +62,7 @@ module.exports = {
{
method: 'GET',
path: '/files',
handler: 'upload.find',
handler: 'admin-api.find',
config: {
policies: [
'admin::isAuthenticatedAdmin',
@ -78,7 +78,7 @@ module.exports = {
{
method: 'GET',
path: '/files/:id',
handler: 'upload.findOne',
handler: 'admin-api.findOne',
config: {
policies: [
'admin::isAuthenticatedAdmin',
@ -94,7 +94,7 @@ module.exports = {
{
method: 'GET',
path: '/search/:id',
handler: 'upload.search',
handler: 'admin-api.search',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
@ -102,7 +102,7 @@ module.exports = {
{
method: 'DELETE',
path: '/files/:id',
handler: 'upload.destroy',
handler: 'admin-api.destroy',
config: {
policies: [
'admin::isAuthenticatedAdmin',

View File

@ -6,27 +6,27 @@ module.exports = {
{
method: 'POST',
path: '/',
handler: 'upload.upload',
handler: 'content-api.upload',
},
{
method: 'GET',
path: '/files/count',
handler: 'upload.count',
handler: 'content-api.count',
},
{
method: 'GET',
path: '/files',
handler: 'upload.find',
handler: 'content-api.find',
},
{
method: 'GET',
path: '/files/:id',
handler: 'upload.findOne',
handler: 'content-api.findOne',
},
{
method: 'DELETE',
path: '/files/:id',
handler: 'upload.destroy',
handler: 'content-api.destroy',
},
],
};

View File

@ -44,10 +44,11 @@ const sendMediaMetrics = data => {
};
const combineFilters = params => {
// FIXME: until we support boolean operators for querying we need to make mime_ncontains use AND instead of OR
if (_.has(params, 'mime_ncontains') && Array.isArray(params.mime_ncontains)) {
params._where = params.mime_ncontains.map(val => ({ mime_ncontains: val }));
const mimeFilters = { $or: params.mime_ncontains.map(val => ({ mime_ncontains: val })) };
delete params.mime_ncontains;
params.filters = params.filters ? { $and: [params.filters, mimeFilters] } : mimeFilters;
}
};
@ -201,7 +202,7 @@ module.exports = ({ strapi }) => ({
caption: _.isNil(caption) ? dbFile.caption : caption,
};
return this.update({ id }, newInfos, { user });
return this.update(id, newInfos, { user });
},
async replace(id, { data, file }, { user } = {}) {
@ -274,20 +275,20 @@ module.exports = ({ strapi }) => ({
height,
});
return this.update({ id }, fileData, { user });
return this.update(id, fileData, { user });
},
async update(params, values, { user } = {}) {
async update(id, values, { user } = {}) {
const fileValues = { ...values };
if (user) {
fileValues[UPDATED_BY_ATTRIBUTE] = user.id;
}
sendMediaMetrics(fileValues);
//
const res = await strapi
.query('plugin::upload.file')
.update({ where: params, data: fileValues });
// const res = await strapi
// .query('plugin::upload.file')
// .update({ where: params, data: fileValues });
const res = await strapi.entityService.update('plugin::upload.file', id, { data: fileValues });
this.emitEvent(MEDIA_UPDATE, res);
@ -309,18 +310,18 @@ module.exports = ({ strapi }) => ({
return res;
},
findOne(params, populate) {
return strapi.query('plugin::upload.file').findOne({ where: params, populate });
findOne(filters, populate) {
return strapi.entityService.findOne('plugin::upload.file', { params: { filters, populate } });
},
fetchAll(params) {
combineFilters(params);
return strapi.query('plugin::upload.file').findMany({ ...params });
fetchAll(query) {
combineFilters(query);
return strapi.entityService.find('plugin::upload.file', { params: query });
},
count(params) {
combineFilters(params);
return strapi.query('plugin::upload.file').count({ ...params });
count(query) {
combineFilters(query);
return strapi.entityService.count('plugin::upload.file', { params: query });
},
async remove(file) {