From e79ed3d292a7d468fe182b3593d2c5e4a55a0f31 Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Fri, 8 May 2020 18:27:26 +0200 Subject: [PATCH 1/7] Attempt to allow fuzzy-match filter in json-api Notes ----- Regarding integration into fortune.js I am unsure what field of request.options may be used. As I understand it, Adapter.find leaves the possibility of extra fields on the options object unspecified. Therefore, I added the fuzzyMatch on the ptions object. Regarding validity JSON:API Filtering is relatively unspecified. As such, I assume the extra filter type is legal. --- lib/helpers.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/helpers.js b/lib/helpers.js index 1ade979..cf38a8d 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -353,6 +353,20 @@ function attachQueries (request) { request.options.range[field][index] = castValue(query[parameter], fieldType, options) } + else if (filterType === 'fuzzy-match'){ + + if ( ! fields[field].type ){ + throw new BadRequestError( `fuzzy-match only allowed on attributes.` ) + } + + if ( fields[field].type.name !== "String"){ + throw new BadRequestError( + `fuzzy-match only allowed on String types. ${field} is of type ${fields[field].type.name }` ) + } + + if (!('fuzzyMatch' in request.options)) request.options['fuzzyMatch'] = {} + request.options.fuzzyMatch[field] = query[parameter] + } else throw new BadRequestError( `The filter "${filterType}" is not valid.`) } From fce424b2539b1fa8bb412f7b240df78052771d7f Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Mon, 11 May 2020 18:09:36 +0200 Subject: [PATCH 2/7] WIP: first version filtering relationships --- lib/helpers.js | 62 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index cf38a8d..7ab43bf 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -329,10 +329,21 @@ function attachQueries (request) { const field = inflect(matches[1]) const filterType = matches[2] - if (!(field in fields)) throw new BadRequestError( - `The field "${field}" is non-existent.`) + if(field.split('.').length > 1){ + const relationPath = field; + if(filterType !== 'fuzzy-match'){ + throw new BadRequestError('Filtering relationship only supported on fuzzy-match for now') + } + console.log('Checking path') + if(! isValidRelationPath( recordTypes, fields, field.split('.')) ){ + throw new BadRequestError(`Path ${relationPath} is not valid`) + } + } - const fieldType = fields[field][keys.type] + else if (!(field in fields)) + throw new BadRequestError(`The field "${field}" is non-existent.`) + + const fieldType = getLastTypeInPath( recordTypes, fields, field.split('.') )[keys.type] if (filterType === void 0) { if (!('match' in request.options)) request.options.match = {} @@ -355,11 +366,11 @@ function attachQueries (request) { } else if (filterType === 'fuzzy-match'){ - if ( ! fields[field].type ){ - throw new BadRequestError( `fuzzy-match only allowed on attributes.` ) + if ( ! getLastTypeInPath( recordTypes, fields, field.split('.') )[keys.type] ){ + throw new BadRequestError( `fuzzy-match only allowed on attributes. For ${field}` ) } - if ( fields[field].type.name !== "String"){ + if ( getLastTypeInPath( recordTypes, fields, field.split('.') )[keys.type].name !== "String"){ throw new BadRequestError( `fuzzy-match only allowed on String types. ${field} is of type ${fields[field].type.name }` ) } @@ -529,3 +540,42 @@ function setInflectType (inflect, types) { return out } + + +function isValidRelationPath( recordTypes, fieldsCurrentType, remainingPathSegments ){ + if( !remainingPathSegments.length ){ + return false + } + const pathSegment = remainingPathSegments[0] + + if( !fieldsCurrentType[pathSegment] ){ + return false + } + if( fieldsCurrentType[pathSegment].type && remainingPathSegments.length == 1 ){ + return true + } + + const nextTypeToCompare = fieldsCurrentType[pathSegment].link + if( nextTypeToCompare ){ + return isValidRelationPath( recordTypes, recordTypes[nextTypeToCompare], remainingPathSegments.slice(1) ) + } + return false +} + +function getLastTypeInPath( recordTypes, fieldsCurrentType, remainingPathSegments ){ + if( !remainingPathSegments.length ){ + return fieldsCurrentType + } + + const pathSegment = remainingPathSegments[0] + + if( !fieldsCurrentType[pathSegment] ){ + return {} + } + + const nextType = fieldsCurrentType[pathSegment].link + if( nextType ){ + return getLastTypeInPath( recordTypes, recordTypes[nextType], remainingPathSegments.slice(1) ) + } + return fieldsCurrentType[pathSegment] +} From 801446ff9345e15805a7425e1eb10458607b8c49 Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Fri, 15 May 2020 12:33:52 +0200 Subject: [PATCH 3/7] abstract away filter parsing. --- lib/helpers.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 7ab43bf..7fc9900 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -329,13 +329,13 @@ function attachQueries (request) { const field = inflect(matches[1]) const filterType = matches[2] - if(field.split('.').length > 1){ + if(isRelationFilter(field)){ const relationPath = field; if(filterType !== 'fuzzy-match'){ throw new BadRequestError('Filtering relationship only supported on fuzzy-match for now') } console.log('Checking path') - if(! isValidRelationPath( recordTypes, fields, field.split('.')) ){ + if(! isValidRelationPath( recordTypes, fields, getRelationFilterSegments(field) ) ){ throw new BadRequestError(`Path ${relationPath} is not valid`) } } @@ -343,7 +343,7 @@ function attachQueries (request) { else if (!(field in fields)) throw new BadRequestError(`The field "${field}" is non-existent.`) - const fieldType = getLastTypeInPath( recordTypes, fields, field.split('.') )[keys.type] + const fieldType = getLastTypeInPath( recordTypes, fields, getRelationFilterSegments(field) )[keys.type] if (filterType === void 0) { if (!('match' in request.options)) request.options.match = {} @@ -366,11 +366,11 @@ function attachQueries (request) { } else if (filterType === 'fuzzy-match'){ - if ( ! getLastTypeInPath( recordTypes, fields, field.split('.') )[keys.type] ){ + if ( ! getLastTypeInPath( recordTypes, fields, getRelationFilterSegments(field) )[keys.type] ){ throw new BadRequestError( `fuzzy-match only allowed on attributes. For ${field}` ) } - if ( getLastTypeInPath( recordTypes, fields, field.split('.') )[keys.type].name !== "String"){ + if ( getLastTypeInPath( recordTypes, fields, getRelationFilterSegments(field) )[keys.type].name !== "String"){ throw new BadRequestError( `fuzzy-match only allowed on String types. ${field} is of type ${fields[field].type.name }` ) } @@ -541,7 +541,6 @@ function setInflectType (inflect, types) { return out } - function isValidRelationPath( recordTypes, fieldsCurrentType, remainingPathSegments ){ if( !remainingPathSegments.length ){ return false @@ -579,3 +578,11 @@ function getLastTypeInPath( recordTypes, fieldsCurrentType, remainingPathSegment } return fieldsCurrentType[pathSegment] } + +function isRelationFilter( field ){ + return field.split(':').length > 1 +} + +function getRelationFilterSegments( field ){ + return field.split(':') +} From 2cb203e3ed1ee2d9f545a29835d57450e587c7df Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Wed, 17 Jun 2020 17:55:23 +0200 Subject: [PATCH 4/7] fix with auto lint fix --- lib/helpers.js | 62 ++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 02cbbd7..d0e8eaa 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -305,7 +305,7 @@ function attachQueries (request) { request.options = {} // Iterate over dynamic query strings. - for (const parameter of Object.keys(query)) { + for (const parameter of Object.keys(query)) // Attach fields option. if (parameter.match(isField)) { const sparseField = Array.isArray(query[parameter]) ? @@ -330,15 +330,14 @@ function attachQueries (request) { const field = inflect(matches[1]) const filterType = matches[2] - if(isRelationFilter(field)){ - const relationPath = field; - if(filterType !== 'fuzzy-match'){ + if (isRelationFilter(field)) { + const relationPath = field + if (filterType !== 'fuzzy-match') throw new BadRequestError('Filtering relationship only supported on fuzzy-match for now') - } + console.log('Checking path') - if(! isValidRelationPath( recordTypes, fields, getRelationFilterSegments(field) ) ){ + if (! isValidRelationPath( recordTypes, fields, getRelationFilterSegments(field) ) ) throw new BadRequestError(`Path ${relationPath} is not valid`) - } } else if (!(field in fields)) @@ -365,16 +364,15 @@ function attachQueries (request) { request.options.range[field][index] = castValue(query[parameter], fieldType, options) } - else if (filterType === 'fuzzy-match'){ - - if ( ! getLastTypeInPath( recordTypes, fields, getRelationFilterSegments(field) )[keys.type] ){ + else if (filterType === 'fuzzy-match') { + if ( ! getLastTypeInPath( recordTypes, fields, getRelationFilterSegments(field) )[keys.type] ) throw new BadRequestError( `fuzzy-match only allowed on attributes. For ${field}` ) - } - if ( getLastTypeInPath( recordTypes, fields, getRelationFilterSegments(field) )[keys.type].name !== "String"){ + + if ( getLastTypeInPath( recordTypes, fields, getRelationFilterSegments(field) )[keys.type].name !== 'String') throw new BadRequestError( `fuzzy-match only allowed on String types. ${field} is of type ${fields[field].type.name }` ) - } + if (!('fuzzyMatch' in request.options)) request.options['fuzzyMatch'] = {} request.options.fuzzyMatch[field] = query[parameter] @@ -382,7 +380,7 @@ function attachQueries (request) { else throw new BadRequestError( `The filter "${filterType}" is not valid.`) } - } + // Attach include option. if (reservedKeys.include in query) { @@ -542,48 +540,48 @@ function setInflectType (inflect, types) { return out } -function isValidRelationPath( recordTypes, fieldsCurrentType, remainingPathSegments ){ - if( !remainingPathSegments.length ){ +function isValidRelationPath ( recordTypes, fieldsCurrentType, remainingPathSegments ) { + if ( !remainingPathSegments.length ) return false - } + const pathSegment = remainingPathSegments[0] - if( !fieldsCurrentType[pathSegment] ){ + if ( !fieldsCurrentType[pathSegment] ) return false - } - if( fieldsCurrentType[pathSegment].type && remainingPathSegments.length == 1 ){ + + if ( fieldsCurrentType[pathSegment].type && remainingPathSegments.length == 1 ) return true - } + const nextTypeToCompare = fieldsCurrentType[pathSegment].link - if( nextTypeToCompare ){ + if ( nextTypeToCompare ) return isValidRelationPath( recordTypes, recordTypes[nextTypeToCompare], remainingPathSegments.slice(1) ) - } + return false } -function getLastTypeInPath( recordTypes, fieldsCurrentType, remainingPathSegments ){ - if( !remainingPathSegments.length ){ +function getLastTypeInPath ( recordTypes, fieldsCurrentType, remainingPathSegments ) { + if ( !remainingPathSegments.length ) return fieldsCurrentType - } + const pathSegment = remainingPathSegments[0] - if( !fieldsCurrentType[pathSegment] ){ + if ( !fieldsCurrentType[pathSegment] ) return {} - } + const nextType = fieldsCurrentType[pathSegment].link - if( nextType ){ + if ( nextType ) return getLastTypeInPath( recordTypes, recordTypes[nextType], remainingPathSegments.slice(1) ) - } + return fieldsCurrentType[pathSegment] } -function isRelationFilter( field ){ +function isRelationFilter ( field ) { return field.split(':').length > 1 } -function getRelationFilterSegments( field ){ +function getRelationFilterSegments ( field ) { return field.split(':') } From 53ef89e4e8f9748b412d40c8bc700e2e337e099e Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Wed, 17 Jun 2020 18:12:28 +0200 Subject: [PATCH 5/7] further linting tests --- lib/helpers.js | 55 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index d0e8eaa..4ffd0fb 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -333,17 +333,25 @@ function attachQueries (request) { if (isRelationFilter(field)) { const relationPath = field if (filterType !== 'fuzzy-match') - throw new BadRequestError('Filtering relationship only supported on fuzzy-match for now') - - console.log('Checking path') - if (! isValidRelationPath( recordTypes, fields, getRelationFilterSegments(field) ) ) + throw new BadRequestError( + `Filtering relationship only + supported on fuzzy-match for now + `) + + const isValidPath = + isValidRelationPath( recordTypes, + fields, + getRelationFilterSegments(field) ) + if (! isValidPath ) throw new BadRequestError(`Path ${relationPath} is not valid`) } else if (!(field in fields)) throw new BadRequestError(`The field "${field}" is non-existent.`) - const fieldType = getLastTypeInPath( recordTypes, fields, getRelationFilterSegments(field) )[keys.type] + const filterSegments = getRelationFilterSegments(field) + const fieldType = getLastTypeInPath( recordTypes, + fields, filterSegments )[keys.type] if (filterType === void 0) { if (!('match' in request.options)) request.options.match = {} @@ -365,16 +373,24 @@ function attachQueries (request) { castValue(query[parameter], fieldType, options) } else if (filterType === 'fuzzy-match') { - if ( ! getLastTypeInPath( recordTypes, fields, getRelationFilterSegments(field) )[keys.type] ) - throw new BadRequestError( `fuzzy-match only allowed on attributes. For ${field}` ) + const lastTypeInPath = + getLastTypeInPath( recordTypes, + fields, + getRelationFilterSegments(field) ) + if ( ! lastTypeInPath[keys.type] ) + throw new BadRequestError( + `fuzzy-match only allowed on attributes. For ${field}` ) - if ( getLastTypeInPath( recordTypes, fields, getRelationFilterSegments(field) )[keys.type].name !== 'String') + if ( lastTypeInPath[keys.type].name !== 'String') throw new BadRequestError( - `fuzzy-match only allowed on String types. ${field} is of type ${fields[field].type.name }` ) + `fuzzy-match only allowed on String types. + ${field} is of type ${fields[field].type.name } + ` ) - if (!('fuzzyMatch' in request.options)) request.options['fuzzyMatch'] = {} + if (!('fuzzyMatch' in request.options)) + request.options['fuzzyMatch'] = {} request.options.fuzzyMatch[field] = query[parameter] } else throw new BadRequestError( @@ -540,7 +556,9 @@ function setInflectType (inflect, types) { return out } -function isValidRelationPath ( recordTypes, fieldsCurrentType, remainingPathSegments ) { +function isValidRelationPath ( recordTypes, + fieldsCurrentType, + remainingPathSegments ) { if ( !remainingPathSegments.length ) return false @@ -549,18 +567,23 @@ function isValidRelationPath ( recordTypes, fieldsCurrentType, remainingPathSegm if ( !fieldsCurrentType[pathSegment] ) return false - if ( fieldsCurrentType[pathSegment].type && remainingPathSegments.length == 1 ) + if ( fieldsCurrentType[pathSegment].type + && remainingPathSegments.length === 1 ) return true const nextTypeToCompare = fieldsCurrentType[pathSegment].link if ( nextTypeToCompare ) - return isValidRelationPath( recordTypes, recordTypes[nextTypeToCompare], remainingPathSegments.slice(1) ) + return isValidRelationPath( recordTypes, + recordTypes[nextTypeToCompare], + remainingPathSegments.slice(1) ) return false } -function getLastTypeInPath ( recordTypes, fieldsCurrentType, remainingPathSegments ) { +function getLastTypeInPath ( recordTypes, + fieldsCurrentType, + remainingPathSegments ) { if ( !remainingPathSegments.length ) return fieldsCurrentType @@ -573,7 +596,9 @@ function getLastTypeInPath ( recordTypes, fieldsCurrentType, remainingPathSegmen const nextType = fieldsCurrentType[pathSegment].link if ( nextType ) - return getLastTypeInPath( recordTypes, recordTypes[nextType], remainingPathSegments.slice(1) ) + return getLastTypeInPath( recordTypes, + recordTypes[nextType], + remainingPathSegments.slice(1) ) return fieldsCurrentType[pathSegment] } From 113ba98c659f5dd301de4ea7e3a9651264a7d80f Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Wed, 17 Jun 2020 18:13:44 +0200 Subject: [PATCH 6/7] bugfix exception handling --- lib/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers.js b/lib/helpers.js index 4ffd0fb..cde320a 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -385,7 +385,7 @@ function attachQueries (request) { if ( lastTypeInPath[keys.type].name !== 'String') throw new BadRequestError( `fuzzy-match only allowed on String types. - ${field} is of type ${fields[field].type.name } + ${field} is of type ${lastTypeInPath[keys.type].name} ` ) From b4b74944a6b30566a06089dc26f83eb022eaa689 Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Thu, 18 Jun 2020 17:50:24 +0200 Subject: [PATCH 7/7] added a unitest. Note: - It relies on a unrelease version of fortune.js to make it run. --- package-lock.json | 5 ++--- package.json | 2 +- test/index.js | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index d8cbe57..bf00cd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -760,9 +760,8 @@ "dev": true }, "fortune": { - "version": "5.5.17", - "resolved": "https://registry.npmjs.org/fortune/-/fortune-5.5.17.tgz", - "integrity": "sha512-xFGDev45KLW0LCIakb3hT19WQ37tE0XxKvFSa3ZcWeUglcyhrK12xLhpG2QLR3aYCi+DpKzUcYIUJu3t7pmT1A==", + "version": "git+https://github.com/cecemel/fortune.git#64a6f63874b369fc2738c2f0a81e8fe8abd691fa", + "from": "git+https://github.com/cecemel/fortune.git#64a6f63874b369fc2738c2f0a81e8fe8abd691fa", "dev": true, "requires": { "error-class": "^2.0.2", diff --git a/package.json b/package.json index 50897f8..0cf06d8 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "chalk": "^3.0.0", "eslint": "^6.8.0", "eslint-config-boss": "^1.0.6", - "fortune": "^5.5.17", + "fortune": "https://github.com/cecemel/fortune.git#64a6f63874b369fc2738c2f0a81e8fe8abd691fa", "fortune-http": "^1.2.26", "tapdance": "^5.1.1" }, diff --git a/test/index.js b/test/index.js index 71f8e19..1bb9e7c 100644 --- a/test/index.js +++ b/test/index.js @@ -332,6 +332,21 @@ run((assert, comment) => { }) }) +run((assert, comment) => { + comment(`filter fuzzy-match: Jane and John have + a common friend called something like "soft".`) + return test(`/users?${qs.stringify({ + 'filter[friends:name][fuzzy-match]': 'soft' + })}`, null, response => { + assert(validate(response.body), 'response adheres to json api') + assert(response.status === 200, 'status is correct') + assert(~response.body.links.self.indexOf('/users'), 'link is correct') + assert(deepEqual( + response.body.data.map(record => record.attributes.name).sort(), + [ 'Jane Doe', 'John Doe' ]), 'match is correct') + }) +}) + run((assert, comment) => { comment('dasherizes the camel cased fields') return test('/users/1', null, response => {