From 3a24574ac0bbda2cb085e5443ac1e6f2fb6893a6 Mon Sep 17 00:00:00 2001 From: Tim Leslie Date: Mon, 30 Sep 2019 17:07:56 +1000 Subject: [PATCH] Update queryParser to access a listAdapter --- .changeset/plenty-owls-vanish/changes.json | 10 + .changeset/plenty-owls-vanish/changes.md | 1 + .../adapter-mongoose/lib/adapter-mongoose.js | 19 +- .../examples/nested-relationship/index.js | 62 +- .../examples/relationship/index.js | 75 +- packages/mongo-join-builder/index.js | 6 - .../mongo-join-builder/lib/query-parser.js | 25 +- .../mongo-join-builder/tests/index.test.js | 157 +-- .../tests/mongo-results.test.js | 274 +----- .../tests/query-parser.test.js | 922 ++---------------- packages/mongo-join-builder/tests/utils.js | 104 ++ 11 files changed, 275 insertions(+), 1380 deletions(-) create mode 100644 .changeset/plenty-owls-vanish/changes.json create mode 100644 .changeset/plenty-owls-vanish/changes.md create mode 100644 packages/mongo-join-builder/tests/utils.js diff --git a/.changeset/plenty-owls-vanish/changes.json b/.changeset/plenty-owls-vanish/changes.json new file mode 100644 index 00000000000..f71fb9743c8 --- /dev/null +++ b/.changeset/plenty-owls-vanish/changes.json @@ -0,0 +1,10 @@ +{ + "releases": [{ "name": "@keystone-alpha/mongo-join-builder", "type": "major" }], + "dependents": [ + { + "name": "@keystone-alpha/adapter-mongoose", + "type": "patch", + "dependencies": ["@keystone-alpha/mongo-join-builder"] + } + ] +} diff --git a/.changeset/plenty-owls-vanish/changes.md b/.changeset/plenty-owls-vanish/changes.md new file mode 100644 index 00000000000..be1d7b553af --- /dev/null +++ b/.changeset/plenty-owls-vanish/changes.md @@ -0,0 +1 @@ +Update queryParser to access a `{ listAdapter }` rather than a `{ tokenizer }`. This means that `{ simpleTokenizer, relationshipTokenizer, getRelatedListAdapterFromQueryPathFactory}` do not need to be exported from `mongo-join-builder`. \ No newline at end of file diff --git a/packages/adapter-mongoose/lib/adapter-mongoose.js b/packages/adapter-mongoose/lib/adapter-mongoose.js index dbdc1b6c281..f96b5818591 100644 --- a/packages/adapter-mongoose/lib/adapter-mongoose.js +++ b/packages/adapter-mongoose/lib/adapter-mongoose.js @@ -15,9 +15,6 @@ const { BaseFieldAdapter, } = require('@keystone-alpha/keystone'); const { - simpleTokenizer, - relationshipTokenizer, - getRelatedListAdapterFromQueryPathFactory, queryParser, pipelineBuilder, mutationBuilder, @@ -122,21 +119,7 @@ class MongooseListAdapter extends BaseListAdapter { this.model = null; this.queryBuilder = async (query, aggregate) => { - const queryTree = queryParser( - { - tokenizer: { - // executed for simple query components (eg; 'fulfilled: false' / name: 'a') - simple: simpleTokenizer({ - getRelatedListAdapterFromQueryPath: getRelatedListAdapterFromQueryPathFactory(this), - }), - // executed for complex query components (eg; items: { ... }) - relationship: relationshipTokenizer({ - getRelatedListAdapterFromQueryPath: getRelatedListAdapterFromQueryPathFactory(this), - }), - }, - }, - query - ); + const queryTree = queryParser({ listAdapter: this }, query); const pipeline = pipelineBuilder(queryTree); const postQueryMutations = mutationBuilder(queryTree.relationships); // Run the query against the given database and collection diff --git a/packages/mongo-join-builder/examples/nested-relationship/index.js b/packages/mongo-join-builder/examples/nested-relationship/index.js index 46821f2af6e..37c1ebaabdc 100644 --- a/packages/mongo-join-builder/examples/nested-relationship/index.js +++ b/packages/mongo-join-builder/examples/nested-relationship/index.js @@ -1,62 +1,8 @@ -const omitBy = require('lodash.omitby'); - const { queryParser, pipelineBuilder, mutationBuilder } = require('../../'); const getDatabase = require('../database'); -const builder = async (query, aggregate) => { - const queryTree = queryParser( - { - tokenizer: { - // executed for simple query components (eg; 'fulfilled: false' / name: 'a') - // eslint-disable-next-line no-unused-vars - simple: (query, key, path) => [{ [key]: { $eq: query[key] } }], - - // executed for complex query components (eg; items: { ... }) - relationship: (query, key, path, uid) => { - const [field, filter] = key.split('_'); - - const fieldToTableMap = { - items: 'items', - stock: 'warehouses', - }; - - return { - from: fieldToTableMap[field], // the collection name to join with - field: field, // The field on the 'orders' collection - // A mutation to run on the data post-join. Useful for merging joined - // data back into the original object. - // Executed on a depth-first basis for nested relationships. - // eslint-disable-next-line no-unused-vars - postQueryMutation: (parentObj, keyOfRelationship, rootObj, pathToParent) => { - // For this example, we want the joined items to overwrite the array - //of IDs - return omitBy( - { - ...parentObj, - [field]: parentObj[keyOfRelationship], - }, - // Clean up the result to remove the intermediate results - (_, keyToOmit) => keyToOmit.startsWith(uid) - ); - }, - // The conditions under which an item from the 'orders' collection is - // considered a match and included in the end result - // All the keys on an 'order' are available, plus 3 special keys: - // 1) __every - is `true` when every joined item matches the - // query - // 2) __some - is `true` when some joined item matches the - // query - // 3) __none - is `true` when none of the joined items match - // the query - matchTerm: { [`${uid}_${field}_${filter}`]: true }, - // Flag this is a to-many relationship - many: true, - }; - }, - }, - }, - query - ); +const builder = async (query, aggregate, listAdapter) => { + const queryTree = queryParser({ listAdapter }, query); const pipeline = pipelineBuilder(queryTree); const postQueryMutations = mutationBuilder(queryTree.relationships); // Run the query against the given database and collection @@ -79,9 +25,9 @@ const query = { (async () => { const database = await getDatabase(); - + const listAdapter = {}; try { - const result = await builder(query, getAggregate(database, 'orders')); + const result = await builder(query, getAggregate(database, 'orders'), listAdapter); console.log('orders:', prettyPrintResults(result)); } catch (error) { console.error(error); diff --git a/packages/mongo-join-builder/examples/relationship/index.js b/packages/mongo-join-builder/examples/relationship/index.js index 2c9b07931ed..149e79f82f0 100644 --- a/packages/mongo-join-builder/examples/relationship/index.js +++ b/packages/mongo-join-builder/examples/relationship/index.js @@ -1,74 +1,7 @@ -const omitBy = require('lodash.omitby'); - const { queryParser, pipelineBuilder, mutationBuilder } = require('../../'); const getDatabase = require('../database'); -const builder = async (query, aggregate) => { - const queryTree = queryParser( - { - tokenizer: { - // executed for simple query components (eg; 'fulfilled: false' / name: 'a') - simple: (query, key, path) => { - if (path[0] === 'fulfilled') { - return [ - { - // for the fulfilled clause, we want a direct equality check - [key]: { $eq: query[key] }, - }, - ]; - } else { - return [ - { - // in this example, we want an 'in' check, so we use a regex - [key]: { $regex: new RegExp(query[key]) }, - }, - ]; - } - }, - - // executed for complex query components (eg; items: { ... }) - relationship: (query, key, path, uid) => { - const fieldToTableMap = { - items: 'items', - stock: 'warehouses', - }; - - return { - from: fieldToTableMap[key], // the collection name to join with - field: key, // The field on the 'orders' collection - // A mutation to run on the data post-join. Useful for merging joined - // data back into the original object. - // Executed on a depth-first basis for nested relationships. - // eslint-disable-next-line no-unused-vars - postQueryMutation: (parentObj, keyOfRelationship, rootObj, pathToParent) => { - // For this example, we want the joined items to overwrite the array - //of IDs - return omitBy( - { - ...parentObj, - [key]: parentObj[keyOfRelationship], - }, - // Clean up the result to remove the intermediate results - (_, keyToOmit) => keyToOmit.startsWith(uid) - ); - }, - // The conditions under which an item from the 'orders' collection is - // considered a match and included in the end result - // All the keys on an 'order' are available, plus 3 special keys: - // 1) __every - is `true` when every joined item matches the - // query - // 2) __some - is `true` when some joined item matches the - // query - // 3) __none - is `true` when none of the joined items match - // the query - matchTerm: { [`${uid}_${key}_every`]: true }, - // Flag this is a to-many relationship - many: true, - }; - }, - }, - }, - query - ); +const builder = async (query, aggregate, listAdapter) => { + const queryTree = queryParser({ listAdapter }, query); const pipeline = pipelineBuilder(queryTree); const postQueryMutations = mutationBuilder(queryTree.relationships); // Run the query against the given database and collection @@ -85,9 +18,9 @@ const query = { (async () => { const database = await getDatabase(); - + const listAdapter = {}; try { - const result = await builder(query, getAggregate(database, 'orders')); + const result = await builder(query, getAggregate(database, 'orders'), listAdapter); console.log('orders:', prettyPrintResults(result)); } catch (error) { console.error(error); diff --git a/packages/mongo-join-builder/index.js b/packages/mongo-join-builder/index.js index 363dd607561..f305d02a394 100644 --- a/packages/mongo-join-builder/index.js +++ b/packages/mongo-join-builder/index.js @@ -1,13 +1,7 @@ -const { simpleTokenizer } = require('./lib/tokenizers/simple'); -const { relationshipTokenizer } = require('./lib/tokenizers/relationship'); -const { getRelatedListAdapterFromQueryPathFactory } = require('./lib/tokenizers/relationship-path'); const { queryParser } = require('./lib/query-parser'); const { pipelineBuilder, mutationBuilder } = require('./lib/join-builder'); module.exports = { - simpleTokenizer, - relationshipTokenizer, - getRelatedListAdapterFromQueryPathFactory, queryParser, pipelineBuilder, mutationBuilder, diff --git a/packages/mongo-join-builder/lib/query-parser.js b/packages/mongo-join-builder/lib/query-parser.js index 1961560a0d7..3c8650e02b1 100644 --- a/packages/mongo-join-builder/lib/query-parser.js +++ b/packages/mongo-join-builder/lib/query-parser.js @@ -1,6 +1,10 @@ const cuid = require('cuid'); const { getType, flatten, objMerge } = require('@keystone-alpha/utils'); +const { simpleTokenizer } = require('./tokenizers/simple'); +const { relationshipTokenizer } = require('./tokenizers/relationship'); +const { getRelatedListAdapterFromQueryPathFactory } = require('./tokenizers/relationship-path'); + // If it's 0 or 1 items, we can use it as-is. Any more needs an $and/$or const joinTerms = (matchTerms, joinOp) => matchTerms.length > 1 ? { [joinOp]: matchTerms } : matchTerms[0]; @@ -11,7 +15,9 @@ const flattenQueries = (parsedQueries, joinOp) => ({ relationships: objMerge(parsedQueries.map(q => q.relationships)), }); -function parser({ tokenizer, getUID = cuid }, query, pathSoFar = []) { +function parser({ listAdapter, getUID = cuid }, query, pathSoFar = []) { + const getRelatedListAdapterFromQueryPath = getRelatedListAdapterFromQueryPathFactory(listAdapter); + if (getType(query) !== 'Object') { throw new Error( `Expected an Object for query, got ${getType(query)} at path ${pathSoFar.join('.')}` @@ -23,16 +29,21 @@ function parser({ tokenizer, getUID = cuid }, query, pathSoFar = []) { if (['AND', 'OR'].includes(key)) { // An AND/OR query component return flattenQueries( - value.map((_query, index) => parser({ tokenizer, getUID }, _query, [...path, index])), + value.map((_query, index) => parser({ listAdapter, getUID }, _query, [...path, index])), { AND: '$and', OR: '$or' }[key] ); } else if (getType(value) === 'Object') { // A relationship query component const uid = getUID(key); - const queryAst = tokenizer.relationship(query, key, path, uid); + const queryAst = relationshipTokenizer({ getRelatedListAdapterFromQueryPath })( + query, + key, + path, + uid + ); if (getType(queryAst) !== 'Object') { throw new Error( - `Must return an Object from 'tokenizer.relationship' function, given ${path.join('.')}` + `Must return an Object from 'relationshipTokenizer' function, given ${path.join('.')}` ); } return { @@ -40,14 +51,14 @@ function parser({ tokenizer, getUID = cuid }, query, pathSoFar = []) { // parent item is included in the final list matchTerm: queryAst.matchTerm, postJoinPipeline: [], - relationships: { [uid]: { ...queryAst, ...parser({ tokenizer, getUID }, value, path) } }, + relationships: { [uid]: { ...queryAst, ...parser({ listAdapter, getUID }, value, path) } }, }; } else { // A simple field query component - const queryAst = tokenizer.simple(query, key, path); + const queryAst = simpleTokenizer({ getRelatedListAdapterFromQueryPath })(query, key, path); if (getType(queryAst) !== 'Object') { throw new Error( - `Must return an Object from 'tokenizer.simple' function, given ${path.join('.')}` + `Must return an Object from 'simpleTokenizer' function, given ${path.join('.')}` ); } return { diff --git a/packages/mongo-join-builder/tests/index.test.js b/packages/mongo-join-builder/tests/index.test.js index 93ad6fd4f32..e909631e84c 100644 --- a/packages/mongo-join-builder/tests/index.test.js +++ b/packages/mongo-join-builder/tests/index.test.js @@ -1,76 +1,23 @@ const { queryParser, pipelineBuilder, mutationBuilder } = require('../'); +const { listAdapter } = require('./utils'); describe('Test main export', () => { - describe('throws if tokenising function returns non-Object', () => { - test('simple', async () => { - let tokenizer = { simple: () => undefined, relationship: () => ({}) }; - expect(() => queryParser({ tokenizer }, { name: 'foobar' })).toThrow(Error); + test('throws if listAdapter is non-Object', async () => { + expect(() => queryParser({ listAdapter: undefined }, { name: 'foobar' })).toThrow(Error); - tokenizer = { simple: () => 10, relationship: () => ({}) }; - expect(() => queryParser({ tokenizer }, { name: 'foobar' })).toThrow(Error); - - tokenizer = { simple: () => 'hello', relationship: () => ({}) }; - expect(() => queryParser({ tokenizer }, { name: 'foobar' })).toThrow(Error); - - // Shouldn't throw - tokenizer = { simple: () => ({}), relationship: () => ({}) }; - await queryParser({ tokenizer }, { name: 'foobar' }); - - // Shouldn't throw - tokenizer = { simple: () => ({}), relationship: () => [] }; - await queryParser({ tokenizer }, { name: 'foobar' }); - }); - - test('relationship', async () => { - let tokenizer = { relationship: () => undefined, simple: () => ({}) }; - expect(() => queryParser({ tokenizer }, { posts: {} })).toThrow(Error); - - tokenizer = { relationship: () => 10, simple: () => ({}) }; - expect(() => queryParser({ tokenizer }, { posts: {} })).toThrow(Error); - - tokenizer = { relationship: () => 'hello', simple: () => ({}) }; - expect(() => queryParser({ tokenizer }, { posts: {} })).toThrow(Error); - - // Shouldn't throw - tokenizer = { relationship: () => ({}), simple: () => ({}) }; - await queryParser({ tokenizer }, { posts: {} }); - - // Shouldn't throw - tokenizer = { relationship: () => ({}), simple: () => [] }; - await queryParser({ tokenizer }, { posts: {} }); - }); + // Shouldn't throw + await queryParser({ listAdapter }, { name: 'foobar' }); }); test('runs the query', async () => { - // Purposely mutate the objects down to a simple object for the lolz - // called with (parentValue, keyOfRelationship, rootObject, path) - const postQueryMutation = jest.fn((parentValue, key, rootObject, path) => ({ - ...parentValue, - mutated: path.join('.'), - })); - - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key) => { - const [table] = key.split('_'); - return { - from: `${table}-collection`, - field: table, - postQueryMutation, - matchTerm: { $exists: true, $ne: [] }, - many: true, - }; - }), - }; - const query = { AND: [ { name: 'foobar' }, { age: 23 }, - { posts_every: { AND: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] } }, + { posts_every: { AND: [{ title: 'hello' }, { tags_some: { name: 'foo' } }] } }, ], }; - const queryTree = queryParser({ tokenizer, getUID: jest.fn(key => key) }, query); + const queryTree = queryParser({ listAdapter, getUID: jest.fn(key => key) }, query); const aggregateResponse = [ { @@ -82,8 +29,8 @@ describe('Test main export', () => { { id: 1, title: 'hello', - labels: [4, 5], - labels_some_labels: [ + tags: [4, 5], + tags_some_tags: [ { id: 4, name: 'foo', @@ -97,8 +44,8 @@ describe('Test main export', () => { { id: 3, title: 'hello', - labels: [6], - labels_some_labels: [ + tags: [6], + tags_some_tags: [ { id: 6, name: 'foo', @@ -116,18 +63,18 @@ describe('Test main export', () => { expect(pipeline).toMatchObject([ { $lookup: { - from: 'posts-collection', + from: 'posts', as: 'posts_every_posts', let: { posts_every_posts_ids: '$posts' }, pipeline: [ { $match: { $expr: { $in: ['$_id', '$$posts_every_posts_ids'] } } }, { $lookup: { - from: 'labels-collection', - as: 'labels_some_labels', - let: { labels_some_labels_ids: '$labels' }, + from: 'tags', + as: 'tags_some_tags', + let: { tags_some_tags_ids: '$tags' }, pipeline: [ - { $match: { $expr: { $in: ['$_id', '$$labels_some_labels_ids'] } } }, + { $match: { $expr: { $in: ['$_id', '$$tags_some_tags_ids'] } } }, { $match: { name: { $eq: 'foo' } } }, { $addFields: { id: '$_id' } }, ], @@ -135,16 +82,16 @@ describe('Test main export', () => { }, { $addFields: { - labels_some_labels_every: { - $eq: [{ $size: '$labels_some_labels' }, { $size: '$labels' }], + tags_some_tags_every: { + $eq: [{ $size: '$tags_some_tags' }, { $size: '$tags' }], }, - labels_some_labels_none: { $eq: [{ $size: '$labels_some_labels' }, 0] }, - labels_some_labels_some: { $gt: [{ $size: '$labels_some_labels' }, 0] }, + tags_some_tags_none: { $eq: [{ $size: '$tags_some_tags' }, 0] }, + tags_some_tags_some: { $gt: [{ $size: '$tags_some_tags' }, 0] }, }, }, { $match: { - $and: [{ title: { $eq: 'hello' } }, { $exists: true, $ne: [] }], + $and: [{ title: { $eq: 'hello' } }, { tags_some_tags_some: true }], }, }, { @@ -162,7 +109,11 @@ describe('Test main export', () => { }, { $match: { - $and: [{ name: { $eq: 'foobar' } }, { age: { $eq: 23 } }, { $exists: true, $ne: [] }], + $and: [ + { name: { $eq: 'foobar' } }, + { age: { $eq: 23 } }, + { posts_every_posts_every: true }, + ], }, }, { $addFields: { id: '$_id' } }, @@ -170,66 +121,10 @@ describe('Test main export', () => { expect(result).toMatchObject([ { - mutated: '0', name: 'foobar', age: 23, posts: [1, 3], - posts_every_posts: [ - { - id: 1, - mutated: '0.posts_every_posts.0', - title: 'hello', - labels: [4, 5], - labels_some_labels: [ - { - id: 4, - name: 'foo', - }, - { - id: 5, - name: 'foo', - }, - ], - }, - { - mutated: '0.posts_every_posts.1', - id: 3, - title: 'hello', - labels: [6], - labels_some_labels: [ - { - id: 6, - name: 'foo', - }, - ], - }, - ], }, ]); }); - - test('correctly rejects when error in simple tokeniser', async () => { - const tokenizer = { - simple: () => { - throw new Error('Whoops'); - }, - relationship: () => {}, - }; - - const query = { name: 'foobar' }; - - await expect(() => queryParser({ tokenizer }, query)).toThrow('Whoops'); - }); - - test('correctly rejects when error in relationship tokeniser', async () => { - const tokenizer = { - simple: () => {}, - relationship: () => { - throw new Error('Uh-oh'); - }, - }; - - const query = { user: { name: 'foobar' } }; - await expect(() => queryParser({ tokenizer }, query)).toThrow('Uh-oh'); - }); }); diff --git a/packages/mongo-join-builder/tests/mongo-results.test.js b/packages/mongo-join-builder/tests/mongo-results.test.js index 03b8bf686fd..507503db5b3 100644 --- a/packages/mongo-join-builder/tests/mongo-results.test.js +++ b/packages/mongo-join-builder/tests/mongo-results.test.js @@ -1,4 +1,5 @@ const { queryParser, pipelineBuilder, mutationBuilder } = require('../'); +const { postsAdapter, listAdapter } = require('./utils'); const { MongoClient } = require('mongodb'); const MongoDBMemoryServer = require('mongodb-memory-server').default; @@ -62,22 +63,7 @@ describe('mongo memory servier is alive', () => { describe('Testing against real data', () => { test('performs simple queries', async () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key) => { - const [table] = key.split('_'); - return { - from: `${table}-collection`, - field: table, - // called with (parentValue, keyOfRelationship, rootObject, path) - postQueryMutation: () => {}, - matchTerm: { $exists: true, $ne: [] }, - many: true, - }; - }), - }; - - const builder = mongoJoinBuilder({ tokenizer }); + const builder = mongoJoinBuilder({ listAdapter }); const collection = mongoDb.collection('users'); await collection.insertMany([ @@ -130,22 +116,7 @@ describe('Testing against real data', () => { }); test('performs AND queries', async () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key) => { - const [table] = key.split('_'); - return { - from: `${table}-collection`, - field: table, - // called with (parentValue, keyOfRelationship, rootObject, path) - postQueryMutation: () => {}, - matchTerm: { $exists: true, $ne: [] }, - many: true, - }; - }), - }; - - const builder = mongoJoinBuilder({ tokenizer }); + const builder = mongoJoinBuilder({ listAdapter }); const collection = mongoDb.collection('users'); await collection.insertMany([ @@ -197,29 +168,7 @@ describe('Testing against real data', () => { }); test('performs to-one relationship queries', async () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key, path, uid) => { - const tableMap = { - author: 'users', - }; - - return { - from: tableMap[key], - field: key, - // called with (parentValue, keyOfRelationship, rootObject, path) - postQueryMutation: (parentData, keyOfRelationship) => ({ - ...parentData, - // Merge the found item back into the original key - [key]: parentData[keyOfRelationship][0], - }), - matchTerm: { [`${uid}_${key}_every`]: true }, - many: false, - }; - }), - }; - - const builder = mongoJoinBuilder({ tokenizer }); + const builder = mongoJoinBuilder({ listAdapter: postsAdapter }); const usersCollection = mongoDb.collection('users'); const postsCollection = mongoDb.collection('posts'); @@ -271,41 +220,13 @@ describe('Testing against real data', () => { { title: 'Hello world', status: 'published', - author: { - name: 'Jess', - type: 'author', - }, + author: insertedIds[0], }, ]); }); test('performs to-many relationship queries with no filter', async () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key, path, uid) => { - const [table, criteria] = key.split('_'); - return { - from: table, - field: table, - // called with (parentValue, keyOfRelationship, rootObject, path) - postQueryMutation: (parentData, keyOfRelationship) => ({ - ...parentData, - // Merge the found items back into the original key - [table]: parentData[table].map(id => { - return ( - parentData[keyOfRelationship].find( - relatedItem => relatedItem._id.toString() === id.toString() - ) || id - ); - }), - }), - matchTerm: { [`${uid}_${table}_${criteria}`]: true }, - many: true, - }; - }), - }; - - const builder = mongoJoinBuilder({ tokenizer }); + const builder = mongoJoinBuilder({ listAdapter }); const usersCollection = mongoDb.collection('users'); const postsCollection = mongoDb.collection('posts'); @@ -349,7 +270,6 @@ describe('Testing against real data', () => { const query = { type: 'author', - posts_every: {}, }; const result = await builder(query, getAggregate(mongoDb, 'users')); @@ -358,64 +278,18 @@ describe('Testing against real data', () => { { name: 'Jess', type: 'author', - posts: [ - { - title: 'Hello world', - status: 'published', - }, - { - title: 'An awesome post', - status: 'draft', - }, - ], + posts: [insertedIds[0], insertedIds[2]], }, { name: 'Alice', type: 'author', - posts: [ - { - title: 'Testing', - status: 'published', - }, - { - title: 'Another Thing', - status: 'published', - }, - ], + posts: [insertedIds[1], insertedIds[3]], }, ]); }); test('performs to-many relationship queries with postJoinPipeline', async () => { - const tokenizer = { - simple: jest.fn((query, key) => { - const value = query[key]; - if (key === '$limit') { - return { postJoinPipeline: [{ $limit: value }] }; - } else if (key === '$sort') { - const [sortBy, sortDirection] = value.split('_'); - return { postJoinPipeline: [{ $sort: { [sortBy]: sortDirection === 'ASC' ? 1 : -1 } }] }; - } - return { matchTerm: { [key]: { $eq: value } } }; - }), - relationship: jest.fn((query, key, path, uid) => { - const [table, criteria] = key.split('_'); - return { - from: table, - field: table, - // called with (parentValue, keyOfRelationship, rootObject, path) - postQueryMutation: (parentData, keyOfRelationship) => ({ - ...parentData, - // Merge the found items back into the original key - [table]: parentData[keyOfRelationship], - }), - matchTerm: { [`${uid}_${table}_${criteria}`]: true }, - many: true, - }; - }), - }; - - const builder = mongoJoinBuilder({ tokenizer }); + const builder = mongoJoinBuilder({ listAdapter }); const usersCollection = mongoDb.collection('users'); const postsCollection = mongoDb.collection('posts'); @@ -468,56 +342,21 @@ describe('Testing against real data', () => { status: 'published', $sort: 'title_ASC', }, - $limit: 1, + $first: 1, }; const result = await builder(query, getAggregate(mongoDb, 'users')); - expect(result).toMatchObject([ { name: 'Alice', type: 'author', - posts: [ - { - title: 'Another Thing', - status: 'published', - }, - { - title: 'Testing', - status: 'published', - }, - ], + posts: [insertedIds[1], insertedIds[3]], }, ]); }); test('performs to-many relationship queries', async () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key, path, uid) => { - const [table, criteria] = key.split('_'); - return { - from: table, - field: table, - // called with (parentValue, keyOfRelationship, rootObject, path) - postQueryMutation: (parentData, keyOfRelationship) => ({ - ...parentData, - // Merge the found items back into the original key - [table]: parentData[table].map(id => { - return ( - parentData[keyOfRelationship].find( - relatedItem => relatedItem._id.toString() === id.toString() - ) || id - ); - }), - }), - matchTerm: { [`${uid}_${table}_${criteria}`]: true }, - many: true, - }; - }), - }; - - const builder = mongoJoinBuilder({ tokenizer }); + const builder = mongoJoinBuilder({ listAdapter }); const usersCollection = mongoDb.collection('users'); const postsCollection = mongoDb.collection('posts'); @@ -572,47 +411,13 @@ describe('Testing against real data', () => { { name: 'Alice', type: 'author', - posts: [ - { - title: 'Testing', - status: 'published', - }, - { - title: 'Another Thing', - status: 'published', - }, - ], + posts: [insertedIds[1], insertedIds[3]], }, ]); }); test('performs to-many relationship queries with nested AND', async () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key, path, uid) => { - const [table, criteria] = key.split('_'); - return { - from: table, - field: table, - // called with (parentValue, keyOfRelationship, rootObject, path) - postQueryMutation: (parentData, keyOfRelationship) => ({ - ...parentData, - // Merge the found items back into the original key - [table]: parentData[table].map(id => { - return ( - parentData[keyOfRelationship].find( - relatedItem => relatedItem._id.toString() === id.toString() - ) || id - ); - }), - }), - matchTerm: { [`${uid}_${table}_${criteria}`]: true }, - many: true, - }; - }), - }; - - const builder = mongoJoinBuilder({ tokenizer }); + const builder = mongoJoinBuilder({ listAdapter }); const usersCollection = mongoDb.collection('users'); const postsCollection = mongoDb.collection('posts'); @@ -671,49 +476,13 @@ describe('Testing against real data', () => { { name: 'Alice', type: 'author', - posts: [ - { - title: 'Testing', - status: 'published', - approved: true, - }, - { - title: 'Another Thing', - status: 'published', - approved: true, - }, - ], + posts: [insertedIds[1], insertedIds[3]], }, ]); }); test('performs AND query with nested to-many relationship', async () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key, path, uid) => { - const [table, criteria] = key.split('_'); - return { - from: table, - field: table, - // called with (parentValue, keyOfRelationship, rootObject, path) - postQueryMutation: (parentData, keyOfRelationship) => ({ - ...parentData, - // Merge the found items back into the original key - [table]: parentData[table].map(id => { - return ( - parentData[keyOfRelationship].find( - relatedItem => relatedItem._id.toString() === id.toString() - ) || id - ); - }), - }), - matchTerm: { [`${uid}_${table}_${criteria}`]: true }, - many: true, - }; - }), - }; - - const builder = mongoJoinBuilder({ tokenizer }); + const builder = mongoJoinBuilder({ listAdapter }); const usersCollection = mongoDb.collection('users'); const postsCollection = mongoDb.collection('posts'); @@ -772,16 +541,7 @@ describe('Testing against real data', () => { { name: 'Alice', type: 'author', - posts: [ - { - title: 'Testing', - status: 'published', - }, - { - title: 'Another Thing', - status: 'published', - }, - ], + posts: [insertedIds[1], insertedIds[3]], }, ]); }); diff --git a/packages/mongo-join-builder/tests/query-parser.test.js b/packages/mongo-join-builder/tests/query-parser.test.js index 67026fcac98..dfc59db8439 100644 --- a/packages/mongo-join-builder/tests/query-parser.test.js +++ b/packages/mongo-join-builder/tests/query-parser.test.js @@ -1,632 +1,32 @@ const { queryParser } = require('../lib/query-parser'); +const { listAdapter } = require('./utils'); describe('query parser', () => { - test('requires a tokenizer option', () => { + test('requires a listAdapter option', () => { expect(() => queryParser()).toThrow(Error); expect(() => queryParser({}, { name: 'foobar' })).toThrow(Error); - expect(() => queryParser({ tokenizer: 'hello' }, { name: 'foobar' })).toThrow(Error); - expect(() => queryParser({ tokenizer: 10 }, { name: 'foobar' })).toThrow(Error); + expect(() => queryParser({ listAdapter }, { name: 'foobar' })).not.toThrow(Error); }); test('requires an object for the query', () => { expect(() => { - queryParser({ tokenizer: { simple: () => undefined } }, 'foobar'); + queryParser({ listAdapter }, 'foobar'); }).toThrow(Error); }); - describe('throws if tokenising function returns non-Object or non-Array', () => { - test('simple', () => { - expect(() => { - queryParser({ tokenizer: { simple: () => undefined } }, { name: 'foobar' }); - }).toThrow(Error); - - expect(() => { - queryParser({ tokenizer: { simple: () => 10 } }, { name: 'foobar' }); - }).toThrow(Error); - - expect(() => { - queryParser({ tokenizer: { simple: () => 'hello' } }, { name: 'foobar' }); - }).toThrow(Error); - - // Shouldn't throw - queryParser({ tokenizer: { simple: () => ({}) } }, { name: 'foobar' }); - }); - - test('relationship', () => { - expect(() => { - queryParser({ tokenizer: { relationship: () => undefined } }, { posts: {} }); - }).toThrow(Error); - - expect(() => { - queryParser({ tokenizer: { relationship: () => 10 } }, { posts: {} }); - }).toThrow(Error); - - expect(() => { - queryParser({ tokenizer: { relationship: () => 'hello' } }, { posts: {} }); - }).toThrow(Error); - - // Shouldn't throw - queryParser({ tokenizer: { relationship: () => ({}) } }, { posts: {} }); - }); - }); - describe('calling tokenizing functions', () => { - test('simple query', () => { - const simpleTokenizer = { simple: jest.fn(() => ({})) }; - queryParser( - { tokenizer: simpleTokenizer }, - { - name: 'foobar', - age_lte: 23, - } - ); - - expect(simpleTokenizer.simple).toHaveBeenCalledTimes(2); - // Change path to array - expect(simpleTokenizer.simple).toHaveBeenCalledWith( - { - name: 'foobar', - age_lte: 23, - }, - 'name', - ['name'] - ); - expect(simpleTokenizer.simple).toHaveBeenCalledWith( - { - name: 'foobar', - age_lte: 23, - }, - 'age_lte', - ['age_lte'] - ); - }); - - test('single relationship query', () => { - const complexTokenizer = { - simple: jest.fn(() => ({})), - relationship: jest.fn(() => ({})), - }; - queryParser( - { tokenizer: complexTokenizer }, - { - name: 'foobar', - age: 23, - posts_every: { title: 'hello' }, - } - ); - - expect(complexTokenizer.simple).toHaveBeenCalledTimes(3); - expect(complexTokenizer.simple).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { title: 'hello' }, - }, - 'name', - ['name'] - ); - expect(complexTokenizer.simple).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { title: 'hello' }, - }, - 'age', - ['age'] - ); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ title: 'hello' }, 'title', [ - 'posts_every', - 'title', - ]); - - expect(complexTokenizer.relationship).toHaveBeenCalledTimes(1); - expect(complexTokenizer.relationship).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { title: 'hello' }, - }, - 'posts_every', - ['posts_every'], - expect.any(String) - ); - }); - - test('nested relationship query', () => { - const complexTokenizer = { - simple: jest.fn(() => ({})), - relationship: jest.fn(() => ({})), - }; - queryParser( - { tokenizer: complexTokenizer }, - { - name: 'foobar', - age: 23, - posts_every: { - title: 'hello', - labels_some: { name: 'foo' }, - }, - } - ); - - expect(complexTokenizer.simple).toHaveBeenCalledTimes(4); - expect(complexTokenizer.simple).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { - title: 'hello', - labels_some: { name: 'foo' }, - }, - }, - 'name', - ['name'] - ); - expect(complexTokenizer.simple).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { - title: 'hello', - labels_some: { name: 'foo' }, - }, - }, - 'age', - ['age'] - ); - expect(complexTokenizer.simple).toHaveBeenCalledWith( - { - title: 'hello', - labels_some: { name: 'foo' }, - }, - 'title', - ['posts_every', 'title'] - ); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ name: 'foo' }, 'name', [ - 'posts_every', - 'labels_some', - 'name', - ]); - - expect(complexTokenizer.relationship).toHaveBeenCalledTimes(2); - expect(complexTokenizer.relationship).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { - title: 'hello', - labels_some: { name: 'foo' }, - }, - }, - 'posts_every', - ['posts_every'], - expect.any(String) - ); - expect(complexTokenizer.relationship).toHaveBeenCalledWith( - { - title: 'hello', - labels_some: { name: 'foo' }, - }, - 'labels_some', - ['posts_every', 'labels_some'], - expect.any(String) - ); - }); - - test('AND query', () => { - const simpleTokenizer = { simple: jest.fn(() => ({})) }; - queryParser({ tokenizer: simpleTokenizer }, { AND: [{ name: 'foobar' }, { age_lte: 23 }] }); - - expect(simpleTokenizer.simple).toHaveBeenCalledTimes(2); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ name: 'foobar' }, 'name', [ - 'AND', - 0, - 'name', - ]); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ age_lte: 23 }, 'age_lte', [ - 'AND', - 1, - 'age_lte', - ]); - }); - - test('AND query with extra key', () => { - const simpleTokenizer = { simple: jest.fn(() => ({})) }; - queryParser( - { tokenizer: simpleTokenizer }, - { - AND: [{ name: 'foobar' }, { age_lte: 23 }], - age_gte: 20, - } - ); - - expect(simpleTokenizer.simple).toHaveBeenCalledTimes(3); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ name: 'foobar' }, 'name', [ - 'AND', - 0, - 'name', - ]); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ age_lte: 23 }, 'age_lte', [ - 'AND', - 1, - 'age_lte', - ]); - expect(simpleTokenizer.simple).toHaveBeenCalledWith( - { - AND: [{ name: 'foobar' }, { age_lte: 23 }], - age_gte: 20, - }, - 'age_gte', - ['age_gte'] - ); - }); - - test('OR query', () => { - const simpleTokenizer = { simple: jest.fn(() => ({})) }; - queryParser({ tokenizer: simpleTokenizer }, { OR: [{ name: 'foobar' }, { age_lte: 23 }] }); - - expect(simpleTokenizer.simple).toHaveBeenCalledTimes(2); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ name: 'foobar' }, 'name', [ - 'OR', - 0, - 'name', - ]); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ age_lte: 23 }, 'age_lte', [ - 'OR', - 1, - 'age_lte', - ]); - }); - - test('OR query with extra key', () => { - const simpleTokenizer = { simple: jest.fn(() => ({})) }; - queryParser( - { tokenizer: simpleTokenizer }, - { - OR: [{ name: 'foobar' }, { age_lte: 23 }], - age_gte: 20, - } - ); - - expect(simpleTokenizer.simple).toHaveBeenCalledTimes(3); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ name: 'foobar' }, 'name', [ - 'OR', - 0, - 'name', - ]); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ age_lte: 23 }, 'age_lte', [ - 'OR', - 1, - 'age_lte', - ]); - expect(simpleTokenizer.simple).toHaveBeenCalledWith( - { - OR: [{ name: 'foobar' }, { age_lte: 23 }], - age_gte: 20, - }, - 'age_gte', - ['age_gte'] - ); - }); - - test('OR query with extra AND query', () => { - const simpleTokenizer = { simple: jest.fn(() => ({})) }; - queryParser( - { tokenizer: simpleTokenizer }, - { - OR: [{ name: 'foobar' }, { age_lte: 23 }], - AND: [{ age_gte: 20 }, { email: 'foo@bar.com' }], - } - ); - - expect(simpleTokenizer.simple).toHaveBeenCalledTimes(4); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ name: 'foobar' }, 'name', [ - 'OR', - 0, - 'name', - ]); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ age_lte: 23 }, 'age_lte', [ - 'OR', - 1, - 'age_lte', - ]); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ name: 'foobar' }, 'name', [ - 'OR', - 0, - 'name', - ]); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ age_gte: 20 }, 'age_gte', [ - 'AND', - 0, - 'age_gte', - ]); - expect(simpleTokenizer.simple).toHaveBeenCalledWith({ email: 'foo@bar.com' }, 'email', [ - 'AND', - 1, - 'email', - ]); - }); - test('AND query with invalid query type', () => { - const simpleTokenizer = { simple: jest.fn(() => []) }; - expect(() => - queryParser({ tokenizer: simpleTokenizer }, { AND: [{ name: 'foobar' }, 23] }) - ).toThrow(Error); + expect(() => queryParser({ listAdapter }, { AND: [{ name: 'foobar' }, 23] })).toThrow(Error); }); test('OR query with invalid query type', () => { - const simpleTokenizer = { simple: jest.fn(() => []) }; - expect(() => - queryParser({ tokenizer: simpleTokenizer }, { OR: [{ name: 'foobar' }, 23] }) - ).toThrow(Error); - }); - - test('complex query with nested AND', () => { - const complexTokenizer = { - simple: jest.fn(() => ({})), - relationship: jest.fn(() => ({})), - }; - queryParser( - { tokenizer: complexTokenizer }, - { - name: 'foobar', - age: 23, - posts_every: { AND: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] }, - } - ); - - expect(complexTokenizer.simple).toHaveBeenCalledTimes(4); - expect(complexTokenizer.simple).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { AND: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] }, - }, - 'name', - ['name'] - ); - expect(complexTokenizer.simple).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { AND: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] }, - }, - 'age', - ['age'] - ); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ title: 'hello' }, 'title', [ - 'posts_every', - 'AND', - 0, - 'title', - ]); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ name: 'foo' }, 'name', [ - 'posts_every', - 'AND', - 1, - 'labels_some', - 'name', - ]); - - expect(complexTokenizer.relationship).toHaveBeenCalledTimes(2); - expect(complexTokenizer.relationship).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { AND: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] }, - }, - 'posts_every', - ['posts_every'], - expect.any(String) - ); - expect(complexTokenizer.relationship).toHaveBeenCalledWith( - { labels_some: { name: 'foo' } }, - 'labels_some', - ['posts_every', 'AND', 1, 'labels_some'], - expect.any(String) - ); - }); - - test('complex query with nested OR', () => { - const complexTokenizer = { - simple: jest.fn(() => ({})), - relationship: jest.fn(() => ({})), - }; - queryParser( - { tokenizer: complexTokenizer }, - { - name: 'foobar', - age: 23, - posts_every: { OR: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] }, - } - ); - - expect(complexTokenizer.simple).toHaveBeenCalledTimes(4); - expect(complexTokenizer.simple).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { OR: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] }, - }, - 'name', - ['name'] - ); - expect(complexTokenizer.simple).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { OR: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] }, - }, - 'age', - ['age'] - ); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ title: 'hello' }, 'title', [ - 'posts_every', - 'OR', - 0, - 'title', - ]); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ name: 'foo' }, 'name', [ - 'posts_every', - 'OR', - 1, - 'labels_some', - 'name', - ]); - - expect(complexTokenizer.relationship).toHaveBeenCalledTimes(2); - expect(complexTokenizer.relationship).toHaveBeenCalledWith( - { - name: 'foobar', - age: 23, - posts_every: { OR: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] }, - }, - 'posts_every', - ['posts_every'], - expect.any(String) - ); - expect(complexTokenizer.relationship).toHaveBeenCalledWith( - { labels_some: { name: 'foo' } }, - 'labels_some', - ['posts_every', 'OR', 1, 'labels_some'], - expect.any(String) - ); - }); - - test('AND with nested complex query with nested AND', () => { - const complexTokenizer = { - simple: jest.fn(() => ({})), - relationship: jest.fn(() => ({})), - }; - queryParser( - { tokenizer: complexTokenizer }, - { - AND: [ - { name: 'foobar' }, - { age: 23 }, - { posts_every: { AND: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] } }, - ], - } - ); - - expect(complexTokenizer.simple).toHaveBeenCalledTimes(4); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ name: 'foobar' }, 'name', [ - 'AND', - 0, - 'name', - ]); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ age: 23 }, 'age', ['AND', 1, 'age']); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ title: 'hello' }, 'title', [ - 'AND', - 2, - 'posts_every', - 'AND', - 0, - 'title', - ]); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ name: 'foo' }, 'name', [ - 'AND', - 2, - 'posts_every', - 'AND', - 1, - 'labels_some', - 'name', - ]); - - expect(complexTokenizer.relationship).toHaveBeenCalledTimes(2); - expect(complexTokenizer.relationship).toHaveBeenCalledWith( - { - posts_every: { - AND: [{ title: 'hello' }, { labels_some: { name: 'foo' } }], - }, - }, - 'posts_every', - ['AND', 2, 'posts_every'], - expect.any(String) - ); - expect(complexTokenizer.relationship).toHaveBeenCalledWith( - { labels_some: { name: 'foo' } }, - 'labels_some', - ['AND', 2, 'posts_every', 'AND', 1, 'labels_some'], - expect.any(String) - ); - }); - - test('OR with nested complex query with nested OR', () => { - const complexTokenizer = { - simple: jest.fn(() => ({})), - relationship: jest.fn(() => ({})), - }; - queryParser( - { tokenizer: complexTokenizer }, - { - OR: [ - { name: 'foobar' }, - { age: 23 }, - { posts_every: { OR: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] } }, - ], - } - ); - - expect(complexTokenizer.simple).toHaveBeenCalledTimes(4); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ name: 'foobar' }, 'name', [ - 'OR', - 0, - 'name', - ]); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ age: 23 }, 'age', ['OR', 1, 'age']); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ title: 'hello' }, 'title', [ - 'OR', - 2, - 'posts_every', - 'OR', - 0, - 'title', - ]); - expect(complexTokenizer.simple).toHaveBeenCalledWith({ name: 'foo' }, 'name', [ - 'OR', - 2, - 'posts_every', - 'OR', - 1, - 'labels_some', - 'name', - ]); - - expect(complexTokenizer.relationship).toHaveBeenCalledTimes(2); - expect(complexTokenizer.relationship).toHaveBeenCalledWith( - { - posts_every: { - OR: [{ title: 'hello' }, { labels_some: { name: 'foo' } }], - }, - }, - 'posts_every', - ['OR', 2, 'posts_every'], - expect.any(String) - ); - expect(complexTokenizer.relationship).toHaveBeenCalledWith( - { labels_some: { name: 'foo' } }, - 'labels_some', - ['OR', 2, 'posts_every', 'OR', 1, 'labels_some'], - expect.any(String) - ); + expect(() => queryParser({ listAdapter }, { OR: [{ name: 'foobar' }, 23] })).toThrow(Error); }); }); describe('simple queries', () => { test('builds a simple query tree', () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - }; - - const queryTree = queryParser( - { tokenizer }, - { - name: 'foobar', - age: 23, - } - ); + const queryTree = queryParser({ listAdapter }, { name: 'foobar', age: 23 }); expect(queryTree).toMatchObject({ // No relationships in this test @@ -640,7 +40,10 @@ describe('query parser', () => { simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), }; - const queryTree = queryParser({ tokenizer }, { AND: [{ name: 'foobar' }, { age: 23 }] }); + const queryTree = queryParser( + { listAdapter, tokenizer }, + { AND: [{ name: 'foobar' }, { age: 23 }] } + ); expect(queryTree).toMatchObject({ // No relationships in this test @@ -654,7 +57,10 @@ describe('query parser', () => { simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), }; - const queryTree = queryParser({ tokenizer }, { OR: [{ name: 'foobar' }, { age: 23 }] }); + const queryTree = queryParser( + { listAdapter, tokenizer }, + { OR: [{ name: 'foobar' }, { age: 23 }] } + ); expect(queryTree).toMatchObject({ // No relationships in this test @@ -666,35 +72,12 @@ describe('query parser', () => { describe('relationship queries', () => { test('builds a query tree with to-many relationship and other postjoin filters', () => { - let relationPrefix; - - const tokenizer = { - simple: jest.fn((query, key) => { - const value = query[key]; - if (key.startsWith('$')) { - return { postJoinPipeline: [{ [key]: value }] }; - } - return { matchTerm: { [key]: { $eq: value } } }; - }), - relationship: jest.fn((query, key, path, prefix) => { - relationPrefix = prefix; - const field = key; - return { - from: `${field}-collection`, - field, - postQueryMutation: () => {}, - matchTerm: { [`${prefix}${field}_every`]: { $eq: true } }, - many: true, - }; - }), - }; - const queryTree = queryParser( - { tokenizer, getUID: jest.fn(key => key) }, + { listAdapter, getUID: jest.fn(key => key) }, { name: 'foobar', age: 23, - $limit: 1, + $first: 1, posts: { title: 'hello', $orderBy: 'title_ASC', @@ -705,46 +88,25 @@ describe('query parser', () => { expect(queryTree).toMatchObject({ relationships: { posts: { - from: 'posts-collection', + from: 'posts', field: 'posts', matchTerm: { title: { $eq: 'hello' } }, - postJoinPipeline: [{ $orderBy: 'title_ASC' }], + postJoinPipeline: [{ $sort: { title: 1 } }], postQueryMutation: expect.any(Function), many: true, relationships: {}, }, }, matchTerm: { - $and: [ - { name: { $eq: 'foobar' } }, - { age: { $eq: 23 } }, - { [`${relationPrefix}posts_every`]: { $eq: true } }, - ], + $and: [{ name: { $eq: 'foobar' } }, { age: { $eq: 23 } }, { posts_posts_every: true }], }, postJoinPipeline: [{ $limit: 1 }], }); }); test('builds a query tree with to-many relationship', () => { - let relationPrefix; - - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key, path, prefix) => { - relationPrefix = prefix; - const field = key; - return { - from: `${field}-collection`, - field, - postQueryMutation: () => {}, - matchTerm: { [`${prefix}${field}_every`]: { $eq: true } }, - many: true, - }; - }), - }; - const queryTree = queryParser( - { tokenizer, getUID: jest.fn(key => key) }, + { listAdapter, getUID: jest.fn(key => key) }, { name: 'foobar', age: 23, @@ -755,7 +117,7 @@ describe('query parser', () => { expect(queryTree).toMatchObject({ relationships: { posts: { - from: 'posts-collection', + from: 'posts', field: 'posts', matchTerm: { title: { $eq: 'hello' } }, postQueryMutation: expect.any(Function), @@ -765,35 +127,14 @@ describe('query parser', () => { }, }, matchTerm: { - $and: [ - { name: { $eq: 'foobar' } }, - { age: { $eq: 23 } }, - { [`${relationPrefix}posts_every`]: { $eq: true } }, - ], + $and: [{ name: { $eq: 'foobar' } }, { age: { $eq: 23 } }, { posts_posts_every: true }], }, }); }); test('builds a query tree for a relationship with no filters', () => { - let relationPrefix; - - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key, path, prefix) => { - relationPrefix = prefix; - const field = key; - return { - from: `${field}-collection`, - field, - postQueryMutation: () => {}, - matchTerm: { [`${prefix}${field}_every`]: { $eq: true } }, - many: true, - }; - }), - }; - const queryTree = queryParser( - { tokenizer, getUID: jest.fn(key => key) }, + { listAdapter, getUID: jest.fn(key => key) }, { name: 'foobar', age: 23, @@ -804,7 +145,7 @@ describe('query parser', () => { expect(queryTree).toMatchObject({ relationships: { posts: { - from: 'posts-collection', + from: 'posts', field: 'posts', matchTerm: undefined, postQueryMutation: expect.any(Function), @@ -814,35 +155,14 @@ describe('query parser', () => { }, }, matchTerm: { - $and: [ - { name: { $eq: 'foobar' } }, - { age: { $eq: 23 } }, - { [`${relationPrefix}posts_every`]: { $eq: true } }, - ], + $and: [{ name: { $eq: 'foobar' } }, { age: { $eq: 23 } }, { posts_posts_every: true }], }, }); }); test('builds a query tree with to-single relationship', () => { - let relationPrefix; - - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key, path, prefix) => { - relationPrefix = prefix; - const field = key; - return { - from: `${field}-collection`, - field, - postQueryMutation: () => {}, - matchTerm: { [`${prefix}${field}_every`]: { $eq: true } }, - many: false, - }; - }), - }; - const queryTree = queryParser( - { tokenizer, getUID: jest.fn(key => key) }, + { listAdapter, getUID: jest.fn(key => key) }, { name: 'foobar', age: 23, @@ -866,29 +186,15 @@ describe('query parser', () => { $and: [ { name: { $eq: 'foobar' } }, { age: { $eq: 23 } }, - { [`${relationPrefix}company_every`]: { $eq: true } }, + { company_company_every: true }, ], }, }); }); test('builds a query tree with nested relationship', () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key) => { - const [table] = key.split('_'); - return { - from: `${table}-collection`, - field: table, - postQueryMutation: () => {}, - matchTerm: { [key]: { $eq: true } }, - many: true, - }; - }), - }; - const queryTree = queryParser( - { tokenizer, getUID: jest.fn(key => key) }, + { listAdapter, getUID: jest.fn(key => key) }, { name: 'foobar', age: 23, @@ -905,23 +211,25 @@ describe('query parser', () => { expect(queryTree).toMatchObject({ relationships: { posts_every: { - from: 'posts-collection', + from: 'posts', field: 'posts', - matchTerm: { $and: [{ title: { $eq: 'hello' } }, { tags_some: { $eq: true } }] }, + matchTerm: { $and: [{ title: { $eq: 'hello' } }, { tags_some_tags_some: true }] }, postQueryMutation: expect.any(Function), postJoinPipeline: [], many: true, relationships: { tags_some: { - from: 'tags-collection', + from: 'tags', field: 'tags', - matchTerm: { $and: [{ name: { $eq: 'React' } }, { posts_every: { $eq: true } }] }, + matchTerm: { + $and: [{ name: { $eq: 'React' } }, { posts_every_posts_every: true }], + }, postQueryMutation: expect.any(Function), postJoinPipeline: [], many: true, relationships: { posts_every: { - from: 'posts-collection', + from: 'posts', field: 'posts', matchTerm: { title: { $eq: 'foo' } }, postQueryMutation: expect.any(Function), @@ -935,33 +243,23 @@ describe('query parser', () => { }, }, matchTerm: { - $and: [{ name: { $eq: 'foobar' } }, { age: { $eq: 23 } }, { posts_every: { $eq: true } }], + $and: [ + { name: { $eq: 'foobar' } }, + { age: { $eq: 23 } }, + { posts_every_posts_every: true }, + ], }, }); }); test('builds a query tree with nested relationship with nested AND', () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key) => { - const [table] = key.split('_'); - return { - from: `${table}-collection`, - field: table, - postQueryMutation: () => {}, - matchTerm: { $exists: true, $ne: [] }, - many: true, - }; - }), - }; - const queryTree = queryParser( - { tokenizer, getUID: jest.fn(key => key) }, + { listAdapter, getUID: jest.fn(key => key) }, { AND: [ { name: 'foobar' }, { age: 23 }, - { posts_every: { AND: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] } }, + { posts_every: { AND: [{ title: 'hello' }, { tags_some: { name: 'foo' } }] } }, ], } ); @@ -970,15 +268,15 @@ describe('query parser', () => { relationships: { posts_every: { field: 'posts', - from: 'posts-collection', - matchTerm: { $and: [{ title: { $eq: 'hello' } }, { $exists: true, $ne: [] }] }, + from: 'posts', + matchTerm: { $and: [{ title: { $eq: 'hello' } }, { tags_some_tags_some: true }] }, postQueryMutation: expect.any(Function), postJoinPipeline: [], many: true, relationships: { - labels_some: { - field: 'labels', - from: 'labels-collection', + tags_some: { + field: 'tags', + from: 'tags', matchTerm: { name: { $eq: 'foo' } }, postQueryMutation: expect.any(Function), postJoinPipeline: [], @@ -989,33 +287,23 @@ describe('query parser', () => { }, }, matchTerm: { - $and: [{ name: { $eq: 'foobar' } }, { age: { $eq: 23 } }, { $exists: true, $ne: [] }], + $and: [ + { name: { $eq: 'foobar' } }, + { age: { $eq: 23 } }, + { posts_every_posts_every: true }, + ], }, }); }); test('builds a query tree with nested relationship with nested OR', () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key) => { - const [table] = key.split('_'); - return { - from: `${table}-collection`, - field: table, - postQueryMutation: () => {}, - matchTerm: { $exists: true, $ne: [] }, - many: true, - }; - }), - }; - const queryTree = queryParser( - { tokenizer, getUID: jest.fn(key => key) }, + { listAdapter, getUID: jest.fn(key => key) }, { OR: [ { name: 'foobar' }, { age: 23 }, - { posts_every: { OR: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] } }, + { posts_every: { OR: [{ title: 'hello' }, { tags_some: { name: 'foo' } }] } }, ], } ); @@ -1024,15 +312,15 @@ describe('query parser', () => { relationships: { posts_every: { field: 'posts', - from: 'posts-collection', - matchTerm: { $or: [{ title: { $eq: 'hello' } }, { $exists: true, $ne: [] }] }, + from: 'posts', + matchTerm: { $or: [{ title: { $eq: 'hello' } }, { tags_some_tags_some: true }] }, postQueryMutation: expect.any(Function), postJoinPipeline: [], many: true, relationships: { - labels_some: { - field: 'labels', - from: 'labels-collection', + tags_some: { + field: 'tags', + from: 'tags', matchTerm: { name: { $eq: 'foo' } }, postQueryMutation: expect.any(Function), postJoinPipeline: [], @@ -1043,33 +331,23 @@ describe('query parser', () => { }, }, matchTerm: { - $or: [{ name: { $eq: 'foobar' } }, { age: { $eq: 23 } }, { $exists: true, $ne: [] }], + $or: [ + { name: { $eq: 'foobar' } }, + { age: { $eq: 23 } }, + { posts_every_posts_every: true }, + ], }, }); }); test('builds a query tree with nested relationship with nested AND/OR', () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key) => { - const [table] = key.split('_'); - return { - from: `${table}-collection`, - field: table, - postQueryMutation: () => {}, - matchTerm: { $exists: true, $ne: [] }, - many: true, - }; - }), - }; - const queryTree = queryParser( - { tokenizer, getUID: jest.fn(key => key) }, + { listAdapter, getUID: jest.fn(key => key) }, { AND: [ { name: 'foobar' }, { age: 23 }, - { posts_every: { OR: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] } }, + { posts_every: { OR: [{ title: 'hello' }, { tags_some: { name: 'foo' } }] } }, ], } ); @@ -1078,15 +356,15 @@ describe('query parser', () => { relationships: { posts_every: { field: 'posts', - from: 'posts-collection', - matchTerm: { $or: [{ title: { $eq: 'hello' } }, { $exists: true, $ne: [] }] }, + from: 'posts', + matchTerm: { $or: [{ title: { $eq: 'hello' } }, { tags_some_tags_some: true }] }, postQueryMutation: expect.any(Function), postJoinPipeline: [], many: true, relationships: { - labels_some: { - field: 'labels', - from: 'labels-collection', + tags_some: { + field: 'tags', + from: 'tags', matchTerm: { name: { $eq: 'foo' } }, postQueryMutation: expect.any(Function), postJoinPipeline: [], @@ -1097,33 +375,23 @@ describe('query parser', () => { }, }, matchTerm: { - $and: [{ name: { $eq: 'foobar' } }, { age: { $eq: 23 } }, { $exists: true, $ne: [] }], + $and: [ + { name: { $eq: 'foobar' } }, + { age: { $eq: 23 } }, + { posts_every_posts_every: true }, + ], }, }); }); test('builds a query tree with nested relationship with nested OR/AND', () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key) => { - const [table] = key.split('_'); - return { - from: `${table}-collection`, - field: table, - postQueryMutation: () => {}, - matchTerm: { $exists: true, $ne: [] }, - many: true, - }; - }), - }; - const queryTree = queryParser( - { tokenizer, getUID: jest.fn(key => key) }, + { listAdapter, getUID: jest.fn(key => key) }, { OR: [ { name: 'foobar' }, { age: 23 }, - { posts_every: { AND: [{ title: 'hello' }, { labels_some: { name: 'foo' } }] } }, + { posts_every: { AND: [{ title: 'hello' }, { tags_some: { name: 'foo' } }] } }, ], } ); @@ -1132,15 +400,15 @@ describe('query parser', () => { relationships: { posts_every: { field: 'posts', - from: 'posts-collection', - matchTerm: { $and: [{ title: { $eq: 'hello' } }, { $exists: true, $ne: [] }] }, + from: 'posts', + matchTerm: { $and: [{ title: { $eq: 'hello' } }, { tags_some_tags_some: true }] }, postJoinPipeline: [], postQueryMutation: expect.any(Function), many: true, relationships: { - labels_some: { - field: 'labels', - from: 'labels-collection', + tags_some: { + field: 'tags', + from: 'tags', matchTerm: { name: { $eq: 'foo' } }, postJoinPipeline: [], postQueryMutation: expect.any(Function), @@ -1151,28 +419,18 @@ describe('query parser', () => { }, }, matchTerm: { - $or: [{ name: { $eq: 'foobar' } }, { age: { $eq: 23 } }, { $exists: true, $ne: [] }], + $or: [ + { name: { $eq: 'foobar' } }, + { age: { $eq: 23 } }, + { posts_every_posts_every: true }, + ], }, }); }); test('builds a query tree with nested relationship with parallel OR/AND', () => { - const tokenizer = { - simple: jest.fn((query, key) => ({ matchTerm: { [key]: { $eq: query[key] } } })), - relationship: jest.fn((query, key) => { - const [table] = key.split('_'); - return { - from: `${table}-collection`, - field: table, - postQueryMutation: () => {}, - matchTerm: { $exists: true, $ne: [] }, - many: true, - }; - }), - }; - const queryTree = queryParser( - { tokenizer, getUID: jest.fn(key => key) }, + { listAdapter, getUID: jest.fn(key => key) }, { OR: [{ name: 'foobar' }, { age: 23 }], AND: [{ age: 30 }, { email: 'foo@bar.com' }], diff --git a/packages/mongo-join-builder/tests/utils.js b/packages/mongo-join-builder/tests/utils.js new file mode 100644 index 00000000000..eb8cf8ad555 --- /dev/null +++ b/packages/mongo-join-builder/tests/utils.js @@ -0,0 +1,104 @@ +const findFieldAdapterForQuerySegment = ({ fieldAdapters }) => segment => + fieldAdapters.find(({ path }) => path === segment || path === segment.split('_')[0]); + +const tagsAdapter = { + key: 'tags', + model: { collection: { name: 'tags' } }, + fieldAdapters: [ + { + dbPath: 'name', + getQueryConditions: dbPath => ({ [dbPath]: val => ({ [dbPath]: { $eq: val } }) }), + }, + ], + graphQlQueryPathToMongoField: orderField => orderField, +}; + +const postsAdapter = { + key: 'posts', + model: { collection: { name: 'posts' } }, + fieldAdapters: [ + { + dbPath: 'title', + getQueryConditions: dbPath => ({ [dbPath]: val => ({ [dbPath]: { $eq: val } }) }), + }, + { + dbPath: 'status', + getQueryConditions: dbPath => ({ [dbPath]: val => ({ [dbPath]: { $eq: val } }) }), + }, + { + path: 'tags', + field: { many: true }, + getQueryConditions: () => {}, + getRefListAdapter: () => tagsAdapter, + }, + ], + graphQlQueryPathToMongoField: orderField => orderField, +}; + +const listAdapter = { + key: 'users', + model: { collection: { name: 'users' } }, + fieldAdapters: [ + { + dbPath: 'name', + getQueryConditions: dbPath => ({ [dbPath]: val => ({ [dbPath]: { $eq: val } }) }), + }, + { + dbPath: 'age', + getQueryConditions: dbPath => ({ [dbPath]: val => ({ [dbPath]: { $eq: val } }) }), + }, + { + dbPath: 'address', + getQueryConditions: dbPath => ({ [dbPath]: val => ({ [dbPath]: { $eq: val } }) }), + }, + { + dbPath: 'email', + getQueryConditions: dbPath => ({ [dbPath]: val => ({ [dbPath]: { $eq: val } }) }), + }, + { + dbPath: 'type', + getQueryConditions: dbPath => ({ [dbPath]: val => ({ [dbPath]: { $eq: val } }) }), + }, + { + path: 'company', + field: { many: false }, + getQueryConditions: () => {}, + getRefListAdapter: () => ({ + model: { collection: { name: 'company-collection' } }, + fieldAdapters: [ + { + dbPath: 'name', + getQueryConditions: dbPath => ({ [dbPath]: val => ({ [dbPath]: { $eq: val } }) }), + }, + ], + }), + }, + ], +}; + +listAdapter.fieldAdapters.push({ + getQueryConditions: () => {}, + path: 'posts', + field: { many: true }, + getRefListAdapter: () => postsAdapter, +}); + +tagsAdapter.fieldAdapters.push({ + path: 'posts', + field: { many: true }, + getQueryConditions: () => {}, + getRefListAdapter: () => postsAdapter, +}); + +postsAdapter.fieldAdapters.push({ + getQueryConditions: () => {}, + path: 'author', + field: { many: false }, + getRefListAdapter: () => listAdapter, +}); + +postsAdapter.findFieldAdapterForQuerySegment = findFieldAdapterForQuerySegment(postsAdapter); +tagsAdapter.findFieldAdapterForQuerySegment = findFieldAdapterForQuerySegment(tagsAdapter); +listAdapter.findFieldAdapterForQuerySegment = findFieldAdapterForQuerySegment(listAdapter); + +module.exports = { tagsAdapter, postsAdapter, listAdapter };