From e306f51c28c2ee307b5e9fe2184f32e31083d5c0 Mon Sep 17 00:00:00 2001 From: Mike Cornwell Date: Mon, 12 Jan 2026 11:44:39 -0500 Subject: [PATCH 1/2] fix(ors): fix or logic --- features/search.feature | 8 ++ features/step_definitions/steps.ts | 134 ++++++++++++++++++++++++++++- package.json | 2 +- src/lib.ts | 16 +++- test/src/lib.test.ts | 89 +++++++++++++++++++ 5 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 features/search.feature diff --git a/features/search.feature b/features/search.feature new file mode 100644 index 0000000..82526b2 --- /dev/null +++ b/features/search.feature @@ -0,0 +1,8 @@ +Feature: Search Features + + Scenario: A property search with ors + Given an orm is setup + Given ModelList1 is created and inserted into the database + When search named OrPropertySearch is executed on model named ModelA + Then 3 instances are found + diff --git a/features/step_definitions/steps.ts b/features/step_definitions/steps.ts index c2bdf42..842f228 100644 --- a/features/step_definitions/steps.ts +++ b/features/step_definitions/steps.ts @@ -1,7 +1,19 @@ import { assert } from 'chai' import { Given, When, Then } from '@cucumber/cucumber' import { datastoreAdapter } from '../../src' -import { createOrm, Orm, PrimaryKeyUuidProperty, queryBuilder, TextProperty } from 'functional-models' +import { + createOrm, + DatetimeProperty, + IntegerProperty, + Model, + ModelType, + ModelWithReferencesConstructorProps, + Orm, + OrmModelInstance, + PrimaryKeyUuidProperty, + queryBuilder, + TextProperty, +} from 'functional-models' const SeedData = { SeedData1: () => { @@ -60,6 +72,91 @@ const Models = { } } +const MODELS: Record< + string, + (props: ModelWithReferencesConstructorProps) => { + models: readonly ModelType[] + instances: readonly OrmModelInstance[] + } +> = { + ModelList1: ({ Model, fetcher }) => { + const ModelA = Model({ + pluralName: 'ModelA', + namespace: 'functional-models-orm-memory', + properties: { + id: PrimaryKeyUuidProperty(), + name: TextProperty({ required: true }), + age: IntegerProperty({ required: true }), + datetime: DatetimeProperty(), + }, + }) + + return { + models: [ModelA] as ModelType[], + instances: [ + ModelA.create({ + id: 'edf73dba-216a-4e10-a38f-398a4b38350a', + name: 'name-2', + age: 2, + }), + ModelA.create({ + id: '2c3e6547-2d6b-44c3-ad2c-1220a3d305be', + name: 'name-3', + age: 10, + datetime: new Date('2020-02-01T00:00:00.000Z'), + }), + ModelA.create({ + id: 'ed1dc8ff-fdc5-401c-a229-8566a418ceb5', + name: 'name-1', + age: 1, + datetime: new Date('2020-01-01T00:00:00.000Z'), + }), + ModelA.create({ + name: 'name-4', + age: 15, + datetime: new Date('2020-03-01T00:00:00.000Z'), + }), + ModelA.create({ + name: 'name-5', + age: 20, + }), + ModelA.create({ + name: 'name-7', + age: 20, + }), + ModelA.create({ + name: 'name-6', + age: 20, + }), + ModelA.create({ + name: 'name-9', + age: 30, + }), + ModelA.create({ + name: 'name-10', + age: 100, + datetime: new Date('2020-05-01T00:00:00.000Z'), + }), + ModelA.create({ + name: 'name-8', + age: 50, + }), + ] as OrmModelInstance[], + } + }, +} + +const SEARCHES = { + OrPropertySearch: () => + queryBuilder() + .property('name', 'name-8') + .or() + .property('name', 'name-1') + .or() + .property('name', 'name-10') + .compile(), +} + Given('a datastore using seed data {word} is created', function(key: string) { const seedData = SeedData[key]() this.datastoreAdapter = datastoreAdapter.create({seedData}) @@ -94,3 +191,38 @@ Then('the result matches {word}', function(dataKey: string) { const actual = this.result assert.deepEqual(actual, expected) }) + +Given('an orm is setup', function () { + this.datastoreAdapter = datastoreAdapter.create() + this.orm = createOrm({ datastoreAdapter: this.datastoreAdapter }) +}) + +Given( + '{word} is created and inserted into the database', + async function (key: string) { + const result = MODELS[key](this.orm) + this.models = result.models + // @ts-ignore + await result.instances.reduce(async (accP, i) => { + await accP + return i.save() + }, Promise.resolve()) + } +) + +When( + 'search named {word} is executed on model named {word}', + async function (key: string, modelPluralName: string) { + const search = SEARCHES[key]() + const model = this.models.find( + x => x.getModelDefinition().pluralName === modelPluralName + ) + this.result = await model.search(search) + } +) + +Then(/^(\d+) instances are found$/, function (count: number) { + const actual = this.result.instances.length + const expected = count + assert.equal(actual, expected) +}) diff --git a/package.json b/package.json index d06f3db..a0e656c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "functional-models-orm-memory", - "version": "3.0.4", + "version": "3.0.5", "description": "An in-memory datastore adapter for functional-models", "main": "index.js", "types": "index.d.ts", diff --git a/src/lib.ts b/src/lib.ts index 6c43f41..024c691 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -161,6 +161,11 @@ const _allCheck = (listOfChecks: any[]) => (obj: object) => { return x(obj) }) } +const _anyCheck = (listOfChecks: any[]) => (obj: object) => { + return listOfChecks.some(x => { + return x(obj) + }) +} const _buildChecks = (o: QueryTokens): ((obj: object) => boolean) => { if (isPropertyBasedQuery(o)) { @@ -174,6 +179,12 @@ const _buildChecks = (o: QueryTokens): ((obj: object) => boolean) => { } const threes = threeitize(o) + // Check if all links are the same type (all OR or all AND) + const allLinksAreSame = threes.every( + ([, link]) => link.toLowerCase() === threes[0][1].toLowerCase() + ) + const allLinksAreOr = allLinksAreSame && threes[0][1].toLowerCase() === 'or' + const checks = threes.reduce((acc, [a, link, b]) => { const check1 = _buildChecks(a) const check2 = _buildChecks(b) @@ -181,7 +192,10 @@ const _buildChecks = (o: QueryTokens): ((obj: object) => boolean) => { const combinedCheck = checkFunc(check1, check2) return [...acc, combinedCheck] }, []) - return _allCheck(checks) + + // If all links are OR, combine checks with OR logic + // Otherwise, combine with AND logic (which handles mixed AND/OR correctly) + return allLinksAreOr ? _anyCheck(checks) : _allCheck(checks) } /* istanbul ignore next */ throw new Error('Should never happen') diff --git a/test/src/lib.test.ts b/test/src/lib.test.ts index 5f581ec..8ff0af3 100644 --- a/test/src/lib.test.ts +++ b/test/src/lib.test.ts @@ -89,6 +89,59 @@ const TestData2 = [ }, ] +const OrQueryTestData = [ + { + id: 'edf73dba-216a-4e10-a38f-398a4b38350a', + name: 'name-2', + age: 2, + }, + { + id: '2c3e6547-2d6b-44c3-ad2c-1220a3d305be', + name: 'name-3', + age: 10, + }, + { + id: 'ed1dc8ff-fdc5-401c-a229-8566a418ceb5', + name: 'name-1', + age: 1, + }, + { + id: 'name-4-id', + name: 'name-4', + age: 15, + }, + { + id: 'name-5-id', + name: 'name-5', + age: 20, + }, + { + id: 'name-7-id', + name: 'name-7', + age: 20, + }, + { + id: 'name-6-id', + name: 'name-6', + age: 20, + }, + { + id: 'name-9-id', + name: 'name-9', + age: 30, + }, + { + id: 'name-10-id', + name: 'name-10', + age: 100, + }, + { + id: 'name-8-id', + name: 'name-8', + age: 50, + }, +] + describe('/src/lib.ts', () => { describe('#filterResults()', () => { it('should return 1 result with TestData2 when searching an object as a stringifyied json', () => { @@ -330,5 +383,41 @@ describe('/src/lib.ts', () => { const expected = 2 assert.deepEqual(actual, expected) }) + it('should return 3 results with OrQueryTestData when searching three OR properties (name-8 OR name-1 OR name-10)', () => { + const actual = filterResults( + queryBuilder() + .property('name', 'name-8') + .or() + .property('name', 'name-1') + .or() + .property('name', 'name-10') + .compile(), + OrQueryTestData + ) + const expected = 3 + assert.equal(actual.length, expected, `Expected 3 results but got ${actual.length}. Results: ${JSON.stringify(actual.map(r => r.name))}`) + // Verify we got the correct items (order doesn't matter) + const names = new Set(actual.map(r => r.name)) + const expectedNames = new Set(['name-1', 'name-8', 'name-10']) + assert.deepEqual(names, expectedNames, 'Should find name-1, name-8, and name-10 (order independent)') + }) + it('should return 3 results with OrQueryTestData when searching three OR properties in different order (name-1 OR name-10 OR name-8)', () => { + const actual = filterResults( + queryBuilder() + .property('name', 'name-1') + .or() + .property('name', 'name-10') + .or() + .property('name', 'name-8') + .compile(), + OrQueryTestData + ) + const expected = 3 + assert.equal(actual.length, expected, `Expected 3 results but got ${actual.length}. Results: ${JSON.stringify(actual.map(r => r.name))}`) + // Verify we got the correct items (order doesn't matter) + const names = new Set(actual.map(r => r.name)) + const expectedNames = new Set(['name-1', 'name-8', 'name-10']) + assert.deepEqual(names, expectedNames, 'Should find name-1, name-8, and name-10 (order independent)') + }) }) }) From 6b959ba91ae31dd38578d407baca993ab360728d Mon Sep 17 00:00:00 2001 From: Mike Cornwell Date: Mon, 12 Jan 2026 11:46:25 -0500 Subject: [PATCH 2/2] style(prettier): run prettier --- src/lib.ts | 4 ++-- test/src/lib.test.ts | 24 ++++++++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/lib.ts b/src/lib.ts index 024c691..be11468 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -184,7 +184,7 @@ const _buildChecks = (o: QueryTokens): ((obj: object) => boolean) => { ([, link]) => link.toLowerCase() === threes[0][1].toLowerCase() ) const allLinksAreOr = allLinksAreSame && threes[0][1].toLowerCase() === 'or' - + const checks = threes.reduce((acc, [a, link, b]) => { const check1 = _buildChecks(a) const check2 = _buildChecks(b) @@ -192,7 +192,7 @@ const _buildChecks = (o: QueryTokens): ((obj: object) => boolean) => { const combinedCheck = checkFunc(check1, check2) return [...acc, combinedCheck] }, []) - + // If all links are OR, combine checks with OR logic // Otherwise, combine with AND logic (which handles mixed AND/OR correctly) return allLinksAreOr ? _anyCheck(checks) : _allCheck(checks) diff --git a/test/src/lib.test.ts b/test/src/lib.test.ts index 8ff0af3..405a272 100644 --- a/test/src/lib.test.ts +++ b/test/src/lib.test.ts @@ -395,11 +395,19 @@ describe('/src/lib.ts', () => { OrQueryTestData ) const expected = 3 - assert.equal(actual.length, expected, `Expected 3 results but got ${actual.length}. Results: ${JSON.stringify(actual.map(r => r.name))}`) + assert.equal( + actual.length, + expected, + `Expected 3 results but got ${actual.length}. Results: ${JSON.stringify(actual.map(r => r.name))}` + ) // Verify we got the correct items (order doesn't matter) const names = new Set(actual.map(r => r.name)) const expectedNames = new Set(['name-1', 'name-8', 'name-10']) - assert.deepEqual(names, expectedNames, 'Should find name-1, name-8, and name-10 (order independent)') + assert.deepEqual( + names, + expectedNames, + 'Should find name-1, name-8, and name-10 (order independent)' + ) }) it('should return 3 results with OrQueryTestData when searching three OR properties in different order (name-1 OR name-10 OR name-8)', () => { const actual = filterResults( @@ -413,11 +421,19 @@ describe('/src/lib.ts', () => { OrQueryTestData ) const expected = 3 - assert.equal(actual.length, expected, `Expected 3 results but got ${actual.length}. Results: ${JSON.stringify(actual.map(r => r.name))}`) + assert.equal( + actual.length, + expected, + `Expected 3 results but got ${actual.length}. Results: ${JSON.stringify(actual.map(r => r.name))}` + ) // Verify we got the correct items (order doesn't matter) const names = new Set(actual.map(r => r.name)) const expectedNames = new Set(['name-1', 'name-8', 'name-10']) - assert.deepEqual(names, expectedNames, 'Should find name-1, name-8, and name-10 (order independent)') + assert.deepEqual( + names, + expectedNames, + 'Should find name-1, name-8, and name-10 (order independent)' + ) }) }) })