Merge branch 'cm-configure-view-lv' of https://github.com/strapi/strapi into cm-settings/drag-and-drop

This commit is contained in:
ronronscelestes 2021-10-18 16:54:24 +02:00
commit 5cce0eb854
30 changed files with 175 additions and 440 deletions

View File

@ -100,6 +100,7 @@ function ListView({
try { try {
const opts = source ? { cancelToken: source.token } : null; const opts = source ? { cancelToken: source.token } : null;
const { const {
data: { results, pagination: paginationResult }, data: { results, pagination: paginationResult },
} = await axiosInstance.get(endPoint, opts); } = await axiosInstance.get(endPoint, opts);

View File

@ -4,19 +4,16 @@ import createPluginsFilter from './createPluginsFilter';
/** /**
* Creates a valid query string from an object of queryParams * Creates a valid query string from an object of queryParams
* This includes: * This includes:
* - a _where clause * - a filters clause
* - plugin options * - plugin options
*/ */
const buildQueryString = (queryParams = {}) => { const buildQueryString = (queryParams = {}) => {
const _where = queryParams._where || [];
/** /**
* Extracting pluginOptions from the query since we don't want them to be part * Extracting pluginOptions from the query since we don't want them to be part
* of the url * of the url
*/ */
const { plugins: _, ...otherQueryParams } = { const { plugins: _, ...otherQueryParams } = {
...queryParams, ...queryParams,
_where,
...createPluginsFilter(queryParams.plugins), ...createPluginsFilter(queryParams.plugins),
}; };

View File

@ -1,13 +1,4 @@
const createPluginsFilter = obj => const createPluginsFilter = obj =>
Object.values(obj || {}).reduce((acc, current) => { Object.values(obj || {}).reduce((acc, current) => Object.assign(acc, current), {});
return {
...acc,
...Object.keys(current).reduce((accumulator, key) => {
accumulator[`_${key}`] = current[key];
return accumulator;
}, {}),
};
}, {});
export default createPluginsFilter; export default createPluginsFilter;

View File

@ -25,28 +25,28 @@ describe('buildQueryString', () => {
const queryString = buildQueryString(queryParams); const queryString = buildQueryString(queryParams);
expect(queryString).toBe('?page=1&pageSize=10&sort=name:ASC&_locale=en'); expect(queryString).toBe('?page=1&pageSize=10&sort=name:ASC&locale=en');
}); });
it('creates a valid query string with a _where clause', () => { it('creates a valid query string with a filters clause', () => {
const queryParams = { const queryParams = {
page: '1', page: '1',
pageSize: '10', pageSize: '10',
sort: 'name:ASC', sort: 'name:ASC',
_where: [{ name: 'hello world' }], filters: [{ name: 'hello world' }],
}; };
const queryString = buildQueryString(queryParams); const queryString = buildQueryString(queryParams);
expect(queryString).toBe('?page=1&pageSize=10&sort=name:ASC&_where[0][name]=hello world'); expect(queryString).toBe('?page=1&pageSize=10&sort=name:ASC&filters[0][name]=hello world');
}); });
it('creates a valid query string with a _where and plugin options', () => { it('creates a valid query string with a filters and plugin options', () => {
const queryParams = { const queryParams = {
page: '1', page: '1',
pageSize: '10', pageSize: '10',
sort: 'name:ASC', sort: 'name:ASC',
_where: [{ name: 'hello world' }], filters: [{ name: 'hello world' }],
plugins: { plugins: {
i18n: { locale: 'en' }, i18n: { locale: 'en' },
}, },
@ -55,7 +55,7 @@ describe('buildQueryString', () => {
const queryString = buildQueryString(queryParams); const queryString = buildQueryString(queryParams);
expect(queryString).toBe( expect(queryString).toBe(
'?page=1&pageSize=10&sort=name:ASC&_where[0][name]=hello world&_locale=en' '?page=1&pageSize=10&sort=name:ASC&filters[0][name]=hello world&locale=en'
); );
}); });
}); });

View File

@ -1,55 +0,0 @@
// List of all the possible filters
const VALID_REST_OPERATORS = [
'eq',
'ne',
'in',
'nin',
'contains',
'ncontains',
'containss',
'ncontainss',
'lt',
'lte',
'gt',
'gte',
'null',
];
// from strapi-utils/convert-rest-query-params
const findAppliedFilter = whereClause => {
// Useful to remove the mainField of relation fields.
const formattedWhereClause = whereClause.split('.')[0];
const separatorIndex = whereClause.lastIndexOf('_');
if (separatorIndex === -1) {
return { operator: '=', field: formattedWhereClause };
}
const fieldName = formattedWhereClause.substring(0, separatorIndex);
const operator = whereClause.slice(separatorIndex + 1);
// the field as underscores
if (!VALID_REST_OPERATORS.includes(operator)) {
return { operator: '=', field: formattedWhereClause };
}
return { operator: `_${operator}`, field: fieldName };
};
const formatFiltersFromQuery = ({ _where }) => {
if (!_where) {
return [];
}
return _where.map(obj => {
const [key] = Object.keys(obj);
const { field, operator } = findAppliedFilter(key);
const value = obj[key];
return { name: field, filter: operator, value };
});
};
export default formatFiltersFromQuery;
export { findAppliedFilter };

View File

@ -1,27 +0,0 @@
import { get } from 'lodash';
const formatFilterName = (name, metadatas) => {
const mainField = get(metadatas, [name, 'list', 'mainField', 'name'], null);
if (mainField) {
return `${name}.${mainField}`;
}
return name;
};
const formatFiltersToQuery = (array, metadatas) => {
const nextFilters = array.map(({ name, filter, value }) => {
const formattedName = formatFilterName(name, metadatas);
if (filter === '=') {
return { [formattedName]: value };
}
return { [`${formattedName}${filter}`]: value };
});
return { _where: nextFilters };
};
export default formatFiltersToQuery;

View File

@ -2,8 +2,6 @@ export { default as arrayMoveItem } from './arrayMoveItem';
export { default as checkIfAttributeIsDisplayable } from './checkIfAttributeIsDisplayable'; export { default as checkIfAttributeIsDisplayable } from './checkIfAttributeIsDisplayable';
export { default as createDefaultForm } from './createDefaultForm'; export { default as createDefaultForm } from './createDefaultForm';
export { default as dateFormats } from './dateFormats'; export { default as dateFormats } from './dateFormats';
export { default as formatFiltersFromQuery } from './formatFiltersFromQuery';
export { default as formatFiltersToQuery } from './formatFiltersToQuery';
export { default as formatLayoutToApi } from './formatLayoutToApi'; export { default as formatLayoutToApi } from './formatLayoutToApi';
export { default as generatePermissionsObject } from './generatePermissionsObject'; export { default as generatePermissionsObject } from './generatePermissionsObject';
export { default as getDisplayedValue } from './getDisplayedValue'; export { default as getDisplayedValue } from './getDisplayedValue';

View File

@ -1,66 +0,0 @@
import formatFiltersFromQuery, { findAppliedFilter } from '../formatFiltersFromQuery';
describe('CONTENT MANAGER | utils', () => {
describe('findAppliedFilter', () => {
it('should return the correct filter', () => {
expect(findAppliedFilter('categories.name')).toEqual({ operator: '=', field: 'categories' });
expect(findAppliedFilter('categories.name_lt')).toEqual({
operator: '_lt',
field: 'categories',
});
expect(findAppliedFilter('city')).toEqual({ operator: '=', field: 'city' });
expect(findAppliedFilter('city_nee')).toEqual({ operator: '=', field: 'city_nee' });
expect(findAppliedFilter('city_ne')).toEqual({ operator: '_ne', field: 'city' });
expect(findAppliedFilter('city_lt')).toEqual({ operator: '_lt', field: 'city' });
expect(findAppliedFilter('city_lte')).toEqual({ operator: '_lte', field: 'city' });
expect(findAppliedFilter('city_gt')).toEqual({ operator: '_gt', field: 'city' });
expect(findAppliedFilter('city_gte')).toEqual({ operator: '_gte', field: 'city' });
});
});
describe('formatFiltersFromQuery', () => {
it('should return an empty array if there is no where clause', () => {
expect(formatFiltersFromQuery({})).toHaveLength(0);
});
it('should return array of filter', () => {
const query = {
_where: [
{
city_ne_ne: 'paris',
},
{
city_ne: 'paris',
},
{
city: 'paris',
},
{
'categories.name_ne': 'first',
},
{
'like.numbers_lt': 34,
},
],
};
const expected = [
{ name: 'city_ne', filter: '_ne', value: 'paris' },
{ name: 'city', filter: '_ne', value: 'paris' },
{ name: 'city', filter: '=', value: 'paris' },
{
name: 'categories',
filter: '_ne',
value: 'first',
},
{
name: 'like',
filter: '_lt',
value: 34,
},
];
expect(formatFiltersFromQuery(query)).toEqual(expected);
});
});
});

View File

@ -1,57 +0,0 @@
import formatFiltersToQuery from '../formatFiltersToQuery';
describe('CONTENT MANAGER | utils', () => {
describe('formatFiltersToQuery', () => {
it('should return the filters query', () => {
const metadatas = {
categories: {
list: {
mainField: { name: 'name' },
},
},
like: {
list: {
mainField: { name: 'numbers' },
},
},
};
const data = [
{ name: 'city_ne', filter: '_ne', value: 'paris' },
{ name: 'city', filter: '_ne', value: 'paris' },
{ name: 'city', filter: '=', value: 'paris' },
{
name: 'categories',
filter: '_ne',
value: 'first',
},
{
name: 'like',
filter: '_lt',
value: 34,
},
];
const expected = {
_where: [
{
city_ne_ne: 'paris',
},
{
city_ne: 'paris',
},
{
city: 'paris',
},
{
'categories.name_ne': 'first',
},
{
'like.numbers_lt': 34,
},
],
};
expect(formatFiltersToQuery(data, metadatas)).toEqual(expected);
});
});
});

View File

@ -223,7 +223,7 @@ module.exports = {
const params = { const params = {
...permissionQuery, ...permissionQuery,
filters: { filters: {
$and: [idsWhereClause].concat(permissionQuery._where || []), $and: [idsWhereClause].concat(permissionQuery.filters || []),
}, },
}; };

View File

@ -142,7 +142,7 @@ const createEntityManager = db => {
await db.lifecycles.run('beforeCount', uid, { params }); await db.lifecycles.run('beforeCount', uid, { params });
const res = await this.createQueryBuilder(uid) const res = await this.createQueryBuilder(uid)
.init(_.pick(['_q', 'where'], params)) .init(_.pick(['_q', 'where', 'filters'], params))
.count() .count()
.first() .first()
.execute(); .execute();
@ -172,7 +172,7 @@ const createEntityManager = db => {
await this.attachRelations(uid, id, data); await this.attachRelations(uid, id, data);
// TODO: in case there is not select or populate specified return the inserted data ? // TODO: in case there is no select or populate specified return the inserted data ?
// TODO: do not trigger the findOne lifecycles ? // TODO: do not trigger the findOne lifecycles ?
const result = await this.findOne(uid, { const result = await this.findOne(uid, {
where: { id }, where: { id },
@ -296,7 +296,7 @@ const createEntityManager = db => {
throw new Error('Delete requires a where parameter'); throw new Error('Delete requires a where parameter');
} }
// TODO: avoid trigger the findOne lifecycles in the case ? // TODO: do not trigger the findOne lifecycles ?
const entity = await this.findOne(uid, { const entity = await this.findOne(uid, {
select: select && ['id'].concat(select), select: select && ['id'].concat(select),
where, where,
@ -469,7 +469,6 @@ const createEntityManager = db => {
const { joinTable } = attribute; const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable; const { joinColumn, inverseJoinColumn } = joinTable;
// TODO: validate logic of delete
if (isOneToAny(attribute) && isBidirectional(attribute)) { if (isOneToAny(attribute) && isBidirectional(attribute)) {
await this.createQueryBuilder(joinTable.name) await this.createQueryBuilder(joinTable.name)
.delete() .delete()

View File

@ -122,7 +122,11 @@ const applyPopulate = async (results, populate, ctx) => {
const attribute = meta.attributes[key]; const attribute = meta.attributes[key];
const targetMeta = db.metadata.get(attribute.target); const targetMeta = db.metadata.get(attribute.target);
const populateValue = pickPopulateParams(populate[key]); const populateValue = {
...pickPopulateParams(populate[key]),
filters: qb.state.filters,
};
const isCount = populateValue.count === true; const isCount = populateValue.count === true;
const fromTargetRow = rowOrRows => fromRow(targetMeta, rowOrRows); const fromTargetRow = rowOrRows => fromRow(targetMeta, rowOrRows);

View File

@ -29,6 +29,7 @@ const createQueryBuilder = (uid, db) => {
return { return {
alias: getAlias(), alias: getAlias(),
getAlias, getAlias,
state,
select(args) { select(args) {
state.type = 'select'; state.type = 'select';
@ -115,7 +116,7 @@ const createQueryBuilder = (uid, db) => {
}, },
init(params = {}) { init(params = {}) {
const { _q, where, select, limit, offset, orderBy, groupBy, populate } = params; const { _q, filters, where, select, limit, offset, orderBy, groupBy, populate } = params;
if (!_.isNil(where)) { if (!_.isNil(where)) {
this.where(where); this.where(where);
@ -151,9 +152,17 @@ const createQueryBuilder = (uid, db) => {
this.populate(populate); this.populate(populate);
} }
if (!_.isNil(filters)) {
this.filters(filters);
}
return this; return this;
}, },
filters(filters) {
state.filters = filters;
},
first() { first() {
state.first = true; state.first = true;
return this; return this;
@ -206,6 +215,19 @@ const createQueryBuilder = (uid, db) => {
processState() { processState() {
state.orderBy = helpers.processOrderBy(state.orderBy, { qb: this, uid, db }); state.orderBy = helpers.processOrderBy(state.orderBy, { qb: this, uid, db });
if (!_.isNil(state.filters)) {
if (_.isFunction(state.filters)) {
const filters = state.filters({ qb: this, uid, meta, db });
if (!_.isNil(filters)) {
state.where.push(filters);
}
} else {
state.where.push(state.filters);
}
}
state.where = helpers.processWhere(state.where, { qb: this, uid, db }); state.where = helpers.processWhere(state.where, { qb: this, uid, db });
state.populate = helpers.processPopulate(state.populate, { qb: this, uid, db }); state.populate = helpers.processPopulate(state.populate, { qb: this, uid, db });
state.data = helpers.toRow(meta, state.data); state.data = helpers.toRow(meta, state.data);

View File

@ -259,7 +259,7 @@ class Strapi {
} }
stop(exitCode = 1) { stop(exitCode = 1) {
this.server.destroy(); this.destroy();
if (this.config.get('autoReload')) { if (this.config.get('autoReload')) {
process.send('stop'); process.send('stop');
@ -431,7 +431,6 @@ class Strapi {
} }
if (this.config.get('autoReload')) { if (this.config.get('autoReload')) {
this.destroy();
process.send('reload'); process.send('reload');
} }
}; };

View File

@ -52,14 +52,12 @@ module.exports = async function({ build, watchAdmin, polling, browser }) {
switch (message) { switch (message) {
case 'reload': case 'reload':
logger.info('The server is restarting\n'); logger.info('The server is restarting\n');
worker.send('isKilled'); worker.send('kill');
break; break;
case 'kill': case 'killed':
worker.kill();
cluster.fork(); cluster.fork();
break; break;
case 'stop': case 'stop':
worker.kill();
process.exit(1); process.exit(1);
default: default:
return; return;
@ -85,10 +83,10 @@ module.exports = async function({ build, watchAdmin, polling, browser }) {
process.on('message', async message => { process.on('message', async message => {
switch (message) { switch (message) {
case 'isKilled': case 'kill':
await strapiInstance.server.destroy(); await strapiInstance.destroy();
process.send('kill'); process.send('killed');
break; process.exit();
default: default:
// Do nothing. // Do nothing.
} }

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
jest.mock('bcrypt', () => ({ hashSync: () => 'secret-password' })); jest.mock('bcryptjs', () => ({ hashSync: () => 'secret-password' }));
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
const createEntityService = require('../'); const createEntityService = require('../');

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const { getOr, toNumber, isString, isBuffer } = require('lodash/fp'); const { getOr, toNumber, isString, isBuffer } = require('lodash/fp');
const bcrypt = require('bcrypt'); const bcrypt = require('bcryptjs');
const transforms = { const transforms = {
password(value, context) { password(value, context) {

View File

@ -1,7 +1,6 @@
'use strict'; 'use strict';
const delegate = require('delegates'); const delegate = require('delegates');
const { pipe } = require('lodash/fp');
const { const {
sanitizeEntity, sanitizeEntity,
@ -16,12 +15,7 @@ const {
updateComponents, updateComponents,
deleteComponents, deleteComponents,
} = require('./components'); } = require('./components');
const { const { transformParamsToQuery, pickSelectionParams } = require('./params');
transformCommonParams,
transformPaginationParams,
transformParamsToQuery,
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
@ -244,13 +238,26 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
const attribute = attributes[field]; const attribute = attributes[field];
const loadParams = const loadParams = {};
attribute.type === 'relation'
? transformParamsToQuery(attribute.target, params) switch (attribute.type) {
: pipe( case 'relation': {
transformCommonParams, Object.assign(loadParams, transformParamsToQuery(attribute.target, params));
transformPaginationParams break;
)(params); }
case 'component': {
Object.assign(loadParams, transformParamsToQuery(attribute.component, params));
break;
}
case 'dynamiczone': {
Object.assign(loadParams, transformParamsToQuery(null, params));
break;
}
case 'media': {
Object.assign(loadParams, transformParamsToQuery('plugin::upload.file', params));
break;
}
}
return db.query(uid).load(entity, field, loadParams); return db.query(uid).load(entity, field, loadParams);
}, },

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
const { pick, pipe, isNil } = require('lodash/fp'); const assert = require('assert').strict;
const { pick, isNil, toNumber, isInteger } = require('lodash/fp');
const { const {
convertSortQueryParams, convertSortQueryParams,
@ -9,62 +10,39 @@ const {
convertPopulateQueryParams, convertPopulateQueryParams,
convertFiltersQueryParams, convertFiltersQueryParams,
convertFieldsQueryParams, convertFieldsQueryParams,
convertPublicationStateParams,
} = require('@strapi/utils/lib/convert-query-params'); } = require('@strapi/utils/lib/convert-query-params');
const { contentTypes: contentTypesUtils } = require('@strapi/utils'); const pickSelectionParams = pick(['fields', 'populate']);
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants; const transformParamsToQuery = (uid, params) => {
// NOTE: can be a CT, a Compo or nothing in the case of polymorphism (DZ & morph relations)
const type = strapi.getModel(uid);
// TODO: to remove once the front is migrated
const convertOldQuery = params => {
const query = {}; const query = {};
Object.keys(params).forEach(key => { const { _q, sort, filters, fields, populate, page, pageSize, start, limit } = params;
if (key.startsWith('_')) {
query[key.slice(1)] = params[key];
} else {
query[key] = params[key];
}
});
return query; if (!isNil(_q)) {
};
const transformCommonParams = (params = {}) => {
const { _q, sort, filters, _where, fields, populate, ...query } = params;
if (_q) {
query._q = _q; query._q = _q;
} }
if (sort) { if (!isNil(sort)) {
query.orderBy = convertSortQueryParams(sort); query.orderBy = convertSortQueryParams(sort);
} }
if (filters) { if (!isNil(filters)) {
query.where = convertFiltersQueryParams(filters); query.where = convertFiltersQueryParams(filters);
} }
if (_where) { if (!isNil(fields)) {
query.where = {
$and: [_where].concat(query.where || []),
};
}
if (fields) {
query.select = convertFieldsQueryParams(fields); query.select = convertFieldsQueryParams(fields);
} }
if (populate) { if (!isNil(populate)) {
query.populate = convertPopulateQueryParams(populate); query.populate = convertPopulateQueryParams(populate);
} }
return { ...convertOldQuery(query), ...query };
};
const transformPaginationParams = (params = {}) => {
const { page, pageSize, start, limit, ...query } = params;
const isPagePagination = !isNil(page) || !isNil(pageSize); const isPagePagination = !isNil(page) || !isNil(pageSize);
const isOffsetPagination = !isNil(start) || !isNil(limit); const isOffsetPagination = !isNil(start) || !isNil(limit);
@ -74,72 +52,38 @@ const transformPaginationParams = (params = {}) => {
); );
} }
if (page) { if (!isNil(page)) {
query.page = Number(page); const pageVal = toNumber(page);
const isValid = isInteger(pageVal) && pageVal > 0;
assert(isValid, `Invalid 'page' parameter. Expected an integer > 0, received: ${page}`);
query.page = pageVal;
} }
if (pageSize) { if (!isNil(pageSize)) {
query.pageSize = Number(pageSize); const pageSizeVal = toNumber(pageSize);
const isValid = isInteger(pageSizeVal) && pageSizeVal > 0;
assert(isValid, `Invalid 'pageSize' parameter. Expected an integer > 0, received: ${page}`);
query.pageSize = pageSizeVal;
} }
if (start) { if (!isNil(start)) {
query.offset = convertStartQueryParams(start); query.offset = convertStartQueryParams(start);
} }
if (limit) { if (!isNil(limit)) {
query.limit = convertLimitQueryParams(limit); query.limit = convertLimitQueryParams(limit);
} }
return { ...convertOldQuery(query), ...query }; convertPublicationStateParams(type, params, query);
};
const transformPublicationStateParams = uid => (params = {}) => { return query;
const contentType = strapi.getModel(uid);
if (!contentType) {
return params;
}
const { publicationState, ...query } = params;
if (publicationState && contentTypesUtils.hasDraftAndPublish(contentType)) {
const { publicationState = 'live' } = params;
const liveClause = {
[PUBLISHED_AT_ATTRIBUTE]: {
$notNull: true,
},
};
if (publicationState === 'live') {
query.where = {
$and: [liveClause].concat(query.where || []),
};
// TODO: propagate nested publicationState filter somehow
}
}
return { ...convertOldQuery(query), ...query };
};
const pickSelectionParams = pick(['fields', 'populate']);
const transformParamsToQuery = (uid, params) => {
return pipe(
// _q, _where, filters, etc...
transformCommonParams,
// page, pageSize, start, limit
transformPaginationParams,
// publicationState
transformPublicationStateParams(uid)
)(params);
}; };
module.exports = { module.exports = {
transformCommonParams,
transformPublicationStateParams,
transformPaginationParams,
transformParamsToQuery, transformParamsToQuery,
pickSelectionParams, pickSelectionParams,
}; };

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const destroyOnSignal = () => { const destroyOnSignal = strapi => {
let signalReceived = false; let signalReceived = false;
// For unknown reasons, we receive signals 2 times. // For unknown reasons, we receive signals 2 times.

View File

@ -90,7 +90,7 @@
"@strapi/plugin-email": "3.6.8", "@strapi/plugin-email": "3.6.8",
"@strapi/plugin-upload": "3.6.8", "@strapi/plugin-upload": "3.6.8",
"@strapi/utils": "3.6.8", "@strapi/utils": "3.6.8",
"bcrypt": "5.0.1", "bcryptjs": "2.4.3",
"boxen": "5.1.2", "boxen": "5.1.2",
"chalk": "4.1.2", "chalk": "4.1.2",
"chokidar": "3.5.2", "chokidar": "3.5.2",

View File

@ -171,6 +171,7 @@ describe('Publication State', () => {
const res = await rq({ method: 'GET', url: `${baseUrl}${query}` }); const res = await rq({ method: 'GET', url: `${baseUrl}${query}` });
expect(res.body.data).toHaveLength(lengthFor(modelName, { mode })); expect(res.body.data).toHaveLength(lengthFor(modelName, { mode }));
expect(res.body.meta.pagination.total).toBe(lengthFor(modelName, { mode })); expect(res.body.meta.pagination.total).toBe(lengthFor(modelName, { mode }));
}); });
}); });
@ -190,7 +191,7 @@ describe('Publication State', () => {
}, },
}); });
products = res.body.data.map(res => ({ id: res.id, ...res.attributes })); products = res.body.data;
}); });
test('Payload integrity', () => { test('Payload integrity', () => {
@ -199,35 +200,29 @@ describe('Publication State', () => {
test('Root level', () => { test('Root level', () => {
products.forEach(product => { products.forEach(product => {
expect(product.publishedAt).toBeISODate(); expect(product.attributes.publishedAt).toBeISODate();
}); });
}); });
// const getApiRef = id => data.product.find(product => product.id === id); test('First level (categories) to be published only', () => {
products.forEach(({ attributes }) => {
const categories = attributes.categories.data;
test.todo('First level (categories)'); categories.forEach(category => {
expect(category.attributes.publishedAt).toBeISODate();
});
});
});
// products.forEach(({ id, categories }) => { test('Second level through component (countries) to be published only', () => {
// const length = getApiRef(id).categories.filter(c => c.publishedAt !== null).length; products.forEach(({ attributes }) => {
// expect(categories).toHaveLength(length); const countries = attributes.comp.countries.data;
// categories.forEach(category => { countries.forEach(country => {
// expect(category.publishedAt).toBeISODate(); expect(country.attributes.publishedAt).toBeISODate();
// }); });
// }); });
// }); });
test.todo('Second level through component (countries)');
// products.forEach(({ id, comp: { countries } }) => {
// const length = getApiRef(id).comp.countries.filter(c => c.publishedAt !== null).length;
// expect(countries).toHaveLength(length);
// countries.forEach(country => {
// expect(country.publishedAt).toBeISODate();
// });
// });
// });
}); });
}); });
}); });

View File

@ -4,11 +4,12 @@
* 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://strapi.io/documentation/developer-docs/latest/developer-resources/content-api/content-api.html#filters * You can read more here: https://strapi.io/documentation/developer-docs/latest/developer-resources/content-api/content-api.html#filters
*/ */
const { has } = 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 QUERY_OPERATORS = ['_where', '_or', '_and']; const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
class InvalidOrderError extends Error { class InvalidOrderError extends Error {
constructor() { constructor() {
@ -132,13 +133,15 @@ const convertPopulateQueryParams = (populate, depth = 0) => {
if (Array.isArray(populate)) { if (Array.isArray(populate)) {
// map convert // map convert
return populate.flatMap(value => { return _.uniq(
if (typeof value !== 'string') { populate.flatMap(value => {
throw new InvalidPopulateError(); if (typeof value !== 'string') {
} throw new InvalidPopulateError();
}
return value.split(',').map(value => _.trim(value)); return value.split(',').map(value => _.trim(value));
}); })
);
} }
if (_.isPlainObject(populate)) { if (_.isPlainObject(populate)) {
@ -146,6 +149,7 @@ const convertPopulateQueryParams = (populate, depth = 0) => {
for (const key in populate) { for (const key in populate) {
transformedPopulate[key] = convertNestedPopulate(populate[key]); transformedPopulate[key] = convertNestedPopulate(populate[key]);
} }
return transformedPopulate; return transformedPopulate;
} }
@ -212,9 +216,31 @@ const convertFieldsQueryParams = (fields, depth = 0) => {
throw new Error('Invalid fields parameter. Expected a string or an array of strings'); throw new Error('Invalid fields parameter. Expected a string or an array of strings');
}; };
// NOTE: We could validate the parameters are on existing / non private attributes
const convertFiltersQueryParams = filters => filters; const convertFiltersQueryParams = filters => filters;
const convertPublicationStateParams = (type, params = {}, query = {}) => {
if (!type) {
return;
}
const { publicationState } = params;
if (!_.isNil(publicationState)) {
if (!contentTypesUtils.constants.DP_PUB_STATES.includes(publicationState)) {
throw new Error(
`Invalid publicationState. Expected one of 'preview','live' received: ${publicationState}.`
);
}
// NOTE: this is the query layer filters not the entity service filters
query.filters = ({ meta }) => {
if (publicationState === 'live' && has(PUBLISHED_AT_ATTRIBUTE, meta.attributes)) {
return { [PUBLISHED_AT_ATTRIBUTE]: { $notNull: true } };
}
};
}
};
module.exports = { module.exports = {
convertSortQueryParams, convertSortQueryParams,
convertStartQueryParams, convertStartQueryParams,
@ -222,5 +248,5 @@ module.exports = {
convertPopulateQueryParams, convertPopulateQueryParams,
convertFiltersQueryParams, convertFiltersQueryParams,
convertFieldsQueryParams, convertFieldsQueryParams,
QUERY_OPERATORS, convertPublicationStateParams,
}; };

View File

@ -4,7 +4,6 @@
* Export shared utilities * Export shared utilities
*/ */
const { buildQuery, hasDeepFilters } = require('./build-query'); const { buildQuery, hasDeepFilters } = require('./build-query');
const { QUERY_OPERATORS } = require('./convert-query-params');
const parseMultipartData = require('./parse-multipart'); const parseMultipartData = require('./parse-multipart');
const sanitizeEntity = require('./sanitize-entity'); const sanitizeEntity = require('./sanitize-entity');
const parseType = require('./parse-type'); const parseType = require('./parse-type');
@ -38,7 +37,6 @@ module.exports = {
formatYupErrors, formatYupErrors,
policy, policy,
templateConfiguration, templateConfiguration,
QUERY_OPERATORS,
buildQuery, buildQuery,
hasDeepFilters, hasDeepFilters,
parseMultipartData, parseMultipartData,

View File

@ -19,7 +19,7 @@ const enhanceRelationLayout = (layout, locale) =>
if (get(current, ['targetModelPluginOptions', 'i18n', 'localized'], false)) { if (get(current, ['targetModelPluginOptions', 'i18n', 'localized'], false)) {
queryInfos = { queryInfos = {
...queryInfos, ...queryInfos,
defaultParams: { ...queryInfos.defaultParams, _locale: locale }, defaultParams: { ...queryInfos.defaultParams, locale },
paramsToKeep: ['plugins.i18n.locale'], paramsToKeep: ['plugins.i18n.locale'],
}; };
} }
@ -82,7 +82,7 @@ const enhanceComponentLayoutForRelations = (layout, locale) =>
) { ) {
const queryInfos = { const queryInfos = {
...field.queryInfos, ...field.queryInfos,
defaultParams: { ...field.queryInfos.defaultParams, _locale: locale }, defaultParams: { ...field.queryInfos.defaultParams, locale },
paramsToKeep: ['plugins.i18n.locale'], paramsToKeep: ['plugins.i18n.locale'],
}; };

View File

@ -277,7 +277,7 @@ describe('i18n | contentManagerHooks | mutateEditViewLayout', () => {
fieldSchema: { type: 'relation' }, fieldSchema: { type: 'relation' },
targetModelPluginOptions: { i18n: { localized: true } }, targetModelPluginOptions: { i18n: { localized: true } },
queryInfos: { queryInfos: {
defaultParams: { test: true, _locale: 'en' }, defaultParams: { test: true, locale: 'en' },
paramsToKeep: ['plugins.i18n.locale'], paramsToKeep: ['plugins.i18n.locale'],
}, },
}, },
@ -299,7 +299,7 @@ describe('i18n | contentManagerHooks | mutateEditViewLayout', () => {
fieldSchema: { type: 'relation' }, fieldSchema: { type: 'relation' },
targetModelPluginOptions: { i18n: { localized: true } }, targetModelPluginOptions: { i18n: { localized: true } },
queryInfos: { queryInfos: {
defaultParams: { test: true, _locale: 'en' }, defaultParams: { test: true, locale: 'en' },
paramsToKeep: ['plugins.i18n.locale'], paramsToKeep: ['plugins.i18n.locale'],
}, },
}, },
@ -518,7 +518,7 @@ describe('i18n | contentManagerHooks | mutateEditViewLayout', () => {
queryInfos: { queryInfos: {
defaultParams: { defaultParams: {
test: true, test: true,
_locale: 'en', locale: 'en',
}, },
paramsToKeep: ['plugins.i18n.locale'], paramsToKeep: ['plugins.i18n.locale'],
}, },

View File

@ -141,7 +141,7 @@ describe('Entity service decorator', () => {
} }
); );
test('Replaces _locale param', async () => { test('Replaces locale param', async () => {
const defaultService = { const defaultService = {
wrapParams: jest.fn(opts => Promise.resolve(opts)), wrapParams: jest.fn(opts => Promise.resolve(opts)),
}; };

View File

@ -19,7 +19,6 @@ const paramsContain = (key, params) => {
* @param {object} params - query params * @param {object} params - query params
* @param {object} ctx * @param {object} ctx
*/ */
// TODO: remove _locale
const wrapParams = async (params = {}, ctx = {}) => { const wrapParams = async (params = {}, ctx = {}) => {
const { action } = ctx; const { action } = ctx;
@ -36,20 +35,6 @@ const wrapParams = async (params = {}, ctx = {}) => {
}; };
} }
// TODO: remove when the _locale is renamed to locale
if (has('_locale', params)) {
if (params['_locale'] === 'all') {
return omit('_locale', params);
}
return {
...omit('_locale', params),
filters: {
$and: [{ locale: params['_locale'] }].concat(params.filters || []),
},
};
}
const entityDefinedById = paramsContain('id', params) && SINGLE_ENTRY_ACTIONS.includes(action); const entityDefinedById = paramsContain('id', params) && SINGLE_ENTRY_ACTIONS.includes(action);
const entitiesDefinedByIds = paramsContain('id.$in', params) && BULK_ACTIONS.includes(action); const entitiesDefinedByIds = paramsContain('id.$in', params) && BULK_ACTIONS.includes(action);

View File

@ -5,8 +5,14 @@
* @param {{ strapi: import('@strapi/strapi').Strapi }} * @param {{ strapi: import('@strapi/strapi').Strapi }}
*/ */
module.exports = ({ strapi }) => { module.exports = ({ strapi }) => {
const sentry = strapi.plugin('sentry').service('sentry'); const sentryService = strapi.plugin('sentry').service('sentry');
sentry.init(); sentryService.init();
const sentry = sentryService.getInstance();
if (!sentry) {
// initialization failed
return;
}
strapi.server.use(async (ctx, next) => { strapi.server.use(async (ctx, next) => {
try { try {

View File

@ -2881,21 +2881,6 @@
npmlog "^4.1.2" npmlog "^4.1.2"
write-file-atomic "^2.3.0" write-file-atomic "^2.3.0"
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz#2a0b32fcb416fb3f2250fd24cb2a81421a4f5950"
integrity sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==
dependencies:
detect-libc "^1.0.3"
https-proxy-agent "^5.0.0"
make-dir "^3.1.0"
node-fetch "^2.6.1"
nopt "^5.0.0"
npmlog "^4.1.2"
rimraf "^3.0.2"
semver "^7.3.4"
tar "^6.1.0"
"@mdx-js/loader@^1.6.22": "@mdx-js/loader@^1.6.22":
version "1.6.22" version "1.6.22"
resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-1.6.22.tgz#d9e8fe7f8185ff13c9c8639c048b123e30d322c4" resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-1.6.22.tgz#d9e8fe7f8185ff13c9c8639c048b123e30d322c4"
@ -7221,15 +7206,7 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
dependencies: dependencies:
tweetnacl "^0.14.3" tweetnacl "^0.14.3"
bcrypt@5.0.1: bcryptjs@2.4.3, bcryptjs@^2.4.3:
version "5.0.1"
resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71"
integrity sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.0"
node-addon-api "^3.1.0"
bcryptjs@^2.4.3:
version "2.4.3" version "2.4.3"
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=
@ -17349,13 +17326,6 @@ nopt@^4.0.1:
abbrev "1" abbrev "1"
osenv "^0.1.4" osenv "^0.1.4"
nopt@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
dependencies:
abbrev "1"
normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0: normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0:
version "2.5.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"