Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: falsy boolean queries #13

Merged
merged 20 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bf9c9d7
test: mock engine should not give values by reference
acodeninja Sep 21, 2024
629b989
test: allow stubbing fs without specifying a filesystem
acodeninja Sep 21, 2024
2c71b6a
refactor: make Engine.configuration public
acodeninja Sep 21, 2024
6ef2bdc
fix: allow falsy matching queries
acodeninja Sep 23, 2024
66b10e1
refactor: unnecessary return await
acodeninja Sep 23, 2024
cff112b
refactor: avoid square-bracket notation
acodeninja Sep 23, 2024
b75d39e
refactor: empty functions
acodeninja Sep 23, 2024
3a8aa41
refactor: variable name shadows variable in outer scope
acodeninja Sep 23, 2024
866776b
refactor: class methods should utilize this
acodeninja Sep 23, 2024
5b343f6
refactor: all code paths should have explicit returns, or none
acodeninja Sep 23, 2024
3fcadbc
refactor: remove shorthand type coercions
acodeninja Sep 23, 2024
646154c
refactor: usage of exported name as property of default import
acodeninja Sep 23, 2024
c9f9aa1
docs: add jsdoc blocks for test models
acodeninja Sep 23, 2024
38ee35f
docs: add jsdoc block for test model collection
acodeninja Sep 23, 2024
f3824fb
docs: add jsdoc blocks tyo all type classes
acodeninja Sep 23, 2024
e7e0068
docs: add jsdoc block for complex types
acodeninja Sep 24, 2024
21f0200
docs: add jsdoc blocks for engine classes
acodeninja Sep 24, 2024
5727394
docs: add jsdoc block for calledWith test function
acodeninja Sep 24, 2024
25b45f8
refactor: file retrival consistency
acodeninja Sep 24, 2024
4b1fee8
refactor: async function should have await expression
acodeninja Sep 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Persist.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import enableTransactions from './Transactions.js';
/**
* @class Persist
*/
export default class Persist {
class Persist {
static _engine = {};
/**
* @memberof Persist
Expand Down Expand Up @@ -42,3 +42,5 @@ export default class Persist {
engine.configure(configuration);
}
}

export default Persist;
6 changes: 3 additions & 3 deletions src/Persist.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import test from 'ava';
class TestEngine {
static configure(configuration = {}) {
class ConfiguredTestEngine extends TestEngine {
static _configuration = configuration;
static configuration = configuration;
}

return ConfiguredTestEngine;
Expand All @@ -19,13 +19,13 @@ test('includes Type', async t => {
test('.addEngine(group, engine, configuration) adds and configures an engine', t => {
Persist.addEngine('one', TestEngine, {test: true});

t.like(Persist._engine.one.TestEngine._configuration, {test: true});
t.like(Persist._engine.one.TestEngine.configuration, {test: true});
});

test('.getEngine(group, engine) retrieves an engine', t => {
Persist.addEngine('one', TestEngine, {test: true});

t.like(Persist.getEngine('one', TestEngine)._configuration, {test: true});
t.like(Persist.getEngine('one', TestEngine).configuration, {test: true});
});

test('.getEngine(group, nonEngine) retrieves no engines', t => {
Expand Down
53 changes: 37 additions & 16 deletions src/Query.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,60 @@
/**
* persist query language features:
* - value match {title: 'test'} or {title: {$is: 'test'}}
* - contains match {list: {$contains: 'test'}} or {string: {$contains: 'es'}}
* - nested query {list: {$contains: {slug: 'test'}}}
* - deep nesting queries {list: {$contains: {string: {$contains: 'test'}}}}
*/

/**
* @class Query
* The `Query` class is responsible for executing searches on an indexed dataset
* based on a structured query. It supports various query types including value matches,
* contains matches, and nested queries.
*
* @example
* // The object has the property `title` witch exactly equals `test`.
* const query = new Query({title: 'test'});
* const query = new Query({title: {$is: 'test'}});
*
* // The object has the property `list` witch contains the string `test`.
* const query = new Query({list: {$contains: 'test'}});
*
* // The object has the property `string` witch contains the string `es`.
* const query = new Query({string: {$contains: 'es'}});
*
* // The object has the property `list` contains an object
* // with a property `string` that contains the string `test`.
* const query = new Query({
* list: {
* $contains: {
* string: {
* $contains: 'test'
* }
* }
* }
* });
*/
class Query {
/**
* The query object that defines the search criteria.
* @type {Object}
*/
query;

/**
* Constructs a new `Query` instance with the provided query object.
*
* @param {object} query
* @param {Object} query - The structured query object defining the search criteria.
*/
constructor(query) {
this.query = query;
}

/**
* Using the input query, find records in an index that match
* Executes the query against a model's index and returns the matching results.
*
* @param {typeof Model} model
* @param {object} index
* @param {Model.constructor} model - The model class that contains the `fromData` method for constructing models from data.
* @param {Object<string, Model>} index - The index dataset to search through.
* @returns {Array<Model>} The models that match the query.
*/
execute(model, index) {
const matchIs = (query) => !!query?.$is;
const matchIs = (query) => query?.$is !== undefined;
const matchPrimitive = (query) => ['string', 'number', 'boolean'].includes(typeof query);
const matchContains = (query) => !!query?.$contains;

const matchesQuery = (subject, inputQuery = this.query) => {
if (!subject || !inputQuery) return false;

if (matchPrimitive(inputQuery)) return subject === inputQuery;

if (matchIs(inputQuery))
Expand Down
195 changes: 95 additions & 100 deletions src/Query.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {MainModel} from '../test/fixtures/TestModel.js';
import {MainModel} from '../test/fixtures/Models.js';
import {Models} from '../test/fixtures/ModelCollection.js';
import Query from './Query.js';
import {TestIndex} from '../test/fixtures/TestIndex.js';
import test from 'ava';

test('new Query(query) stores the query', t => {
Expand All @@ -9,122 +9,117 @@ test('new Query(query) stores the query', t => {
t.deepEqual(query.query, {string: 'test'});
});

test('Query.execute(index) finds exact matches with primitive types', t => {
test('Query.execute(index) finds exact string matches with primitive type', t => {
const models = new Models();
const model = models.createFullTestModel();

const query = new Query({string: 'test'});
const results = query.execute(MainModel, TestIndex);

t.deepEqual(results, [
MainModel.fromData({
id: 'MainModel/000000000000',
string: 'test',
arrayOfString: ['test'],
linkedMany: [{
id: 'LinkedManyModel/000000000000000',
string: 'test',
}],
}),
]);

const results = query.execute(MainModel, models.getIndex(MainModel));

t.like(results, [model.toIndexData()]);
});

test('Query.execute(index) finds exact matches with $is', t => {
test('Query.execute(index) finds exact string matches with $is', t => {
const models = new Models();
const model = models.createFullTestModel();
models.createFullTestModel({string: 'another test'});

const query = new Query({string: {$is: 'test'}});
const results = query.execute(MainModel, TestIndex);

t.deepEqual(results, [
MainModel.fromData({
id: 'MainModel/000000000000',
string: 'test',
arrayOfString: ['test'],
linkedMany: [{
id: 'LinkedManyModel/000000000000000',
string: 'test',
}],
}),
]);
const results = query.execute(MainModel, models.getIndex(MainModel));

t.like(results, [model.toIndexData()]);
});

test('Query.execute(index) finds exact boolean matches with primitive type', t => {
const models = new Models();
const model = models.createFullTestModel();

const query = new Query({boolean: false});
const results = query.execute(MainModel, models.getIndex(MainModel));

t.like(results, [model.toIndexData()]);
});

test('Query.execute(index) finds exact boolean matches with $is', t => {
const models = new Models();
const model = models.createFullTestModel();

const query = new Query({boolean: {$is: false}});
const results = query.execute(MainModel, models.getIndex(MainModel));

t.like(results, [model.toIndexData()]);
});

test('Query.execute(index) finds exact number matches with $is', t => {
const models = new Models();
const model = models.createFullTestModel();

const query = new Query({number: {$is: 24.3}});
const results = query.execute(MainModel, models.getIndex(MainModel));

t.like(results, [model.toIndexData()]);
});

test('Query.execute(index) finds exact number matches with primitive type', t => {
const models = new Models();
const model = models.createFullTestModel();

const query = new Query({number: 24.3});
const results = query.execute(MainModel, models.getIndex(MainModel));

t.like(results, [model.toIndexData()]);
});

test('Query.execute(index) finds matches containing for strings', t => {
const models = new Models();
const model1 = models.createFullTestModel();
const model2 = models.createFullTestModel();
models.createFullTestModel({string: 'not matching'});

model2.string = 'testing';

const query = new Query({string: {$contains: 'test'}});
const results = query.execute(MainModel, TestIndex);

t.deepEqual(results, [
MainModel.fromData({
id: 'MainModel/000000000000',
string: 'test',
arrayOfString: ['test'],
linkedMany: [{
id: 'LinkedManyModel/000000000000000',
string: 'test',
}],
}),
MainModel.fromData({
id: 'MainModel/111111111111',
string: 'testing',
arrayOfString: ['testing'],
linkedMany: [{
id: 'LinkedManyModel/111111111111',
string: 'testing',
}],
}),
const results = query.execute(MainModel, models.getIndex(MainModel));

t.like(results, [
model1.toIndexData(),
model2.toIndexData(),
]);
});

test('Query.execute(index) finds matches containing for arrays', t => {
const models = new Models();
const model = models.createFullTestModel();

const query = new Query({arrayOfString: {$contains: 'test'}});
const results = query.execute(MainModel, TestIndex);

t.deepEqual(results, [
MainModel.fromData({
id: 'MainModel/000000000000',
string: 'test',
arrayOfString: ['test'],
linkedMany: [{
id: 'LinkedManyModel/000000000000000',
string: 'test',
}],
}),
]);
const results = query.execute(MainModel, models.getIndex(MainModel));

t.like(results, [model.toIndexData()]);
});

test('Query.execute(index) finds exact matches for elements in arrays', t => {
const query = new Query({linkedMany: {$contains: {string: 'test'}}});
const results = query.execute(MainModel, TestIndex);

t.deepEqual(results, [
MainModel.fromData({
id: 'MainModel/000000000000',
string: 'test',
arrayOfString: ['test'],
linkedMany: [{
id: 'LinkedManyModel/000000000000000',
string: 'test',
}],
}),
]);
const models = new Models();
const model = models.createFullTestModel();

const query = new Query({linkedMany: {$contains: {string: 'many'}}});
const results = query.execute(MainModel, models.getIndex(MainModel));

t.like(results, [model.toIndexData()]);
});

test('Query.execute(index) finds partial matches for elements in arrays', t => {
const query = new Query({linkedMany: {$contains: {string: {$contains: 'test'}}}});
const results = query.execute(MainModel, TestIndex);

t.deepEqual(results, [
MainModel.fromData({
id: 'MainModel/000000000000',
string: 'test',
arrayOfString: ['test'],
linkedMany: [{
id: 'LinkedManyModel/000000000000000',
string: 'test',
}],
}),
MainModel.fromData({
id: 'MainModel/111111111111',
string: 'testing',
arrayOfString: ['testing'],
linkedMany: [{
id: 'LinkedManyModel/111111111111',
string: 'testing',
}],
}),
const models = new Models();
const model1 = models.createFullTestModel();
const model2 = models.createFullTestModel();

model2.linkedMany[0].string = 'many tests';

const query = new Query({linkedMany: {$contains: {string: {$contains: 'many'}}}});
const results = query.execute(MainModel, models.getIndex(MainModel));

t.like(results, [
model1.toIndexData(),
model2.toIndexData(),
]);
});
Loading