Filters working

This commit is contained in:
Alexandre Bodin 2021-07-05 18:35:16 +02:00
parent 3711ca3072
commit e973804399
17 changed files with 719 additions and 616 deletions

View File

@ -11,7 +11,7 @@ Object {
},
},
"collectionName": "tests",
"connection": "default",
"connection": undefined,
"description": "My description",
"draftAndPublish": false,
"kind": "singleType",

View File

@ -1,6 +1,6 @@
'use strict';
const { formatContentType } = require('../ContentTypes');
const { formatContentType } = require('../content-types');
describe('Content types service', () => {
describe('format ContentType', () => {

View File

@ -83,8 +83,6 @@ class SqliteDialect extends Dialect {
// TODO: get strapi.dir from somewhere else
this.db.config.connection.connection.filename = path.resolve(
// TODO: do this somewhere else
global.strapi ? global.strapi.dir : process.cwd(),
this.db.config.connection.connection.filename
);

View File

@ -5,6 +5,7 @@ const types = require('./types');
const { createField } = require('./fields');
const { createQueryBuilder } = require('./query');
const { createRepository } = require('./entity-repository');
const { isBidirectional } = require('./metadata/relations');
// TODO: move to query layer
const toRow = (metadata, data = {}) => {
@ -269,6 +270,7 @@ const createEntityManager = db => {
* @param {ID} id - entity ID
* @param {object} data - data received for creation
*/
// TODO: wrap Transaction
async attachRelations(metadata, id, data) {
const { attributes } = metadata;
@ -276,7 +278,11 @@ const createEntityManager = db => {
const attribute = attributes[attributeName];
if (attribute.joinColumn && attribute.owner) {
if (attribute.relation === 'oneToOne' && data[attributeName]) {
if (
attribute.relation === 'oneToOne' &&
isBidirectional(attribute) &&
data[attributeName]
) {
await this.createQueryBuilder(metadata.uid)
.where({ [attribute.joinColumn.name]: data[attributeName], id: { $ne: id } })
.update({ [attribute.joinColumn.name]: null })
@ -315,7 +321,10 @@ const createEntityManager = db => {
// TODO: redefine
// TODO: check it is an id & the entity exists (will throw due to FKs otherwise so not a big pbl in SQL)
if (data[attributeName]) {
if (['oneToOne', 'oneToMany'].includes(attribute.relation)) {
if (
['oneToOne', 'oneToMany'].includes(attribute.relation) &&
isBidirectional(attribute)
) {
await this.createQueryBuilder(joinTable.name)
.delete()
.where({ [inverseJoinColumn.name]: _.castArray(data[attributeName]) })
@ -333,7 +342,7 @@ const createEntityManager = db => {
// if there is nothing to insert
if (insert.length === 0) {
return;
continue;
}
await this.createQueryBuilder(joinTable.name)
@ -353,6 +362,7 @@ const createEntityManager = db => {
* @param {object} data - data received for creation
*/
// TODO: check relation exists (handled by FKs except for polymorphics)
// TODO: wrap Transaction
async updateRelations(metadata, id, data) {
const { attributes } = metadata;
@ -424,7 +434,7 @@ const createEntityManager = db => {
// if there is nothing to insert
if (insert.length === 0) {
return;
continue;
}
await this.createQueryBuilder(joinTable.name)
@ -445,6 +455,7 @@ const createEntityManager = db => {
* @param {Metadata} metadata - model metadta
* @param {ID} id - entity ID
*/
// TODO: wrap Transaction
async deleteRelations(metadata, id) {
// TODO: Implement correctly
if (db.dialect.usesForeignKeys()) {

View File

@ -6,6 +6,13 @@
const _ = require('lodash/fp');
const hasInversedBy = _.has('inversedBy');
const hasMappedBy = _.has('mappedBy');
const isBidirectional = attribute => hasInversedBy(attribute) || hasMappedBy(attribute);
const isOwner = attribute => !isBidirectional(attribute) || hasInversedBy(attribute);
const shouldUseJoinTable = attribute => attribute.useJoinTable !== false;
/**
* Creates a relation metadata
*
@ -186,13 +193,6 @@ const relationFactoryMap = {
manyToMany: createManyToMany,
};
const hasInversedBy = _.has('inversedBy');
const hasMappedBy = _.has('mappedBy');
const isBidirectional = attribute => hasInversedBy(attribute) || hasMappedBy(attribute);
const isOwner = attribute => !isBidirectional(attribute) || hasInversedBy(attribute);
const shouldUseJoinTable = attribute => attribute.useJoinTable !== false;
const createJoinColum = (metadata, { attribute /*attributeName, meta */ }) => {
const targetMeta = metadata.get(attribute.target);
@ -226,8 +226,13 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
const joinTableName = _.snakeCase(`${meta.tableName}_${attributeName}_links`);
const joinColumnName = _.snakeCase(`${meta.singularName}_id`);
const inverseJoinColumnName = _.snakeCase(`${targetMeta.singularName}_id`);
let joinColumnName = _.snakeCase(`${meta.singularName}_id`);
let inverseJoinColumnName = _.snakeCase(`${targetMeta.singularName}_id`);
// if relation is slef referencing
if (joinColumnName === inverseJoinColumnName) {
inverseJoinColumnName = `inv_${inverseJoinColumnName}`;
}
metadata.add({
uid: joinTableName,
@ -296,4 +301,6 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
module.exports = {
createRelation,
isBidirectional,
};

View File

@ -24,8 +24,11 @@ const OPERATORS = [
'$startsWith',
'$endsWith',
'$contains',
'$notContains',
];
const ARRAY_OPERATORS = ['$in', '$notIn', '$between'];
const createPivotJoin = (qb, joinTable, alias, tragetMeta) => {
const joinAlias = qb.getAlias();
qb.join({
@ -249,8 +252,135 @@ const processWhere = (where, ctx, depth = 0) => {
return filters;
};
const applyOperator = (qb, column, operator, value) => {
if (Array.isArray(value) && !ARRAY_OPERATORS.includes(operator)) {
return qb.where(subQB => {
value.forEach(subValue =>
subQB.orWhere(innerQB => {
applyOperator(innerQB, column, operator, subValue);
})
);
});
}
switch (operator) {
case '$not': {
qb.whereNot(qb => applyWhereToColumn(qb, column, value));
break;
}
case '$in': {
qb.whereIn(column, _.castArray(value));
break;
}
case '$notIn': {
qb.whereNotIn(column, _.castArray(value));
break;
}
case '$eq': {
if (value === null) {
qb.whereNull(column);
break;
}
qb.where(column, value);
break;
}
case '$ne': {
if (value === null) {
qb.whereNotNull(column);
break;
}
qb.where(column, '<>', value);
break;
}
case '$gt': {
qb.where(column, '>', value);
break;
}
case '$gte': {
qb.where(column, '>=', value);
break;
}
case '$lt': {
qb.where(column, '<', value);
break;
}
case '$lte': {
qb.where(column, '<=', value);
break;
}
case '$null': {
// TODO: make this better
if (value) {
qb.whereNull(column);
}
break;
}
case '$notNull': {
if (value) {
qb.whereNotNull(column);
}
break;
}
case '$between': {
qb.whereBetween(column, value);
break;
}
// case '$regexp': {
// // TODO:
//
// break;
// }
// // string
// // TODO: use $case to make it case insensitive
// case '$like': {
// qb.where(column, 'like', value);
// break;
// }
// TODO: add casting logic
case '$startsWith': {
qb.where(column, 'like', `${value}%`);
break;
}
case '$endsWith': {
qb.where(column, 'like', `%${value}`);
break;
}
case '$contains': {
// TODO: handle insensitive
qb.where(column, 'like', `%${value}%`);
break;
}
case '$notContains': {
// TODO: handle insensitive
qb.whereNot(column, 'like', `%${value}%`);
break;
}
// TODO: json operators
// TODO: relational operators every/some/exists/size ...
default: {
throw new Error(`Undefined operator ${operator}`);
}
}
};
const applyWhereToColumn = (qb, column, columnWhere) => {
if (!_.isPlainObject(columnWhere)) {
if (Array.isArray(columnWhere)) {
return qb.whereIn(column, columnWhere);
}
return qb.where(column, columnWhere);
}
@ -258,122 +388,13 @@ const applyWhereToColumn = (qb, column, columnWhere) => {
Object.keys(columnWhere).forEach(operator => {
const value = columnWhere[operator];
switch (operator) {
case '$not': {
qb.whereNot(qb => applyWhereToColumn(qb, column, value));
break;
}
case '$in': {
qb.whereIn(column, _.castArray(value));
break;
}
case '$notIn': {
qb.whereNotIn(column, _.castArray(value));
break;
}
case '$eq': {
if (Array.isArray(value)) {
return qb.whereIn(column, value);
}
if (value === null) {
qb.whereNull(column);
break;
}
qb.where(column, value);
break;
}
case '$ne': {
if (Array.isArray(value)) {
return qb.whereNotIn(column, value);
}
if (value === null) {
qb.whereNotNull(column);
break;
}
qb.where(column, '<>', value);
break;
}
case '$gt': {
qb.where(column, '>', value);
break;
}
case '$gte': {
qb.where(column, '>=', value);
break;
}
case '$lt': {
qb.where(column, '<', value);
break;
}
case '$lte': {
qb.where(column, '<=', value);
break;
}
case '$null': {
// TODO: make this better
if (value) {
qb.whereNull(column);
}
break;
}
case '$notNull': {
if (value) {
qb.whereNotNull(column);
}
break;
}
case '$between': {
qb.whereBetween(column, value);
break;
}
// case '$regexp': {
// // TODO:
//
// break;
// }
// // string
// // TODO: use $case to make it case insensitive
// case '$like': {
// qb.where(column, 'like', value);
// break;
// }
// TODO: add casting logic
case '$startsWith': {
qb.where(column, 'like', `${value}%`);
break;
}
case '$endsWith': {
qb.where(column, 'like', `%${value}`);
break;
}
case '$contains': {
qb.where(column, 'like', `%${value}%`);
break;
}
// TODO: json operators
// TODO: relational operators every/some/exists/size ...
default: {
throw new Error(`Undefined operator ${operator}`);
}
}
applyOperator(qb, column, operator, value);
});
};
const applyWhere = (qb, where) => {
if (Array.isArray(where)) {
return where.forEach(subWhere => applyWhere(qb, subWhere));
return qb.where(subQB => where.forEach(subWhere => applyWhere(subQB, subWhere)));
}
if (!_.isPlainObject(where)) {
@ -384,14 +405,14 @@ const applyWhere = (qb, where) => {
const value = where[key];
if (key === '$and') {
return qb.where(qb => {
value.forEach(v => applyWhere(qb, v));
return qb.where(subQB => {
value.forEach(v => applyWhere(subQB, v));
});
}
if (key === '$or') {
return qb.where(qb => {
value.forEach(v => qb.orWhere(inner => applyWhere(inner, v)));
return qb.where(subQB => {
value.forEach(v => subQB.orWhere(inner => applyWhere(inner, v)));
});
}
@ -513,6 +534,8 @@ const applyPopulate = async (results, populate, ctx) => {
results.forEach(result => {
result[key] = null;
});
continue;
}
const rows = await db.entityManager
@ -541,8 +564,6 @@ const applyPopulate = async (results, populate, ctx) => {
referencedColumn: referencedColumnName,
} = joinTable.joinColumn;
// TODO: create aliases for the columns
const alias = qb.getAlias();
const rows = await qb
.init(populateValue)
@ -584,6 +605,7 @@ const applyPopulate = async (results, populate, ctx) => {
results.forEach(result => {
result[key] = null;
});
continue;
}
const rows = await db.entityManager
@ -675,7 +697,7 @@ const applyPopulate = async (results, populate, ctx) => {
const fromRow = (metadata, row) => {
if (Array.isArray(row)) {
return row.map(row => fromRow(metadata, row));
return row.map(singleRow => fromRow(metadata, singleRow));
}
const { attributes } = metadata;

View File

@ -155,81 +155,87 @@ const createQueryBuilder = (uid, db) => {
return this.alias + '.' + columnName;
},
async execute({ mapResults = true } = {}) {
getKnexQuery() {
const aliasedTableName = state.type === 'insert' ? tableName : { [this.alias]: tableName };
const qb = db.connection(aliasedTableName);
switch (state.type) {
case 'select': {
if (state.select.length === 0) {
state.select = [this.aliasColumn('*')];
}
if (state.joins.length > 0) {
// add ordered columns to distinct in case of joins
// TODO: make sure we return the right data
qb.distinct(`${this.alias}.id`);
// TODO: add column if they aren't there already
state.select.unshift(...state.orderBy.map(({ column }) => column));
}
qb.select(state.select);
break;
}
case 'count': {
qb.count({ count: state.count });
break;
}
case 'insert': {
qb.insert(state.data);
if (db.dialect.useReturning() && _.has('id', meta.attributes)) {
qb.returning('id');
}
break;
}
case 'update': {
qb.update(state.data);
break;
}
case 'delete': {
qb.del();
break;
}
}
if (state.limit) {
qb.limit(state.limit);
}
if (state.offset) {
qb.offset(state.offset);
}
if (state.orderBy.length > 0) {
qb.orderBy(state.orderBy);
}
if (state.first) {
qb.first();
}
if (state.groupBy.length > 0) {
qb.groupBy(state.groupBy);
}
if (state.where) {
helpers.applyWhere(qb, state.where);
}
if (state.joins.length > 0) {
helpers.applyJoins(qb, state.joins);
}
return qb;
},
async execute({ mapResults = true } = {}) {
try {
const qb = db.connection(aliasedTableName);
switch (state.type) {
case 'select': {
if (state.select.length === 0) {
state.select = [this.aliasColumn('*')];
}
if (state.joins.length > 0) {
// add ordered columns to distinct in case of joins
// TODO: make sure we return the right data
qb.distinct(`${this.alias}.id`);
// TODO: add column if they aren't there already
state.select.unshift(...state.orderBy.map(({ column }) => column));
}
qb.select(state.select);
break;
}
case 'count': {
qb.count({ count: state.count });
break;
}
case 'insert': {
qb.insert(state.data);
if (db.dialect.useReturning() && _.has('id', meta.attributes)) {
qb.returning('id');
}
break;
}
case 'update': {
qb.update(state.data);
break;
}
case 'delete': {
qb.del();
break;
}
}
if (state.limit) {
qb.limit(state.limit);
}
if (state.offset) {
qb.offset(state.offset);
}
if (state.orderBy.length > 0) {
qb.orderBy(state.orderBy);
}
if (state.first) {
qb.first();
}
if (state.groupBy.length > 0) {
qb.groupBy(state.groupBy);
}
if (state.where) {
helpers.applyWhere(qb, state.where);
}
if (state.joins.length > 0) {
helpers.applyJoins(qb, state.joins);
}
const qb = this.getKnexQuery();
const rows = await qb;

View File

@ -312,7 +312,7 @@ class Strapi {
}
// Kill process
process.exit(exitCode);
// process.exit(exitCode);
}
async load() {
@ -362,11 +362,7 @@ class Strapi {
models: Database.transformContentTypes(contentTypes),
});
if (process.env.NODE_ENV === 'test') {
await this.db.schema.reset();
} else {
await this.db.schema.sync();
}
await this.db.schema.sync();
await this.runLifecyclesFunctions(LIFECYCLES.REGISTER);
// await this.db.initialize();

View File

@ -3,6 +3,12 @@
const { pick } = require('lodash/fp');
const delegate = require('delegates');
const {
convertSortQueryParams,
convertLimitQueryParams,
convertStartQueryParams,
} = require('@strapi/utils/lib/convert-rest-query-params');
const {
sanitizeEntity,
webhook: webhookUtils,
@ -36,27 +42,22 @@ module.exports = ctx => {
return service;
};
const defaultLimit = 10;
// TODO: move to Controller ?
const transformParamsToQuery = (params = {}) => {
const query = {};
// TODO: check invalid values add defaults ....
if (params.pagination) {
const { pagination } = params;
if (pagination.start || pagination.limit) {
query.limit = Number(pagination.limit) || defaultLimit;
query.offset = Number(pagination.start) || 0;
}
if (pagination.page || pagination.pageSize) {
query.limit = Number(pagination.pageSize) || defaultLimit;
query.offset = (Number(pagination.page) - 1) * query.limit;
}
if (params.start) {
query.offset = convertStartQueryParams(params.start);
}
if (params.limit) {
query.limit = convertLimitQueryParams(params.limit);
}
if (params.sort) {
// TODO: impl
query.orderBy = params.sort;
query.orderBy = convertSortQueryParams(params.sort);
}
if (params.filters) {
@ -102,14 +103,25 @@ const createDefaultImplementation = ({ db, eventHub, entityValidator }) => ({
async findPage(uid, opts) {
const { params } = await this.wrapOptions(opts, { uid, action: 'findPage' });
const { page = 1, pageSize = 100 } = params;
const pagination = {
page: parseInt(page),
pageSize: parseInt(pageSize),
};
const query = transformParamsToQuery(params);
query.limit = pagination.pageSize;
query.offset = pagination.page * pagination.pageSize;
const [results, total] = await db.query(uid).findWithCount(query);
// TODO: cleanup
return {
results,
pagination: {
...pagination,
pageCount: Math.ceil(total / pageSize),
total,
},
};
@ -161,6 +173,8 @@ const createDefaultImplementation = ({ db, eventHub, entityValidator }) => ({
// entry = await this.findOne({ params: { id: entry.id } }, { model });
// }
// TODO: Implement components CRUD ?
eventHub.emit(ENTRY_CREATE, {
model: modelDef.modelName,
entry: sanitizeEntity(entry, { model: modelDef }),

View File

@ -76,6 +76,9 @@ describe('Core API - Basic + compo', () => {
method: 'POST',
url: '/product-with-compos',
body: product,
qs: {
populate: ['compo'],
},
});
expect(res.statusCode).toBe(200);
@ -88,6 +91,9 @@ describe('Core API - Basic + compo', () => {
const res = await rq({
method: 'GET',
url: '/product-with-compos',
qs: {
populate: ['compo'],
},
});
expect(res.statusCode).toBe(200);
@ -110,6 +116,9 @@ describe('Core API - Basic + compo', () => {
method: 'PUT',
url: `/product-with-compos/${data.productsWithCompo[0].id}`,
body: product,
qs: {
populate: ['compo'],
},
});
expect(res.statusCode).toBe(200);
@ -123,6 +132,9 @@ describe('Core API - Basic + compo', () => {
const res = await rq({
method: 'DELETE',
url: `/product-with-compos/${data.productsWithCompo[0].id}`,
qs: {
populate: ['compo'],
},
});
expect(res.statusCode).toBe(200);
@ -138,6 +150,7 @@ describe('Core API - Basic + compo', () => {
name: 'Product 1',
description: 'Product description',
};
const res = await rq({
method: 'POST',
url: '/product-with-compos',

View File

@ -106,7 +106,7 @@ describe('Deep Filtering API', () => {
method: 'GET',
url: '/collectors',
qs: {
'cards.name': data.card[0].name,
filters: { cards: { name: data.card[0].name } },
},
});
@ -121,7 +121,7 @@ describe('Deep Filtering API', () => {
method: 'GET',
url: '/collectors',
qs: {
'cards.name': data.card[1].name,
filters: { cards: { name: data.card[1].name } },
},
});
@ -137,7 +137,7 @@ describe('Deep Filtering API', () => {
method: 'GET',
url: '/collectors',
qs: {
'collector_friends.name': data.collector[0].name,
filters: { collector_friends: { name: data.collector[0].name } },
},
});
@ -148,14 +148,18 @@ describe('Deep Filtering API', () => {
});
});
describe('With search', () => {
describe.skip('With search', () => {
describe('Filter on a manyWay relation', () => {
test('cards.name + empty search', async () => {
const res = await rq({
method: 'GET',
url: '/collectors',
qs: {
'cards.name': data.card[0].name,
filters: {
cards: {
name: data.card[0].name,
},
},
_q: '',
},
});

View File

@ -34,7 +34,7 @@ describe('Create Strapi API End to End', () => {
afterAll(async () => {
await strapi.destroy();
// await builder.cleanup();
await builder.cleanup();
});
describe('Test manyToMany relation (article - tag) with Content Manager', () => {

View File

@ -297,9 +297,7 @@ describe('Filtering API', () => {
qs: {
filters: {
name: {
$not: {
$contains: 'production',
},
$notContains: 'production',
},
},
},
@ -314,7 +312,7 @@ describe('Filtering API', () => {
url: '/products',
qs: {
filters: {
name: { $not: { $contains: 'ProdUctIon' } },
name: { $notContains: 'ProdUctIon' },
},
},
});
@ -328,7 +326,7 @@ describe('Filtering API', () => {
url: '/products',
qs: {
filters: {
name: { $not: { $contains: 'product' } },
name: { $notContains: 'product' },
},
},
});
@ -340,7 +338,7 @@ describe('Filtering API', () => {
url: '/products',
qs: {
filters: {
name: { $not: { $contains: 'ProDuCt' } },
name: { $notContains: 'ProDuCt' },
},
},
});
@ -350,62 +348,74 @@ describe('Filtering API', () => {
});
// FIXME: Not working on sqlite due to https://www.sqlite.org/draft/pragma.html#pragma_case_sensitive_like
// describe('Filter contains sensitive', () => {
// test.skip('Should return empty if the case does not match', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
describe('Filter contains sensitive', () => {
test.skip('Should return empty if the case does not match', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$contains: 'product',
},
},
},
});
// name_containss: 'product',
// },
// });
expect(res.body).toEqual([]);
});
// expect(res.body).toEqual([]);
// });
test('Should return the entities if the case matches', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$contains: 'Product',
},
},
},
});
// test('Should return the entities if the case matches', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
});
// name_containss: 'Product',
// },
// });
// FIXME: Not working on sqlite due to https://www.sqlite.org/draft/pragma.html#pragma_case_sensitive_like
describe('Filter not contains sensitive', () => {
test.skip('Should return the entities if the case does not match', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$contains: 'product',
},
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
// // FIXME: Not working on sqlite due to https://www.sqlite.org/draft/pragma.html#pragma_case_sensitive_like
// describe('Filter not contains sensitive', () => {
// test.skip('Should return the entities if the case does not match', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
test('Should return an empty array if the case matches', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$notContains: 'Product',
},
},
},
});
// name_ncontainss: 'product',
// },
// });
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
// test('Should return an empty array if the case matches', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// name_ncontainss: 'Product',
// },
// });
// expect(res.body).toEqual([]);
// });
// });
expect(res.body).toEqual([]);
});
});
describe('Filter in', () => {
test('Should return the Product with a single value', async () => {
@ -919,11 +929,7 @@ describe('Filtering API', () => {
price: { $gt: 28 },
},
{
$or: [
{
rank: 91,
},
],
$or: [{ rank: 91 }],
},
],
],
@ -934,7 +940,9 @@ describe('Filtering API', () => {
expect(res.body).toEqual(expect.arrayContaining([data.product[0], data.product[2]]));
});
});
});
describe('Implict or', () => {
test('Filter equals', async () => {
const res = await rq({
method: 'GET',
@ -963,365 +971,388 @@ describe('Filtering API', () => {
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
// test('Filter contains insensitive', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// name_contains: ['Product', '1'],
// },
// },
// });
test.skip('Filter contains insensitive', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$contains: ['Product', '1'],
},
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
// test('Filter not contains insensitive', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// name_ncontains: ['Product', 'Non existent'],
// },
// },
// });
test.skip('Filter not contains insensitive', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$notContains: ['Product', 'Non existent'],
},
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
// test('Filter contains sensitive', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// name_containss: ['Product', 'Non existent'],
// },
// },
// });
test('Filter contains sensitive', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$contains: ['Product', 'Non existent'],
},
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
// test('Filter not contains sensitive', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// name_ncontainss: ['product', 'Non existent'],
// },
// },
// });
test('Filter not contains sensitive', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$notContains: ['product', 'Non existent'],
},
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
// test('Filter greater than', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// rank_gt: [12, 56],
// },
// },
// });
test('Filter greater than', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
rank: { $gt: [12, 56] },
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
// test('Filter greater than or equal', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// rank_gte: [42, 56],
// },
// },
// });
test('Filter greater than or equal', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
rank: {
$gte: [42, 56],
},
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
// test('Filter less than', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// rank_lt: [56, 12],
// },
// },
// });
test('Filter less than', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
rank: { $lt: [56, 12] },
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
// test('Filter less than or equal', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// rank_lte: [12, 42],
// },
// },
// });
test('Filter less than or equal', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
rank: { $lte: [12, 42] },
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
});
// describe('Complexe filtering', () => {
// test('Greater than and less than at the same time', async () => {
// let res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// rank_lte: 42,
// rank_gte: 42,
// },
// },
// });
describe('Complexe filtering', () => {
test('Greater than and less than at the same time', async () => {
let res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
rank: {
$lte: 42,
$gte: 42,
},
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// rank_lt: 43,
// rank_gt: 41,
// },
// },
// });
res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
rank: {
$lt: 43,
$gt: 41,
},
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// rank_lt: 43,
// rank_gt: 431,
// },
// },
// });
res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
rank: { $lt: 43, $gt: 431 },
},
},
});
// expect(res.body).toEqual([]);
// });
expect(res.body).toEqual([]);
});
// test('Contains and Not contains on same column', async () => {
// let res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// name_contains: 'Product',
// name_ncontains: '1',
// },
// },
// });
test('Contains and Not contains on same column', async () => {
let res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$contains: 'Product',
$notContains: '1',
},
},
},
});
// expect(res.body).toEqual(expect.arrayContaining(data.product.slice(1)));
expect(res.body).toEqual(expect.arrayContaining(data.product.slice(1)));
// res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// name_contains: 'Product 1',
// name_ncontains: ['2', '3'],
// },
// },
// });
res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$contains: 'Product 1',
$notContains: ['2', '3'],
},
},
},
});
// expect(res.body).toEqual(expect.not.arrayContaining([data.product[1], data.product[2]]));
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
expect(res.body).toEqual(expect.not.arrayContaining([data.product[1], data.product[2]]));
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// name_contains: '2',
// name_ncontains: 'Product',
// },
// },
// });
res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$contains: '2',
$notContains: 'Product',
},
},
},
});
// expect(res.body).toEqual([]);
// });
expect(res.body).toEqual([]);
});
// test('Combined filters', async () => {
// let res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// filters: {
// name_contains: 'Product',
// rank_lt: 45,
// },
// },
// });
test('Combined filters', async () => {
let res = await rq({
method: 'GET',
url: '/products',
qs: {
filters: {
name: {
$contains: 'Product',
},
rank: {
$lt: 45,
},
},
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
});
// describe('Sorting', () => {
// test('Default sorting is asc', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// _sort: 'rank',
// },
// });
describe('Sorting', () => {
test('Default sorting is asc', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
sort: 'rank',
},
});
// expect(res.body).toEqual(
// expect.arrayContaining(data.product.slice(0).sort((a, b) => a.rank - b.rank))
// );
// });
expect(res.body).toEqual(
expect.arrayContaining(data.product.slice(0).sort((a, b) => a.rank - b.rank))
);
});
// test('Simple sorting', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// _sort: 'rank:asc',
// },
// });
test('Simple sorting', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
sort: 'rank:asc',
},
});
// expect(res.body).toEqual(
// expect.arrayContaining(data.product.slice(0).sort((a, b) => a.rank - b.rank))
// );
expect(res.body).toEqual(
expect.arrayContaining(data.product.slice(0).sort((a, b) => a.rank - b.rank))
);
// const res2 = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// _sort: 'rank:desc',
// },
// });
const res2 = await rq({
method: 'GET',
url: '/products',
qs: {
sort: 'rank:desc',
},
});
// expect(res2.body).toEqual(
// expect.arrayContaining(data.product.slice(0).sort((a, b) => b.rank - a.rank))
// );
// });
expect(res2.body).toEqual(
expect.arrayContaining(data.product.slice(0).sort((a, b) => b.rank - a.rank))
);
});
// test('Multi column sorting', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// _sort: 'price:asc,rank:desc',
// },
// });
test('Multi column sorting', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
sort: 'price:asc,rank:desc',
},
});
// [data.product[3], data.product[0], data.product[2], data.product[1]].forEach(expectedPost => {
// expect(res.body).toEqual(expect.arrayContaining([expectedPost]));
// });
// });
// });
[data.product[3], data.product[0], data.product[2], data.product[1]].forEach(expectedPost => {
expect(res.body).toEqual(expect.arrayContaining([expectedPost]));
});
});
});
// describe('Limit and offset', () => {
// test('Limit', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// _limit: 1,
// _sort: 'rank:asc',
// },
// });
describe('Limit and offset', () => {
test('Limit', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
limit: 1,
sort: 'rank:asc',
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
// test('Limit with sorting', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// _limit: 1,
// _sort: 'rank:desc',
// },
// });
test('Limit with sorting', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
limit: 1,
sort: 'rank:desc',
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[data.product.length - 1]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[data.product.length - 1]]));
});
// test('Offset', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// _start: 1,
// _sort: 'rank:asc',
// },
// });
test('Offset', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
start: 1,
sort: 'rank:asc',
},
});
// expect(res.body).toEqual(expect.arrayContaining(data.product.slice(1)));
// });
expect(res.body).toEqual(expect.arrayContaining(data.product.slice(1)));
});
// test('Offset with limit', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// _limit: 1,
// _start: 1,
// _sort: 'rank:asc',
// },
// });
test('Offset with limit', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
limit: 1,
start: 1,
sort: 'rank:asc',
},
});
// expect(res.body).toEqual(expect.arrayContaining(data.product.slice(1, 2)));
// });
// });
expect(res.body).toEqual(expect.arrayContaining(data.product.slice(1, 2)));
});
});
// describe('Text query', () => {
// test('Cyrillic query', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// _q: 'Опис',
// },
// });
describe.skip('Text query', () => {
test('Cyrillic query', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
_q: 'Опис',
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[4]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[4]]));
});
// test('Multi word query', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// _q: 'Product description',
// },
// });
test('Multi word query', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
_q: 'Product description',
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
});
// test('Multi word cyrillic query', async () => {
// const res = await rq({
// method: 'GET',
// url: '/products',
// qs: {
// _q: 'Опис на продукт',
// },
// });
test('Multi word cyrillic query', async () => {
const res = await rq({
method: 'GET',
url: '/products',
qs: {
_q: 'Опис на продукт',
},
});
// expect(res.body).toEqual(expect.arrayContaining([data.product[4]]));
// });
expect(res.body).toEqual(expect.arrayContaining([data.product[4]]));
});
});
});

View File

@ -93,6 +93,8 @@ const convertSortQueryParams = sortQuery => {
throw new Error(`convertSortQueryParams expected a string, got ${typeof sortQuery}`);
}
// TODO: handle array input
const sortKeys = [];
sortQuery.split(',').forEach(part => {
@ -107,12 +109,10 @@ const convertSortQueryParams = sortQuery => {
throw new Error('order can only be one of asc|desc|ASC|DESC');
}
sortKeys.push({ field, order: order.toLowerCase() });
sortKeys.push({ [field]: order.toLowerCase() });
});
return {
sort: sortKeys,
};
return sortKeys;
};
/**
@ -126,9 +126,7 @@ const convertStartQueryParams = startQuery => {
throw new Error(`convertStartQueryParams expected a positive integer got ${startAsANumber}`);
}
return {
start: startAsANumber,
};
return startAsANumber;
};
/**
@ -142,9 +140,7 @@ const convertLimitQueryParams = limitQuery => {
throw new Error(`convertLimitQueryParams expected a positive integer got ${limitAsANumber}`);
}
return {
limit: limitAsANumber,
};
return limitAsANumber;
};
/**
@ -239,6 +235,9 @@ const convertWhereClause = (whereClause, value) => {
module.exports = {
convertRestQueryParams,
convertSortQueryParams,
convertStartQueryParams,
convertLimitQueryParams,
VALID_REST_OPERATORS,
QUERY_OPERATORS,
};

View File

@ -1,8 +1,10 @@
const path = require('path');
module.exports = ({ env }) => ({
connection: {
client: 'sqlite',
connection: {
filename: env('DATABASE_FILENAME', '<%= connection.filename %>'),
filename: path.join(__dirname, '..', env('DATABASE_FILENAME', '<%= connection.filename %>')),
},
useNullAsDefault: true,
},

View File

@ -26,7 +26,7 @@ const createTestBuilder = (options = {}) => {
},
sanitizedFixturesFor(modelName, strapi) {
const model = strapi.getModel(modelName);
const model = strapi.getModel(`application::${modelName}.${modelName}`);
const fixtures = this.fixturesFor(modelName);
return sanitizeEntity(fixtures, { model });
@ -77,7 +77,7 @@ const createTestBuilder = (options = {}) => {
if (enableTestDataAutoCleanup) {
for (const model of models.reverse()) {
await modelsUtils.cleanupModel(model.uid || model.modelName);
await modelsUtils.cleanupModel(model.modelName);
}
}

View File

@ -122,7 +122,7 @@ const deleteContentTypes = async (modelsName, { strapi } = {}) => {
async function cleanupModels(models, { strapi } = {}) {
for (const model of models) {
await cleanupModel(`application::${model}.${model}`, { strapi });
await cleanupModel(model, { strapi });
}
}