diff --git a/packages/datastore/__tests__/Predicate.ts b/packages/datastore/__tests__/Predicate.ts index e76dd8966c6..55e82b6f417 100644 --- a/packages/datastore/__tests__/Predicate.ts +++ b/packages/datastore/__tests__/Predicate.ts @@ -218,8 +218,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(1); - expect(matches[0].name).toBe('Adam West'); + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on eq - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.eq('Adam West') + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + 'Bob Jones', + 'Clarice Starling', + 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); test('match on ne', async () => { @@ -229,10 +251,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe( - getFlatAuthorsArrayFixture().length - 1 + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + 'Bob Jones', + 'Clarice Starling', + 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on ne - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.ne('Adam West') ); - expect(matches.some(a => a.name === 'Adam West')).toBe(false); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); }); test('match on gt', async () => { @@ -242,13 +284,32 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(2); - expect(matches.map(m => m.name)).toEqual([ + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', 'Debbie Donut', 'Zelda from the Legend of Zelda', ]); }); + test('match on gt - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.gt('Clarice Starling') + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + test('match on ge', async () => { const query = recursivePredicateFor(AuthorMeta).name.ge('Clarice Starling'); @@ -256,14 +317,32 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(3); - expect(matches.map(m => m.name)).toEqual([ + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', 'Clarice Starling', 'Debbie Donut', 'Zelda from the Legend of Zelda', ]); }); + test('match on ge - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.ge('Clarice Starling') + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + test('match on lt', async () => { const query = recursivePredicateFor(AuthorMeta).name.lt('Clarice Starling'); @@ -271,10 +350,29 @@ describe('Predicates', () => { query ); - expect(matches.length).toBe(2); - expect(matches.map(m => m.name)).toEqual([ + expect(matches.map(n => n.name)).toEqual([ 'Adam West', 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on lt - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.lt('Clarice Starling') + ); + const matches = await mechanism.execute>( + query + ); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + 'Clarice Starling', + 'Debbie Donut', + 'Zelda from the Legend of Zelda', ]); }); @@ -285,11 +383,29 @@ describe('Predicates', () => { query ); - expect(matches.length).toBe(3); - expect(matches.map(m => m.name)).toEqual([ + expect(matches.map(n => n.name)).toEqual([ 'Adam West', 'Bob Jones', 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on le - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.le('Clarice Starling') + ); + const matches = await mechanism.execute>( + query + ); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + 'Zelda from the Legend of Zelda', ]); }); @@ -300,8 +416,30 @@ describe('Predicates', () => { query ); - expect(matches.length).toBe(1); - expect(matches[0].name).toBe('Debbie Donut'); + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match beginsWith - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.beginsWith('Debbie') + ); + const matches = await mechanism.execute>( + query + ); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); test('match between an outer inclusive range', async () => { @@ -315,7 +453,6 @@ describe('Predicates', () => { query ); - expect(matches.length).toBe(5); expect(matches.map(m => m.name)).toEqual([ 'Adam West', 'Bob Jones', @@ -325,6 +462,19 @@ describe('Predicates', () => { ]); }); + test('match between an outer inclusive range - NEGATED', async () => { + // `0` is immediately before `A` + // `{` is immediately after `z` + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.between('0', '{') + ); + const matches = await mechanism.execute>( + query + ); + + expect(matches.length).toBe(0); + }); + test('match between with equality at both ends', async () => { const query = recursivePredicateFor(AuthorMeta).name.between( 'Bob Jones', @@ -334,11 +484,29 @@ describe('Predicates', () => { query ); - expect(matches.length).toBe(3); - expect(matches.map(m => m.name)).toEqual([ + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', 'Bob Jones', 'Clarice Starling', 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match between with equality at both ends - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.between('Bob Jones', 'Debbie Donut') + ); + const matches = await mechanism.execute>( + query + ); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', ]); }); @@ -351,11 +519,29 @@ describe('Predicates', () => { query ); - expect(matches.length).toBe(3); - expect(matches.map(m => m.name)).toEqual([ + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', 'Bob Jones', 'Clarice Starling', 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match between an inner range - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.between('Az', 'E') + ); + const matches = await mechanism.execute>( + query + ); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', ]); }); @@ -371,6 +557,17 @@ describe('Predicates', () => { expect(matches.length).toBe(0); }); + test('match nothing between a mismatching range - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.between('{', '}') + ); + const matches = await mechanism.execute>( + query + ); + + expect(matches.length).toBe(5); + }); + test('match contains', async () => { const query = recursivePredicateFor(AuthorMeta).name.contains('Jones'); @@ -378,8 +575,30 @@ describe('Predicates', () => { query ); - expect(matches.length).toBe(1); - expect(matches[0].name).toBe('Bob Jones'); + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match contains - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.contains('Jones') + ); + const matches = await mechanism.execute>( + query + ); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + 'Clarice Starling', + 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); test('match notContains', async () => { @@ -389,14 +608,31 @@ describe('Predicates', () => { query ); - expect(matches.length).toBe(4); - expect(matches.map(m => m.name)).toEqual([ + expect(matches.map(n => n.name)).toEqual([ 'Adam West', + // 'Bob Jones', 'Clarice Starling', 'Debbie Donut', 'Zelda from the Legend of Zelda', ]); }); + + test('match notContains - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.name.notContains('Jones') + ); + const matches = await mechanism.execute>( + query + ); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); }); describe('on boolean fields', () => { @@ -406,47 +642,77 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(3); + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); - test('match on ne', async () => { - const query = recursivePredicateFor(AuthorMeta).isActive.ne(true); + test('match on eq - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.isActive.eq(true) + ); const matches = await mechanism.execute< ModelOf> >(query); - expect(matches.length).toBe(2); + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); }); - test('match on gt true', async () => { - const query = recursivePredicateFor(AuthorMeta).isActive.gt(true); + test('match on ne', async () => { + const query = recursivePredicateFor(AuthorMeta).isActive.ne(true); const matches = await mechanism.execute< ModelOf> >(query); - expect(matches.length).toBe(0); + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); }); - test('match on gt false', async () => { - const query = recursivePredicateFor(AuthorMeta).isActive.gt(false); + test('match on ne - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.isActive.ne(true) + ); const matches = await mechanism.execute< ModelOf> >(query); - expect(matches.length).toBe(3); + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); - test('match on ge true', async () => { - const query = recursivePredicateFor(AuthorMeta).isActive.ge(true); + test('match on gt true', async () => { + const query = recursivePredicateFor(AuthorMeta).isActive.gt(true); const matches = await mechanism.execute< ModelOf> >(query); - expect(matches.length).toBe(3); + expect(matches.length).toBe(0); }); - test('match on ge false', async () => { - const query = recursivePredicateFor(AuthorMeta).isActive.ge(false); + test('match on gt true - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.isActive.gt(true) + ); const matches = await mechanism.execute< ModelOf> >(query); @@ -454,23 +720,126 @@ describe('Predicates', () => { expect(matches.length).toBe(5); }); - test('match on lt true', async () => { - const query = recursivePredicateFor(AuthorMeta).isActive.lt(true); + test('match on gt false', async () => { + const query = recursivePredicateFor(AuthorMeta).isActive.gt(false); const matches = await mechanism.execute< ModelOf> >(query); - expect(matches.length).toBe(2); + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); - test('match on lt false', async () => { - const query = recursivePredicateFor(AuthorMeta).isActive.lt(false); + test('match on gt false - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.isActive.gt(false) + ); const matches = await mechanism.execute< ModelOf> >(query); - expect(matches.length).toBe(0); - }); + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on ge true - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.isActive.ge(true) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on ge false', async () => { + const query = recursivePredicateFor(AuthorMeta).isActive.ge(false); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.length).toBe(5); + }); + + test('match on ge false - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.isActive.ge(false) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.length).toBe(0); + }); + + test('match on lt true', async () => { + const query = recursivePredicateFor(AuthorMeta).isActive.lt(true); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on lt true - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.isActive.lt(true) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on lt false', async () => { + const query = recursivePredicateFor(AuthorMeta).isActive.lt(false); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.length).toBe(0); + }); + + test('match on lt false - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.isActive.lt(false) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.length).toBe(5); + }); test('match on le true', async () => { const query = recursivePredicateFor(AuthorMeta).isActive.le(true); @@ -481,13 +850,47 @@ describe('Predicates', () => { expect(matches.length).toBe(5); }); + test('match on le true - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.isActive.le(true) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.length).toBe(0); + }); + test('match on le false', async () => { const query = recursivePredicateFor(AuthorMeta).isActive.le(false); const matches = await mechanism.execute< ModelOf> >(query); - expect(matches.length).toBe(2); + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on le false - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.isActive.le(false) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); }); @@ -498,8 +901,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(1); - expect(matches[0].name).toEqual('Debbie Donut'); + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on eq - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.karma.eq(3) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); test('match on ne', async () => { @@ -508,8 +933,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(4); - expect(matches.map(n => n.name)).not.toContain('Debbie Donut'); + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on ne - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.karma.ne(3) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); }); test('match on gt', async () => { @@ -518,8 +965,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(1); - expect(matches[0].name).toEqual('Zelda from the Legend of Zelda'); + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on gt - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.karma.gt(3) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); }); test('match on ge', async () => { @@ -528,11 +997,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(2); - expect(matches.map(n => n.name)).toContain('Debbie Donut'); - expect(matches.map(n => n.name)).toContain( - 'Zelda from the Legend of Zelda' + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on ge - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.karma.ge(3) ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); }); test('match on lt', async () => { @@ -541,11 +1029,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(3); - expect(matches.map(n => n.name)).not.toContain('Debbie Donut'); - expect(matches.map(n => n.name)).not.toContain( - 'Zelda from the Legend of Zelda' + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on lt - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.karma.lt(3) ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); test('match on le', async () => { @@ -554,10 +1061,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(4); - expect(matches.map(n => n.name)).not.toContain( - 'Zelda from the Legend of Zelda' + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on le - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.karma.le(3) ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); test('match on between', async () => { @@ -566,11 +1093,29 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(3); expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', 'Bob Jones', 'Clarice Starling', 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on between - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.karma.between(1, 3) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', ]); }); }); @@ -582,8 +1127,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(1); - expect(matches[0].name).toEqual('Debbie Donut'); + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on eq - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.rating.eq(0.75) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); test('match on ne', async () => { @@ -592,8 +1159,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(4); - expect(matches.map(n => n.name)).not.toContain('Debbie Donut'); + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on ne - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.rating.ne(0.75) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); }); test('match on gt', async () => { @@ -602,8 +1191,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(1); - expect(matches[0].name).toEqual('Zelda from the Legend of Zelda'); + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on gt - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.rating.gt(0.75) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); }); test('match on ge', async () => { @@ -612,11 +1223,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(2); - expect(matches.map(n => n.name)).toContain('Debbie Donut'); - expect(matches.map(n => n.name)).toContain( - 'Zelda from the Legend of Zelda' + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on ge - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.rating.ge(0.75) ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); }); test('match on lt', async () => { @@ -625,11 +1255,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(3); - expect(matches.map(n => n.name)).not.toContain('Debbie Donut'); - expect(matches.map(n => n.name)).not.toContain( - 'Zelda from the Legend of Zelda' + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + // 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on lt - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.rating.lt(0.75) ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); test('match on le', async () => { @@ -638,10 +1287,30 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(4); - expect(matches.map(n => n.name)).not.toContain( - 'Zelda from the Legend of Zelda' + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + 'Bob Jones', + 'Clarice Starling', + 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on le - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.rating.le(0.75) ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', + ]); }); test('match on between', async () => { @@ -653,11 +1322,29 @@ describe('Predicates', () => { ModelOf> >(query); - expect(matches.length).toBe(3); expect(matches.map(n => n.name)).toEqual([ + // 'Adam West', 'Bob Jones', 'Clarice Starling', 'Debbie Donut', + // 'Zelda from the Legend of Zelda', + ]); + }); + + test('match on between - NEGATED', async () => { + const query = recursivePredicateFor(AuthorMeta).not(a => + a.rating.between(0.25, 0.75) + ); + const matches = await mechanism.execute< + ModelOf> + >(query); + + expect(matches.map(n => n.name)).toEqual([ + 'Adam West', + // 'Bob Jones', + // 'Clarice Starling', + // 'Debbie Donut', + 'Zelda from the Legend of Zelda', ]); }); }); diff --git a/packages/datastore/src/predicates/next.ts b/packages/datastore/src/predicates/next.ts index f6a5b70e947..63474530d52 100644 --- a/packages/datastore/src/predicates/next.ts +++ b/packages/datastore/src/predicates/next.ts @@ -26,7 +26,9 @@ type GroupOperator = 'and' | 'or' | 'not'; type UntypedCondition = { fetch: (storage: StorageAdapter) => Promise[]>; matches: (item: Record) => Promise; - copy(extract: GroupCondition): [UntypedCondition, GroupCondition | undefined]; + copy( + extract?: GroupCondition + ): [UntypedCondition, GroupCondition | undefined]; toAST(): any; }; @@ -89,48 +91,6 @@ const negations = { notContains: 'contains', }; -/** - * Given a V1 predicate "seed", applies a list of V2 field-level conditions - * to the predicate, returning a new/final V1 predicate chain link. - * @param predicate The base/seed V1 predicate to build on - * @param conditions The V2 conditions to add to the predicate chain. - * @param negateChildren Whether the conditions should be negated first. - * @returns A V1 predicate, with conditions incorporated. - */ -function applyConditionsToV1Predicate( - predicate: T, - conditions: FieldCondition[], - negateChildren: boolean -): T { - let p = predicate; - const finalConditions: FieldCondition[] = []; - - for (const c of conditions) { - if (negateChildren) { - if (c.operator === 'between') { - finalConditions.push( - new FieldCondition(c.field, 'lt', [c.operands[0]]), - new FieldCondition(c.field, 'gt', [c.operands[1]]) - ); - } else { - finalConditions.push( - new FieldCondition(c.field, negations[c.operator], c.operands) - ); - } - } else { - finalConditions.push(c); - } - } - - for (const c of finalConditions) { - p = p[c.field]( - c.operator as never, - (c.operator === 'between' ? c.operands : c.operands[0]) as never - ); - } - return p; -} - /** * A condition that can operate against a single "primitive" field of a model or item. * @member field The field of *some record* to test against. @@ -158,6 +118,22 @@ export class FieldCondition { ]; } + /** + * Produces a tree structure similar to a graphql condition. The returned + * structure is "dumb" and is intended for another query/condition + * generation mechanism to interpret, such as the cloud or storage query + * builders. + * + * E.g., + * + * ```json + * { + * "name": { + * "eq": "robert" + * } + * } + * ``` + */ toAST() { return { [this.field]: { @@ -169,6 +145,44 @@ export class FieldCondition { }; } + /** + * Produces a new condition (`FieldCondition` or `GroupCondition`) that + * matches the opposite of this condition. + * + * Intended to be used when applying De Morgan's Law, which can be done to + * produce more efficient queries against the storage layer if a negation + * appears in the query tree. + * + * For example: + * + * 1. `name.eq('robert')` becomes `name.ne('robert')` + * 2. `price.between(100, 200)` becomes `m => m.or(m => [m.price.lt(100), m.price.gt(200)])` + * + * @param model The model meta to use when construction a new `GroupCondition` + * for cases where the negation requires multiple `FieldCondition`'s. + */ + negated(model: ModelMeta) { + if (this.operator === 'between') { + return new GroupCondition(model, undefined, undefined, 'or', [ + new FieldCondition(this.field, 'lt', [this.operands[0]]), + new FieldCondition(this.field, 'gt', [this.operands[1]]), + ]); + } else if (this.operator === 'beginsWith') { + // beginsWith negation doesn't have a good, safe optimation right now. + // just re-wrap it in negation. The adapter will have to scan-and-filter, + // as is likely optimal for negated beginsWith conditions *anyway*. + return new GroupCondition(model, undefined, undefined, 'not', [ + new FieldCondition(this.field, 'beginsWith', [this.operands[0]]), + ]); + } else { + return new FieldCondition( + this.field, + negations[this.operator], + this.operands + ); + } + } + /** * Not implemented. Not needed. GroupCondition instead consumes FieldConditions and * transforms them into legacy predicates. (*For now.*) @@ -338,7 +352,7 @@ export class GroupCondition { * @param extract A node of interest. Its copy will *also* be returned if the node exists. * @returns [The full copy, the copy of `extract` | undefined] */ - copy(extract: GroupCondition): [GroupCondition, GroupCondition | undefined] { + copy(extract?: GroupCondition): [GroupCondition, GroupCondition | undefined] { const copied = new GroupCondition( this.model, this.field, @@ -359,6 +373,33 @@ export class GroupCondition { return [copied, extractedCopy]; } + /** + * Creates a new `GroupCondition` that contains only the local field conditions, + * omitting related model conditions. That resulting `GroupCondition` can be + * used to produce predicates that are compatible with the storage adapters and + * Cloud storage. + * + * @param negate Whether the condition tree should be negated according + * to De Morgan's law. + */ + withFieldConditionsOnly(negate: boolean) { + const negateChildren = negate !== (this.operator === 'not'); + return new GroupCondition( + this.model, + undefined, + undefined, + (negate ? negations[this.operator] : this.operator) as + | 'or' + | 'and' + | 'not', + this.operands + .filter(o => o instanceof FieldCondition) + .map(o => + negateChildren ? (o as FieldCondition).negated(this.model) : o + ) + ); + } + /** * Returns a version of the predicate tree with unnecessary logical groups * condensed and merged together. This is intended to create a dense tree @@ -558,17 +599,9 @@ export class GroupCondition { // if conditions is empty at this point, child predicates found no matches. // i.e., we can stop looking and return empty. if (conditions.length > 0) { - const predicate = FlatModelPredicateCreator.createFromExisting( - this.model.schema, - p => - p[operator](c => - applyConditionsToV1Predicate(c, conditions, negateChildren) - ) - ); - - resultGroups.push( - await storage.query(this.model.builder, predicate as any) - ); + const predicate = + this.withFieldConditionsOnly(negateChildren).toStoragePredicate(); + resultGroups.push(await storage.query(this.model.builder, predicate)); } else if (conditions.length === 0 && resultGroups.length === 0) { resultGroups.push(await storage.query(this.model.builder)); } diff --git a/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts b/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts index ccf0a0dc1e1..487fb11fc92 100644 --- a/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts +++ b/packages/datastore/src/storage/adapter/IndexedDBAdapter.ts @@ -443,6 +443,17 @@ class IndexedDBAdapter implements Adapter { const hasPagination = pagination && pagination.limit; const records: T[] = (await (async () => { + // + // NOTE: @svidgen explored removing this and letting query() take care of automatic + // index leveraging. This would eliminate some amount of very similar code. + // But, getAll is slightly slower than get() + // + // On Chrome: + // ~700ms vs ~1175ms per 10k reads. + // + // You can (and should) check my work here: + // https://gist.github.com/svidgen/74e55d573b19c3e5432b1b5bdf0f4d96 + // if (queryByKey) { const record = await this.getByKey(storeName, queryByKey); return record ? [record] : []; @@ -495,7 +506,16 @@ class IndexedDBAdapter implements Adapter { for (const key of keyPath) { const predicateObj = predicateObjs.find( - p => isPredicateObj(p) && p.field === key && p.operator === 'eq' + p => + // it's a relevant predicate object only if it's an equality + // operation for a key field from the key: + isPredicateObj(p) && + p.field === key && + p.operator === 'eq' && + // it's only valid if it's not nullish. + // (IDB will throw a fit if it's nullish.) + p.operand !== null && + p.operand !== undefined ) as PredicateObject; predicateObj && keyValues.push(predicateObj.operand);