diff --git a/lib/helpers.js b/lib/helpers.js index 62623b7..cde320a 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,10 +330,28 @@ 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 (isRelationFilter(field)) { + const relationPath = field + if (filterType !== 'fuzzy-match') + 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 = fields[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 = {} @@ -354,10 +372,31 @@ function attachQueries (request) { request.options.range[field][index] = castValue(query[parameter], fieldType, options) } + else if (filterType === 'fuzzy-match') { + const lastTypeInPath = + getLastTypeInPath( recordTypes, + fields, + getRelationFilterSegments(field) ) + if ( ! lastTypeInPath[keys.type] ) + throw new BadRequestError( + `fuzzy-match only allowed on attributes. For ${field}` ) + + + if ( lastTypeInPath[keys.type].name !== 'String') + throw new BadRequestError( + `fuzzy-match only allowed on String types. + ${field} is of type ${lastTypeInPath[keys.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.`) } - } + // Attach include option. if (reservedKeys.include in query) { @@ -516,3 +555,58 @@ 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] +} + +function isRelationFilter ( field ) { + return field.split(':').length > 1 +} + +function getRelationFilterSegments ( field ) { + return field.split(':') +} 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 => {