From f9e691fa61ef0afd2f4e4daddf3adee5737fa35e Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Thu, 11 Jul 2024 19:18:03 +0100 Subject: [PATCH] add logic for meta_data and attributes selectors --- package.json | 1 + src/query-state.ts | 123 ++++---- src/utils.ts | 39 +++ tests/helpers/db.ts | 15 +- tests/query-state.test.ts | 580 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 672 insertions(+), 86 deletions(-) diff --git a/package.json b/package.json index 364b9be..b005e53 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@babel/preset-typescript": "^7.24.6", "@testing-library/react": "^15.0.7", "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.6", "babel-jest": "^29.7.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", diff --git a/src/query-state.ts b/src/query-state.ts index b2d8b18..e557a42 100644 --- a/src/query-state.ts +++ b/src/query-state.ts @@ -3,6 +3,7 @@ import cloneDeep from 'lodash/cloneDeep'; import debounce from 'lodash/debounce'; import forEach from 'lodash/forEach'; import get from 'lodash/get'; +import find from 'lodash/find'; import isEmpty from 'lodash/isEmpty'; import isEqual from 'lodash/isEqual'; import { ObservableResource } from 'observable-hooks'; @@ -19,6 +20,7 @@ import { map, switchMap, distinctUntilChanged, debounceTime, tap, startWith } fr import { SubscribableBase } from './subscribable-base'; import { Search } from './search-state'; +import { normalizeWhereClauses } from './utils'; import type { RxCollection, RxDocument } from 'rxdb'; @@ -289,18 +291,8 @@ export class Query extends SubscribableBase { * Selector helpers */ where(field: string, value: any): this { - if (value === undefined || value === null) { - // Remove the clause if value is null or undefined - // @TODO - what about empty string, array or object? - this.whereClauses = this.whereClauses.filter((clause) => clause.field !== field); - } else { - const existingClause = this.whereClauses.find((clause) => clause.field === field); - if (existingClause) { - existingClause.value = value; - } else { - this.whereClauses.push({ field, value }); - } - } + this.whereClauses.push({ field, value }); + this.whereClauses = normalizeWhereClauses(this.whereClauses); this.updateParams(); return this; } @@ -337,61 +329,29 @@ export class Query extends SubscribableBase { debouncedSearch = debounce(this.search, 250); /** - * Handle attribute selection - * Attributes queries have the form: - * { - * selector: { - * attributes: { - * $allMatch: [ - * { - * name: 'Color', - * option: 'Blue', - * }, - * ], - * }, - * } * - * Note: $allMatch is an array so we need to check if it exists and add/remove to it */ - updateVariationAttributeSelector(attribute: { id: number; name: string; option: string }) { - // this is only a helper for variations - if (this.collection.name !== 'variations') { - throw new Error('updateVariationAttributeSearch is only for variations'); - } - - // add attribute to query - const $allMatch = get(this.getParams(), ['selector', 'attributes', '$allMatch'], []); - const index = $allMatch.findIndex((a) => a.name === attribute.name); - if (index > -1) { - $allMatch[index] = attribute; + private updateParams(additionalParams: Partial = {}): void { + let selector; + + // Construct the $and selector from where clauses + const andClauses = this.whereClauses.map((clause) => ({ + [clause.field]: clause.value, + })); + + if (andClauses.length > 0) { + selector = { $and: andClauses }; } else { - $allMatch.push(attribute); - } - - this.whereClauses.push({ field: 'attributes', value: { $allMatch: [...$allMatch] } }); - this.updateParams(); - } - - resetVariationAttributeSelector() { - if (get(this.getParams(), ['selector', 'attributes'])) { - this.whereClauses = this.whereClauses.filter((clause) => clause.field !== 'attributes'); - this.updateParams(); + selector = {}; } - } - - /** - * - */ - private updateParams(additionalParams: Partial = {}): void { - // Construct the selector from where clauses - const selector = this.whereClauses.reduce((acc, clause) => { - acc[clause.field] = clause.value; - return acc; - }, {}); // Get current params and merge them with additionalParams const currentParams = this.getParams() || {}; - const newParams: QueryParams = { ...currentParams, ...additionalParams, selector }; + const newParams: QueryParams = { + ...currentParams, + ...additionalParams, + selector + }; // Update the BehaviorSubject this.subjects.params.next(newParams); @@ -410,4 +370,47 @@ export class Query extends SubscribableBase { distinctUntilChanged() ); } + + /** + * Helper methods to see if $elemMatch is active + */ + findMetaDataSelector(key: string): any { + for (const clause of this.whereClauses) { + if (clause.field === 'meta_data' && clause.value?.$elemMatch) { + const match = find(clause.value.$elemMatch.$and || [clause.value.$elemMatch], { key }); + if (match) return match.value; + } + } + return undefined; + } + + hasMetaDataSelector(key: string, value: any): boolean { + for (const clause of this.whereClauses) { + if (clause.field === 'meta_data' && clause.value?.$elemMatch) { + const match = find(clause.value.$elemMatch.$and || [clause.value.$elemMatch], { key, value }); + if (match) return true; + } + } + return false; + } + + findAttributesSelector(name: string): any { + for (const clause of this.whereClauses) { + if (clause.field === 'attributes' && clause.value?.$elemMatch) { + const match = find(clause.value.$elemMatch.$and || [clause.value.$elemMatch], { name }); + if (match) return match.option; + } + } + return undefined; + } + + hasAttributesSelector(name: string, option: any): boolean { + for (const clause of this.whereClauses) { + if (clause.field === 'attributes' && clause.value?.$elemMatch) { + const match = find(clause.value.$elemMatch.$and || [clause.value.$elemMatch], { name, option }); + if (match) return true; + } + } + return false; + } } diff --git a/src/utils.ts b/src/utils.ts index 5dc603e..84552d4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -113,3 +113,42 @@ export function getParamValueFromEndpoint(endpoint: string, param: string) { const params = new URLSearchParams(url.search); return params.get(param); } + +/** + * + */ +type WhereClause = { field: string; value: any }; +export function normalizeWhereClauses(clauses: WhereClause[]): WhereClause[] { + const fieldMap = new Map(); + const fieldsToRemove = new Set(); + + for (const clause of clauses) { + if (clause.value === null) { + // Mark the field for removal + fieldsToRemove.add(clause.field); + fieldMap.delete(clause.field); + } else if (clause.value?.$elemMatch) { + const key = `${clause.field}_${clause.value.$elemMatch.key || clause.value.$elemMatch.name}`; + if (clause.value.$elemMatch.value === null || clause.value.$elemMatch.option === null) { + fieldMap.delete(key); + } else { + fieldMap.set(key, clause); + } + } else if (!fieldsToRemove.has(clause.field)) { + fieldMap.set(clause.field, clause); + } + } + + // Ensure fields marked for removal are not in the final output + fieldsToRemove.forEach(field => { + for (const key of fieldMap.keys()) { + if (key.startsWith(field)) { + fieldMap.delete(key); + } + } + }); + + return Array.from(fieldMap.values()); +} + + diff --git a/tests/helpers/db.ts b/tests/helpers/db.ts index 17e10fd..1a38e93 100644 --- a/tests/helpers/db.ts +++ b/tests/helpers/db.ts @@ -72,14 +72,21 @@ export type LogDocument = RxDocument; export type LogCollection = RxCollection; const logs: RxCollectionCreator = { schema: logSchema }; +/** + * Generate a unique database name + */ +function generateUniqueDbName(baseName: string): string { + return `${baseName}_${Date.now()}`; +} + /** * */ export async function createStoreDatabase(): Promise { const db = await createRxDatabase({ - name: 'storedb', + name: generateUniqueDbName('storedb'), storage: getRxStorageMemory(), - ignoreDuplicate: true, + // ignoreDuplicate: true, allowSlowCount: true, }); @@ -93,9 +100,9 @@ export async function createStoreDatabase(): Promise { */ export async function createSyncDatabase(): Promise { const db = await createRxDatabase({ - name: 'syncdb', + name: generateUniqueDbName('syncdb'), storage: getRxStorageMemory(), - ignoreDuplicate: true, + // ignoreDuplicate: true, allowSlowCount: true, }); diff --git a/tests/query-state.test.ts b/tests/query-state.test.ts index 12edcf4..4a0176c 100644 --- a/tests/query-state.test.ts +++ b/tests/query-state.test.ts @@ -1,4 +1,5 @@ import { of, BehaviorSubject } from 'rxjs'; +import { skip } from 'rxjs/operators'; import { Query, QueryParams } from '../src/query-state'; // Adjust the import based on your file structure import { createStoreDatabase, createSyncDatabase } from './helpers/db'; @@ -23,13 +24,14 @@ describe('Query', () => { await new Promise(resolve => setTimeout(resolve, 100)); await storeDatabase.destroy(); + expect(storeDatabase.destroyed).toBe(true); jest.clearAllMocks(); }); /** * */ - it('query.getParams() should return the initialParams', () => { + it('query.getParams() should return the initialParams', async () => { const initialParams = { sortBy: 'name', sortDirection: 'asc', @@ -71,8 +73,8 @@ describe('Query', () => { { uuid: '4', name: 'Item 4', price: '-9.50' }, { uuid: '5', name: 'Item 5', price: '4.00' }, ]; - - await storeDatabase.collections.products.bulkInsert(data); + const { success } = await storeDatabase.collections.products.bulkInsert(data); + expect(success.length).toBe(5); const query = new Query({ collection: storeDatabase.collections.products, initialParams: {} }); @@ -99,6 +101,7 @@ describe('Query', () => { * */ describe('query.sort()', () => { + /** * */ @@ -120,7 +123,8 @@ describe('Query', () => { { uuid: '4', name: 'Item 4', price: '-9.50' }, { uuid: '5', name: 'Item 5', price: '4.00' }, ]; - await storeDatabase.collections.products.bulkInsert(data); + const { success } = await storeDatabase.collections.products.bulkInsert(data); + expect(success.length).toBe(5); const query = new Query({ collection: storeDatabase.collections.products, initialParams }); @@ -175,49 +179,581 @@ describe('Query', () => { /** * */ - it('correctly sets a single where clause', () => { - const query = new Query({ collection: storeDatabase.collections.products, initialParams: {} }); - query.where('status', 'completed'); + it('sets a single where clause', async () => { + const data = [ + { uuid: '1', name: 'Item 1', stock_status: 'outofstock' }, + { uuid: '2', name: 'Item 2', stock_status: 'instock' }, + { uuid: '3', name: 'Item 3', stock_status: 'onbackorder' }, + { uuid: '4', name: 'Item 4', stock_status: 'instock' }, + { uuid: '5', name: 'Item 5', stock_status: 'lowstock' }, + ]; + + const { success } = await storeDatabase.collections.products.bulkInsert(data); + expect(success.length).toBe(5); + + const query = new Query({ + collection: storeDatabase.collections.products, + initialParams: { + sortBy: 'name', + sortDirection: 'asc', + } + }); + + query.where('stock_status', 'instock'); expect(query.getParams()).toEqual({ - selector: { status: 'completed' }, + selector: { $and: [ { stock_status: 'instock' } ] }, + sortBy: 'name', + sortDirection: 'asc', + }); + + return new Promise((resolve) => { + query.result$.subscribe((result) => { + expect(result).toEqual(expect.objectContaining({ + elapsed: expect.any(Number), + searchActive: false, + count: 2, + hits: expect.arrayContaining([ + expect.objectContaining({ id: '2', document: expect.any(Object) }), + expect.objectContaining({ id: '4', document: expect.any(Object) }) + ]) + })); + + resolve(); + }); }); }); /** * */ - it('overwrites an existing where clause', () => { - const query = new Query({ collection: storeDatabase.collections.products, initialParams: {} }); - query.where('status', 'completed'); - query.where('status', 'pending'); + it('overwrites an existing where clause', async () => { + const data = [ + { uuid: '1', name: 'Item 1', stock_status: 'outofstock' }, + { uuid: '2', name: 'Item 2', stock_status: 'instock' }, + { uuid: '3', name: 'Item 3', stock_status: 'onbackorder' }, + { uuid: '4', name: 'Item 4', stock_status: 'instock' }, + { uuid: '5', name: 'Item 5', stock_status: 'lowstock' }, + ]; + + const { success } = await storeDatabase.collections.products.bulkInsert(data); + expect(success.length).toBe(5); + + const query = new Query({ + collection: storeDatabase.collections.products, + initialParams: { + sortBy: 'name', + sortDirection: 'asc', + selector: { + stock_status: 'instock' + } + } + }); + + query.where('stock_status', 'outofstock'); expect(query.getParams()).toEqual({ - selector: { status: 'pending' }, + selector: { $and: [ { stock_status: 'outofstock' } ] }, + sortBy: 'name', + sortDirection: 'asc', + }); + + return new Promise((resolve) => { + query.result$.subscribe((result) => { + expect(result).toEqual(expect.objectContaining({ + elapsed: expect.any(Number), + searchActive: false, + count: 1, + hits: expect.arrayContaining([ + expect.objectContaining({ id: '1', document: expect.any(Object) }), + ]) + })); + + resolve(); + }); }); }); /** * */ - it('removes a where clause when value is null', () => { - const query = new Query({ collection: storeDatabase.collections.products, initialParams: {} }); - query.where('status', 'completed'); - query.where('status', null); + it('removes a where clause when value is null', async () => { + const data = [ + { uuid: '1', name: 'Item 1', stock_status: 'outofstock' }, + { uuid: '2', name: 'Item 2', stock_status: 'instock' }, + { uuid: '3', name: 'Item 3', stock_status: 'onbackorder' }, + { uuid: '4', name: 'Item 4', stock_status: 'instock' }, + { uuid: '5', name: 'Item 5', stock_status: 'lowstock' }, + ]; + + const { success } = await storeDatabase.collections.products.bulkInsert(data); + expect(success.length).toBe(5); + + const query = new Query({ + collection: storeDatabase.collections.products, + initialParams: { + sortBy: 'name', + sortDirection: 'asc', + selector: { + stock_status: 'instock' + } + } + }); + + let count = 0; + + // next sort + const promise = new Promise((resolve) => { + query.result$.subscribe((result) => { + if(count === 0) { + count++; + expect(result).toEqual(expect.objectContaining({ + elapsed: expect.any(Number), + searchActive: false, + count: 2, + hits: expect.arrayContaining([ + expect.objectContaining({ id: '2', document: expect.any(Object) }), + expect.objectContaining({ id: '4', document: expect.any(Object) }) + ]) + })); + + // remove selector + query.where('stock_status', null); + } else { + expect(result).toEqual(expect.objectContaining({ + elapsed: expect.any(Number), + searchActive: false, + count: 5, + hits: expect.arrayContaining([ + expect.objectContaining({ id: '1', document: expect.any(Object) }), + expect.objectContaining({ id: '2', document: expect.any(Object) }), + expect.objectContaining({ id: '3', document: expect.any(Object) }), + expect.objectContaining({ id: '4', document: expect.any(Object) }), + expect.objectContaining({ id: '5', document: expect.any(Object) }) + ]) + })); + resolve(); + } + }) + }); + + return promise; + }); + + /** + * + */ + it('finds a meta data element', async () => { + const data = [ + { uuid: '1', name: 'Item 1', meta_data: [{ key: 'key', value: 'value' }, { key: '_pos_store', value: '64' }] }, + { uuid: '2', name: 'Item 2', meta_data: [{ key: 'key', value: 'value' }] }, + { uuid: '3', name: 'Item 3', meta_data: [{ key: 'key', value: 'value' }, { key: '_pos_store', value: '40' }] }, + { uuid: '4', name: 'Item 4', meta_data: [{ key: 'key', value: 'value' }] }, + { uuid: '5', name: 'Item 5', meta_data: [{ key: 'key', value: 'value' }, { key: '_pos_store', value: '64' }] }, + ]; + + const { success } = await storeDatabase.collections.products.bulkInsert(data); + expect(success.length).toBe(5); + + const query = new Query({ + collection: storeDatabase.collections.products, + initialParams: { + sortBy: 'name', + sortDirection: 'asc', + } + }); + + query.where('meta_data', { $elemMatch: { key: '_pos_store', value: '64' } }); + expect(query.getParams()).toEqual({ + selector: { $and: [ { meta_data: { $elemMatch: { key: '_pos_store', value: '64' } } } ] }, + sortBy: 'name', + sortDirection: 'asc', + }); + + return new Promise((resolve) => { + query.result$.subscribe((result) => { + expect(result).toEqual(expect.objectContaining({ + elapsed: expect.any(Number), + searchActive: false, + count: 2, + hits: expect.arrayContaining([ + expect.objectContaining({ id: '1', document: expect.any(Object) }), + expect.objectContaining({ id: '5', document: expect.any(Object) }) + ]) + })); + + resolve(); + }); + }); + }); + + /** + * + */ + it('finds multiple meta data elements', async () => { + const data = [ + { uuid: '1', name: 'Item 1', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '5' }, + { key: '_pos_store', value: '64' } + ] + }, + { uuid: '2', name: 'Item 2', meta_data: + [ + ] + }, + { uuid: '3', name: 'Item 3', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '3' }, + { key: '_pos_store', value: '40' } + ] + }, + { uuid: '4', name: 'Item 4', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_store', value: '40' } + ] + }, + { uuid: '5', name: 'Item 5', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '3' }, + { key: '_pos_store', value: '64' } + ] + }, + ]; + + const { success } = await storeDatabase.collections.products.bulkInsert(data); + expect(success.length).toBe(5); + + const query = new Query({ + collection: storeDatabase.collections.products, + initialParams: { + sortBy: 'name', + sortDirection: 'asc', + selector: { + meta_data: { $elemMatch: { key: '_pos_user', value: '3' } } + } + } + }); + + query.where('meta_data', { $elemMatch: { key: '_pos_store', value: '64' } }); + expect(query.getParams()).toEqual({ + selector: { + $and: + [ + { meta_data: { $elemMatch: { key: '_pos_user', value: '3' } } }, + { meta_data: { $elemMatch: { key: '_pos_store', value: '64' } } } + ] + }, + sortBy: 'name', + sortDirection: 'asc', + }); + + return new Promise((resolve) => { + query.result$.subscribe((result) => { + expect(result).toEqual(expect.objectContaining({ + elapsed: expect.any(Number), + searchActive: false, + count: 1, + hits: expect.arrayContaining([ + expect.objectContaining({ id: '5', document: expect.any(Object) }) + ]) + })); + + resolve(); + }); + }); + }); + + /** + * + */ + it('overwrites an existing meta data selector', async () => { + const data = [ + { uuid: '1', name: 'Item 1', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '5' }, + { key: '_pos_store', value: '64' } + ] + }, + { uuid: '2', name: 'Item 2', meta_data: + [ + ] + }, + { uuid: '3', name: 'Item 3', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '3' }, + { key: '_pos_store', value: '40' } + ] + }, + { uuid: '4', name: 'Item 4', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_store', value: '40' } + ] + }, + { uuid: '5', name: 'Item 5', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '3' }, + { key: '_pos_store', value: '64' } + ] + }, + ]; + + const { success } = await storeDatabase.collections.products.bulkInsert(data); + expect(success.length).toBe(5); + + const query = new Query({ + collection: storeDatabase.collections.products, + initialParams: { + sortBy: 'name', + sortDirection: 'asc', + selector: { + meta_data: { $elemMatch: { key: '_pos_user', value: '3' } } + } + } + }); + + query.where('meta_data', { $elemMatch: { key: '_pos_user', value: '5' } }); + expect(query.getParams()).toEqual({ + selector: { + $and: + [ + { meta_data: { $elemMatch: { key: '_pos_user', value: '5' } } }, + ] + }, + sortBy: 'name', + sortDirection: 'asc', + }); + + return new Promise((resolve) => { + query.result$.subscribe((result) => { + expect(result).toEqual(expect.objectContaining({ + elapsed: expect.any(Number), + searchActive: false, + count: 1, + hits: expect.arrayContaining([ + expect.objectContaining({ id: '1', document: expect.any(Object) }) + ]) + })); + + resolve(); + }); + }); + }); + + /** + * + */ + it('removes a single meta data selector', async () => { + const data = [ + { uuid: '1', name: 'Item 1', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '5' }, + { key: '_pos_store', value: '64' } + ] + }, + { uuid: '2', name: 'Item 2', meta_data: + [ + ] + }, + { uuid: '3', name: 'Item 3', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '3' }, + { key: '_pos_store', value: '40' } + ] + }, + { uuid: '4', name: 'Item 4', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_store', value: '40' } + ] + }, + { uuid: '5', name: 'Item 5', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '3' }, + { key: '_pos_store', value: '64' } + ] + }, + ]; + + const { success } = await storeDatabase.collections.products.bulkInsert(data); + expect(success.length).toBe(5); + + const query = new Query({ + collection: storeDatabase.collections.products, + initialParams: { + sortBy: 'name', + sortDirection: 'asc', + selector: { + meta_data: { $elemMatch: { key: '_pos_user', value: '3' } }, + meta_data: { $elemMatch: { key: '_pos_store', value: '64' } }, + } + } + }); + + query.where('meta_data', { $elemMatch: { key: '_pos_user', value: null } }); + expect(query.getParams()).toEqual({ + selector: { + $and: + [ + { meta_data: { $elemMatch: { key: '_pos_store', value: '64' } } }, + ] + }, + sortBy: 'name', + sortDirection: 'asc', + }); + + return new Promise((resolve) => { + query.result$.subscribe((result) => { + expect(result).toEqual(expect.objectContaining({ + elapsed: expect.any(Number), + searchActive: false, + count: 2, + hits: expect.arrayContaining([ + expect.objectContaining({ id: '1', document: expect.any(Object) }), + expect.objectContaining({ id: '5', document: expect.any(Object) }) + ]) + })); + + resolve(); + }); + }); + }); + + /** + * + */ + it('removes all meta data selectors', async () => { + const data = [ + { uuid: '1', name: 'Item 1', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '5' }, + { key: '_pos_store', value: '64' } + ] + }, + { uuid: '2', name: 'Item 2', meta_data: + [ + ] + }, + { uuid: '3', name: 'Item 3', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '3' }, + { key: '_pos_store', value: '40' } + ] + }, + { uuid: '4', name: 'Item 4', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_store', value: '40' } + ] + }, + { uuid: '5', name: 'Item 5', meta_data: + [ + { key: 'key', value: 'value' }, + { key: '_pos_user', value: '3' }, + { key: '_pos_store', value: '64' } + ] + }, + ]; + + const { success } = await storeDatabase.collections.products.bulkInsert(data); + expect(success.length).toBe(5); + + const query = new Query({ + collection: storeDatabase.collections.products, + initialParams: { + sortBy: 'name', + sortDirection: 'asc', + selector: { + meta_data: { $elemMatch: { key: '_pos_user', value: '3' } }, + meta_data: { $elemMatch: { key: '_pos_store', value: '64' } }, + } + } + }); + + query.where('meta_data', null); expect(query.getParams()).toEqual({ selector: {}, + sortBy: 'name', + sortDirection: 'asc', + }); + + return new Promise((resolve) => { + query.result$.subscribe((result) => { + expect(result).toEqual(expect.objectContaining({ + elapsed: expect.any(Number), + searchActive: false, + count: 5, + hits: expect.arrayContaining([ + expect.objectContaining({ id: '1', document: expect.any(Object) }), + expect.objectContaining({ id: '2', document: expect.any(Object) }), + expect.objectContaining({ id: '3', document: expect.any(Object) }), + expect.objectContaining({ id: '4', document: expect.any(Object) }), + expect.objectContaining({ id: '5', document: expect.any(Object) }) + ]) + })); + + resolve(); + }); }); }); /** * */ - it('combines where and sort clauses correctly', () => { - const query = new Query({ collection: storeDatabase.collections.products, initialParams: {} }); - query.where('status', 'completed').sort('price', 'asc'); + it('finds an attributes element', async () => { + const data = [ + { uuid: '1', name: 'Item 1', attributes: [{ name: 'name', option: 'option' }, { name: 'Color', option: 'Blue' }] }, + { uuid: '2', name: 'Item 2', attributes: [{ name: 'name', option: 'option' }] }, + { uuid: '3', name: 'Item 3', attributes: [{ name: 'name', option: 'option' }, { name: 'Color', option: 'Red' }] }, + { uuid: '4', name: 'Item 4', attributes: [{ name: 'name', option: 'option' }] }, + { uuid: '5', name: 'Item 5', attributes: [{ name: 'name', option: 'option' }, { name: 'Color', option: 'Blue' }] }, + ]; + + const { success } = await storeDatabase.collections.variations.bulkInsert(data); + expect(success.length).toBe(5); + + const query = new Query({ + collection: storeDatabase.collections.variations, + initialParams: { + sortBy: 'name', + sortDirection: 'asc', + } + }); + + query.where('attributes', { $elemMatch: { name: 'Color', option: 'Blue' } }); expect(query.getParams()).toEqual({ - selector: { status: 'completed' }, - sortBy: 'price', + selector: { $and: [ { attributes: { $elemMatch: { name: 'Color', option: 'Blue' } } } ] }, + sortBy: 'name', sortDirection: 'asc', }); + + return new Promise((resolve) => { + query.result$.subscribe((result) => { + expect(result).toEqual(expect.objectContaining({ + elapsed: expect.any(Number), + searchActive: false, + count: 2, + hits: expect.arrayContaining([ + expect.objectContaining({ id: '1', document: expect.any(Object) }), + expect.objectContaining({ id: '5', document: expect.any(Object) }) + ]) + })); + + resolve(); + }); + }); }); + }); });