Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"parserOptions": {
"ecmaVersion": 2017
},

"env": {
"es6": true
}
}
120 changes: 110 additions & 10 deletions lib/adapter/adapters/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ var comparisons = [
exports.generateId = generateId


exports.applyOptions = function (fields, records, options, meta) {
exports.applyOptions = function (fields, records, options, meta, adapterInstance, type) {
var count, record, field, isInclude, isExclude, language, memoizedRecords
var i, j

Expand All @@ -53,7 +53,7 @@ exports.applyOptions = function (fields, records, options, meta) {
records = []
for (i = 0, j = memoizedRecords.length; i < j; i++) {
record = memoizedRecords[i]
if (match(fields, options, record))
if (match(fields, options, record, adapterInstance, type))
records.push(record)
}
}
Expand Down Expand Up @@ -117,19 +117,19 @@ function checkValue (fieldDefinition, a) {
}
}

function match (fields, options, record) {
function match (fields, options, record, adapterInstance, type) {
var key

for (key in options)
switch (key) {
case 'and':
if (!matchByLogicalAnd(fields, options[key], record)) return false
if (!matchByLogicalAnd(fields, options[key], record, adapterInstance, type)) return false
break
case 'or':
if (!matchByLogicalOr(fields, options[key], record)) return false
if (!matchByLogicalOr(fields, options[key], record, adapterInstance, type)) return false
break
case 'not':
if (match(fields, options[key], record)) return false
if (match(fields, options[key], record, adapterInstance, type)) return false
break
case 'range':
if (!matchByRange(fields, options[key], record)) return false
Expand All @@ -140,26 +140,28 @@ function match (fields, options, record) {
case 'exists':
if (!matchByExistence(fields, options[key], record)) return false
break
case 'fuzzyMatch':
if (!matchByFuzzyMatch (options[key], record, adapterInstance, type) ) return false
default:
}

return true
}

function matchByLogicalAnd (fields, clauses, record) {
function matchByLogicalAnd (fields, clauses, record, adapterInstance, type) {
var i

for (i = 0; i < clauses.length; i++)
if (!match(fields, clauses[i], record)) return false
if (!match(fields, clauses[i], record, adapterInstance, type)) return false

return true
}

function matchByLogicalOr (fields, clauses, record) {
function matchByLogicalOr (fields, clauses, record, adapterInstance, type) {
var i

for (i = 0; i < clauses.length; i++)
if (match(fields, clauses[i], record)) return true
if (match(fields, clauses[i], record, adapterInstance, type)) return true

return false
}
Expand Down Expand Up @@ -232,6 +234,104 @@ function matchByRange (fields, ranges, record) {
}


/**
* Fuzzy matching of attribute values.
* Works both on attributes of the record,
* or attributes of (nested) relations of the record.
*/
function matchByFuzzyMatch (filters, record, adapterInstance, type) {
for(const filterProperty of Object.keys(filters)){
const valueToFilter = filters[filterProperty]
if(isRelationFilter(filterProperty)){
const isMatching = doesFuzzyMatchRelation(record,
filterProperty,
valueToFilter,
adapterInstance,
type)
if(!isMatching) return false;
}
else if(!doesFuzzyMatchSimple(record, filterProperty, valueToFilter))
return false
}
return true
}

function doesFuzzyMatchSimple(record, filterProp, valueToFilter){
const recordValue = record[filterProp]
return String(recordValue).toLowerCase().includes(String(valueToFilter).toLowerCase())
}

function doesFuzzyMatchRelation( record, filterProp, valueToFilter, adapterInstance, type){
const relationFilterSegments = getRelationFilterSegments(filterProp)
const recordTypes = adapterInstance.recordTypes
const currentFields = recordTypes[type]
const typesPath = constructTypesPathToChild(recordTypes,
currentFields,
relationFilterSegments,
[] )
const attributeToFilter = relationFilterSegments.splice(-1)
const recordsForFiltering = walkRelationPath(currentFields,
[ record ],
relationFilterSegments,
typesPath,
adapterInstance)
return recordsForFiltering
.filter(record => doesFuzzyMatchSimple(record, attributeToFilter, valueToFilter) )
.length
}

function walkRelationPath(currentFields, currRecords,
relationPath, typesPath, adapterInstance){

if(!relationPath.length){
return currRecords
}
const nextRecords = []
const relation = relationPath[0]
const targetType = typesPath[0]
const isArray = currentFields[relation].isArray

for(const currRecord of currRecords){
const ids = isArray ? currRecord[relation] : [ currRecord[relation] ]

for(const id of ids){
const record = adapterInstance.db[targetType][id]
if(record) nextRecords.push( record )
}
}

return walkRelationPath(adapterInstance.recordTypes[targetType],
nextRecords, relationPath.slice(1),
typesPath.slice(1), adapterInstance)
}

function isRelationFilter ( field ) {
return field.split(':').length > 1
}

function getRelationFilterSegments ( field ) {
return field.split(':')
}

function constructTypesPathToChild ( recordTypes, parent,
remainingPathSegments, typesPath ) {
if ( !remainingPathSegments.length )
return typesPath


const segment = remainingPathSegments[0]
const nextType = parent[segment].link

//complex type
if ( nextType ) {
typesPath.push( nextType )
parent = recordTypes[nextType]
}
return constructTypesPathToChild(recordTypes, parent,
remainingPathSegments.slice(1), typesPath )
}


function findByType (type) {
return function (pair) {
var hasMatch = type === pair[0] ||
Expand Down
4 changes: 3 additions & 1 deletion lib/adapter/adapters/memory/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ module.exports = function (Adapter) {
var recordTypes = self.recordTypes
var fields = recordTypes[type]
var collection = self.db[type]

var records = []
var i, j, id, record

Expand All @@ -74,7 +75,8 @@ module.exports = function (Adapter) {
else for (id in collection)
records.push(outputRecord.call(self, type, collection[id]))

return Promise.resolve(applyOptions(fields, records, options, meta))
return Promise
.resolve(applyOptions(fields, records, options, meta, self, type))
}


Expand Down
40 changes: 40 additions & 0 deletions test/unit/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,46 @@ module.exports = (adapter, options) => {
})
})

run((assert, comment) => {
comment('find: fuzzyMatch')
return test(adapter => {
debugger
return Promise.all([
adapter.find(type, null, {
fuzzyMatch: {
"friends:name": "jo"
}
})
])
.then(results => {
results.forEach((records) => {
assert(records.length === 1, 'match length is correct')
assert(records[0].name === 'bob', 'matched correct record')
})
})
})
})

run((assert, comment) => {
comment('find: fuzzyMatch in this world, the friend of a friend is myself')
return test(adapter => {
debugger
return Promise.all([
adapter.find(type, null, {
fuzzyMatch: {
"friends:friends:name": "jOHn"
}
})
])
.then(results => {
results.forEach((records) => {
assert(records.length === 1, 'match length is correct')
assert(records[0].name === 'john', 'matched correct record')
})
})
})
})

run((assert, comment) => {
comment('find: match (string)')
return test(adapter => {
Expand Down