From e9738043998a2243e55e09b59e98fda23b3b2666 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Mon, 5 Jul 2021 18:35:16 +0200 Subject: [PATCH] Filters working --- .../__snapshots__/content-types.test.js.snap | 2 +- .../services/__tests__/content-types.test.js | 2 +- packages/core/database/lib/dialects/index.js | 2 - packages/core/database/lib/entity-manager.js | 19 +- .../core/database/lib/metadata/relations.js | 25 +- packages/core/database/lib/query/helpers.js | 258 +++--- .../core/database/lib/query/query-builder.js | 150 ++-- packages/core/strapi/lib/Strapi.js | 8 +- .../strapi/lib/services/entity-service.js | 42 +- .../strapi/tests/api/basic-compo.test.e2e.js | 13 + .../strapi/tests/deepFiltering.test.e2e.js | 14 +- .../core/strapi/tests/endpoint.test.e2e.js | 2 +- .../core/strapi/tests/filtering.test.e2e.js | 769 +++++++++--------- .../utils/lib/convert-rest-query-params.js | 19 +- .../database-templates/sqlite.template | 4 +- test/helpers/builder/index.js | 4 +- test/helpers/models.js | 2 +- 17 files changed, 719 insertions(+), 616 deletions(-) diff --git a/packages/core/content-type-builder/services/__tests__/__snapshots__/content-types.test.js.snap b/packages/core/content-type-builder/services/__tests__/__snapshots__/content-types.test.js.snap index b17f621a1f..9be66745c2 100644 --- a/packages/core/content-type-builder/services/__tests__/__snapshots__/content-types.test.js.snap +++ b/packages/core/content-type-builder/services/__tests__/__snapshots__/content-types.test.js.snap @@ -11,7 +11,7 @@ Object { }, }, "collectionName": "tests", - "connection": "default", + "connection": undefined, "description": "My description", "draftAndPublish": false, "kind": "singleType", diff --git a/packages/core/content-type-builder/services/__tests__/content-types.test.js b/packages/core/content-type-builder/services/__tests__/content-types.test.js index b58625734d..b8c7817b6b 100644 --- a/packages/core/content-type-builder/services/__tests__/content-types.test.js +++ b/packages/core/content-type-builder/services/__tests__/content-types.test.js @@ -1,6 +1,6 @@ 'use strict'; -const { formatContentType } = require('../ContentTypes'); +const { formatContentType } = require('../content-types'); describe('Content types service', () => { describe('format ContentType', () => { diff --git a/packages/core/database/lib/dialects/index.js b/packages/core/database/lib/dialects/index.js index 4a17efd701..c3de19e82f 100644 --- a/packages/core/database/lib/dialects/index.js +++ b/packages/core/database/lib/dialects/index.js @@ -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 ); diff --git a/packages/core/database/lib/entity-manager.js b/packages/core/database/lib/entity-manager.js index d5ccbe181e..8f38cfa91b 100644 --- a/packages/core/database/lib/entity-manager.js +++ b/packages/core/database/lib/entity-manager.js @@ -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()) { diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index df10d5e641..e00cbfef5b 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -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, }; diff --git a/packages/core/database/lib/query/helpers.js b/packages/core/database/lib/query/helpers.js index 08b020eeef..a3462a1054 100644 --- a/packages/core/database/lib/query/helpers.js +++ b/packages/core/database/lib/query/helpers.js @@ -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; diff --git a/packages/core/database/lib/query/query-builder.js b/packages/core/database/lib/query/query-builder.js index 3d6bd21163..b3beb53396 100644 --- a/packages/core/database/lib/query/query-builder.js +++ b/packages/core/database/lib/query/query-builder.js @@ -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; diff --git a/packages/core/strapi/lib/Strapi.js b/packages/core/strapi/lib/Strapi.js index a8f81e0ade..cf80f0329a 100644 --- a/packages/core/strapi/lib/Strapi.js +++ b/packages/core/strapi/lib/Strapi.js @@ -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(); diff --git a/packages/core/strapi/lib/services/entity-service.js b/packages/core/strapi/lib/services/entity-service.js index 15981f35df..1dd87dad5f 100644 --- a/packages/core/strapi/lib/services/entity-service.js +++ b/packages/core/strapi/lib/services/entity-service.js @@ -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 }), diff --git a/packages/core/strapi/tests/api/basic-compo.test.e2e.js b/packages/core/strapi/tests/api/basic-compo.test.e2e.js index ed52bb367e..379e369e95 100644 --- a/packages/core/strapi/tests/api/basic-compo.test.e2e.js +++ b/packages/core/strapi/tests/api/basic-compo.test.e2e.js @@ -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', diff --git a/packages/core/strapi/tests/deepFiltering.test.e2e.js b/packages/core/strapi/tests/deepFiltering.test.e2e.js index 29de1771be..9343263fb6 100644 --- a/packages/core/strapi/tests/deepFiltering.test.e2e.js +++ b/packages/core/strapi/tests/deepFiltering.test.e2e.js @@ -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: '', }, }); diff --git a/packages/core/strapi/tests/endpoint.test.e2e.js b/packages/core/strapi/tests/endpoint.test.e2e.js index de3741ee50..8a93ff9ae5 100644 --- a/packages/core/strapi/tests/endpoint.test.e2e.js +++ b/packages/core/strapi/tests/endpoint.test.e2e.js @@ -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', () => { diff --git a/packages/core/strapi/tests/filtering.test.e2e.js b/packages/core/strapi/tests/filtering.test.e2e.js index 8395e1386f..96c467a46c 100644 --- a/packages/core/strapi/tests/filtering.test.e2e.js +++ b/packages/core/strapi/tests/filtering.test.e2e.js @@ -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]])); + }); }); }); diff --git a/packages/core/utils/lib/convert-rest-query-params.js b/packages/core/utils/lib/convert-rest-query-params.js index e742b4a2e5..cbdb85ef24 100644 --- a/packages/core/utils/lib/convert-rest-query-params.js +++ b/packages/core/utils/lib/convert-rest-query-params.js @@ -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, }; diff --git a/packages/generators/app/lib/resources/templates/database-templates/sqlite.template b/packages/generators/app/lib/resources/templates/database-templates/sqlite.template index e06c2f1b2f..f11263c93b 100644 --- a/packages/generators/app/lib/resources/templates/database-templates/sqlite.template +++ b/packages/generators/app/lib/resources/templates/database-templates/sqlite.template @@ -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, }, diff --git a/test/helpers/builder/index.js b/test/helpers/builder/index.js index fca4ddb16a..e322981bec 100644 --- a/test/helpers/builder/index.js +++ b/test/helpers/builder/index.js @@ -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); } } diff --git a/test/helpers/models.js b/test/helpers/models.js index a403e94a4f..aed0435daa 100644 --- a/test/helpers/models.js +++ b/test/helpers/models.js @@ -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 }); } }