diff --git a/packages/strapi-connector-bookshelf/lib/buildQuery.js b/packages/strapi-connector-bookshelf/lib/buildQuery.js index 115693b70a..a7d5fe2790 100644 --- a/packages/strapi-connector-bookshelf/lib/buildQuery.js +++ b/packages/strapi-connector-bookshelf/lib/buildQuery.js @@ -79,43 +79,50 @@ const buildJoinsAndFilter = (qb, model, whereClauses) => { * @param {Object} destinationInfo - destination with which we are making a join */ const buildJoin = (qb, assoc, originInfo, destinationInfo) => { - if (assoc.nature === 'manyToMany') { + if (['manyToMany', 'manyWay'].includes(assoc.nature)) { const joinTableAlias = generateAlias(assoc.tableCollectionName); + let originColumnNameInJoinTable; + if (assoc.nature === 'manyToMany') { + originColumnNameInJoinTable = `${joinTableAlias}.${singular( + destinationInfo.model.attributes[assoc.via].attribute + )}_${destinationInfo.model.attributes[assoc.via].column}`; + } else if (assoc.nature === 'manyWay') { + originColumnNameInJoinTable = `${joinTableAlias}.${singular( + originInfo.model.collectionName + )}_${originInfo.model.primaryKey}`; + } + qb.leftJoin( `${originInfo.model.databaseName}.${assoc.tableCollectionName} AS ${joinTableAlias}`, - `${joinTableAlias}.${singular( - destinationInfo.model.attributes[assoc.via].attribute - )}_${destinationInfo.model.attributes[assoc.via].column}`, + originColumnNameInJoinTable, `${originInfo.alias}.${originInfo.model.primaryKey}` ); qb.leftJoin( `${destinationInfo.model.databaseName}.${destinationInfo.model.collectionName} AS ${destinationInfo.alias}`, - `${joinTableAlias}.${singular( - originInfo.model.attributes[assoc.alias].attribute - )}_${originInfo.model.attributes[assoc.alias].column}`, + `${joinTableAlias}.${singular(originInfo.model.attributes[assoc.alias].attribute)}_${ + originInfo.model.attributes[assoc.alias].column + }`, `${destinationInfo.alias}.${destinationInfo.model.primaryKey}` ); - return; + } else { + const externalKey = + assoc.type === 'collection' + ? `${destinationInfo.alias}.${assoc.via || destinationInfo.model.primaryKey}` + : `${destinationInfo.alias}.${destinationInfo.model.primaryKey}`; + + const internalKey = + assoc.type === 'collection' + ? `${originInfo.alias}.${originInfo.model.primaryKey}` + : `${originInfo.alias}.${assoc.alias}`; + + qb.leftJoin( + `${destinationInfo.model.databaseName}.${destinationInfo.model.collectionName} AS ${destinationInfo.alias}`, + externalKey, + internalKey + ); } - - const externalKey = - assoc.type === 'collection' - ? `${destinationInfo.alias}.${assoc.via || - destinationInfo.model.primaryKey}` - : `${destinationInfo.alias}.${destinationInfo.model.primaryKey}`; - - const internalKey = - assoc.type === 'collection' - ? `${originInfo.alias}.${originInfo.model.primaryKey}` - : `${originInfo.alias}.${assoc.alias}`; - - qb.leftJoin( - `${destinationInfo.model.databaseName}.${destinationInfo.model.collectionName} AS ${destinationInfo.alias}`, - externalKey, - internalKey - ); }; /** @@ -208,9 +215,7 @@ const buildWhereClause = ({ qb, field, operator, value }) => { if (Array.isArray(value) && !['in', 'nin'].includes(operator)) { return qb.where(subQb => { for (let val of value) { - subQb.orWhere(q => - buildWhereClause({ qb: q, field, operator, value: val }) - ); + subQb.orWhere(q => buildWhereClause({ qb: q, field, operator, value: val })); } }); } @@ -258,7 +263,6 @@ const findModelByAssoc = assoc => { return models[assoc.collection || assoc.model]; }; -const findAssoc = (model, key) => - model.associations.find(assoc => assoc.alias === key); +const findAssoc = (model, key) => model.associations.find(assoc => assoc.alias === key); module.exports = buildQuery; diff --git a/packages/strapi-connector-bookshelf/lib/mount-models.js b/packages/strapi-connector-bookshelf/lib/mount-models.js index 06aa5c2e51..708b0b9285 100644 --- a/packages/strapi-connector-bookshelf/lib/mount-models.js +++ b/packages/strapi-connector-bookshelf/lib/mount-models.js @@ -236,6 +236,7 @@ module.exports = ({ models, target }, ctx) => { if (otherKey === foreignKey) { otherKey = `related_${otherKey}`; + details.attribute = `related_${details.attribute}`; } loadedModel[name] = function() { diff --git a/packages/strapi/__tests__/deepFiltering.test.e2e.js b/packages/strapi/__tests__/deepFiltering.test.e2e.js new file mode 100644 index 0000000000..61c1f14a33 --- /dev/null +++ b/packages/strapi/__tests__/deepFiltering.test.e2e.js @@ -0,0 +1,164 @@ +// Test an API with all the possible filed types and simple filterings (no deep filtering, no relations) + +const { registerAndLogin } = require('../../../test/helpers/auth'); +const createModelsUtils = require('../../../test/helpers/models'); +const { createAuthRequest } = require('../../../test/helpers/request'); + +let rq; +let modelsUtils; +let data = { + paniniCards: [], + collectors: [], +}; + +const paniniCard = { + name: 'paniniCard', + kind: 'collectionType', + attributes: { + name: { + type: 'string', + }, + }, +}; + +const collector = { + name: 'collector', + kind: 'collectionType', + attributes: { + name: { + type: 'string', + }, + panini_cards: { + nature: 'manyWay', + target: 'application::panini-card.panini-card', + unique: false, + }, + collector_friends: { + nature: 'manyWay', + target: '__self__', + unique: false, + }, + }, +}; + +const paniniCardFixtures = [ + { + name: 'Hugo LLORIS', + }, + { + name: 'Samuel UMTITI', + }, +]; + +async function createFixtures() { + for (let paniniCard of paniniCardFixtures) { + const res = await rq({ + method: 'POST', + url: '/panini-cards', + body: paniniCard, + }); + + data.paniniCards.push(res.body); + } + + const collector1Res = await rq({ + method: 'POST', + url: '/collectors', + body: { + name: 'Bernard', + panini_cards: [data.paniniCards[0].id, data.paniniCards[1].id], + }, + }); + data.collectors.push(collector1Res.body); + + const collector2Res = await rq({ + method: 'POST', + url: '/collectors', + body: { + name: 'Isabelle', + panini_cards: [data.paniniCards[0].id], + collector_friends: [data.collectors[0].id], + }, + }); + data.collectors.push(collector2Res.body); +} + +async function deleteFixtures() { + for (let paniniCard of data.paniniCards) { + await rq({ + method: 'DELETE', + url: `/panini-cards/${paniniCard.id}`, + }); + } + for (let collector of data.collectors) { + await rq({ + method: 'DELETE', + url: `/collectors/${collector.id}`, + }); + } +} + +describe('Deep Filtering API', () => { + beforeAll(async () => { + const token = await registerAndLogin(); + rq = createAuthRequest(token); + + modelsUtils = createModelsUtils({ rq }); + await modelsUtils.createContentTypes([paniniCard, collector]); + await createFixtures(); + }, 60000); + + afterAll(async () => { + await deleteFixtures(); + await modelsUtils.deleteContentTypes(['collector', 'panini-card']); + }, 60000); + + describe('Filter on a manyWay relation', () => { + test('Should return 2 results', async () => { + const res = await rq({ + method: 'GET', + url: '/collectors', + qs: { + 'panini_cards.name': data.paniniCards[0].name, + }, + }); + + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBe(2); + expect(res.body[0]).toMatchObject(data.collectors[0]); + expect(res.body[1]).toMatchObject(data.collectors[1]); + }); + + test('Should return 1 result', async () => { + const res = await rq({ + method: 'GET', + url: '/collectors', + qs: { + 'panini_cards.name': data.paniniCards[1].name, + }, + }); + + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBe(1); + expect(res.body[0]).toMatchObject(data.collectors[0]); + }); + }); + + describe('Filter on a self manyWay relation', () => { + test('Should return 1 result', async () => { + const res = await rq({ + method: 'GET', + url: '/collectors', + qs: { + 'collector_friends.name': data.collectors[0].name, + }, + }); + + console.log('res', JSON.stringify(res.body, null, 2)); + + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBe(1); + expect(res.body[0]).toMatchObject(data.collectors[1]); + }); + }); +});