Skip to content

Commit d721070

Browse files
authored
Feature/wip filtering relationship (#73)
* 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. * WIP: first version filtering relationships * abstract away filter parsing. * fix with auto lint fix * further linting tests * bugfix exception handling * added a unitest. Note: - It relies on a unrelease version of fortune.js to make it run.
1 parent 70db2c2 commit d721070

File tree

4 files changed

+117
-9
lines changed

4 files changed

+117
-9
lines changed

lib/helpers.js

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ function attachQueries (request) {
305305
request.options = {}
306306

307307
// Iterate over dynamic query strings.
308-
for (const parameter of Object.keys(query)) {
308+
for (const parameter of Object.keys(query))
309309
// Attach fields option.
310310
if (parameter.match(isField)) {
311311
const sparseField = Array.isArray(query[parameter]) ?
@@ -330,10 +330,28 @@ function attachQueries (request) {
330330
const field = inflect(matches[1])
331331
const filterType = matches[2]
332332

333-
if (!(field in fields)) throw new BadRequestError(
334-
`The field "${field}" is non-existent.`)
333+
if (isRelationFilter(field)) {
334+
const relationPath = field
335+
if (filterType !== 'fuzzy-match')
336+
throw new BadRequestError(
337+
`Filtering relationship only
338+
supported on fuzzy-match for now
339+
`)
340+
341+
const isValidPath =
342+
isValidRelationPath( recordTypes,
343+
fields,
344+
getRelationFilterSegments(field) )
345+
if (! isValidPath )
346+
throw new BadRequestError(`Path ${relationPath} is not valid`)
347+
}
348+
349+
else if (!(field in fields))
350+
throw new BadRequestError(`The field "${field}" is non-existent.`)
335351

336-
const fieldType = fields[field][keys.type]
352+
const filterSegments = getRelationFilterSegments(field)
353+
const fieldType = getLastTypeInPath( recordTypes,
354+
fields, filterSegments )[keys.type]
337355

338356
if (filterType === void 0) {
339357
if (!('match' in request.options)) request.options.match = {}
@@ -354,10 +372,31 @@ function attachQueries (request) {
354372
request.options.range[field][index] =
355373
castValue(query[parameter], fieldType, options)
356374
}
375+
else if (filterType === 'fuzzy-match') {
376+
const lastTypeInPath =
377+
getLastTypeInPath( recordTypes,
378+
fields,
379+
getRelationFilterSegments(field) )
380+
if ( ! lastTypeInPath[keys.type] )
381+
throw new BadRequestError(
382+
`fuzzy-match only allowed on attributes. For ${field}` )
383+
384+
385+
if ( lastTypeInPath[keys.type].name !== 'String')
386+
throw new BadRequestError(
387+
`fuzzy-match only allowed on String types.
388+
${field} is of type ${lastTypeInPath[keys.type].name}
389+
` )
390+
391+
392+
if (!('fuzzyMatch' in request.options))
393+
request.options['fuzzyMatch'] = {}
394+
request.options.fuzzyMatch[field] = query[parameter]
395+
}
357396
else throw new BadRequestError(
358397
`The filter "${filterType}" is not valid.`)
359398
}
360-
}
399+
361400

362401
// Attach include option.
363402
if (reservedKeys.include in query) {
@@ -516,3 +555,58 @@ function setInflectType (inflect, types) {
516555

517556
return out
518557
}
558+
559+
function isValidRelationPath ( recordTypes,
560+
fieldsCurrentType,
561+
remainingPathSegments ) {
562+
if ( !remainingPathSegments.length )
563+
return false
564+
565+
const pathSegment = remainingPathSegments[0]
566+
567+
if ( !fieldsCurrentType[pathSegment] )
568+
return false
569+
570+
if ( fieldsCurrentType[pathSegment].type
571+
&& remainingPathSegments.length === 1 )
572+
return true
573+
574+
575+
const nextTypeToCompare = fieldsCurrentType[pathSegment].link
576+
if ( nextTypeToCompare )
577+
return isValidRelationPath( recordTypes,
578+
recordTypes[nextTypeToCompare],
579+
remainingPathSegments.slice(1) )
580+
581+
return false
582+
}
583+
584+
function getLastTypeInPath ( recordTypes,
585+
fieldsCurrentType,
586+
remainingPathSegments ) {
587+
if ( !remainingPathSegments.length )
588+
return fieldsCurrentType
589+
590+
591+
const pathSegment = remainingPathSegments[0]
592+
593+
if ( !fieldsCurrentType[pathSegment] )
594+
return {}
595+
596+
597+
const nextType = fieldsCurrentType[pathSegment].link
598+
if ( nextType )
599+
return getLastTypeInPath( recordTypes,
600+
recordTypes[nextType],
601+
remainingPathSegments.slice(1) )
602+
603+
return fieldsCurrentType[pathSegment]
604+
}
605+
606+
function isRelationFilter ( field ) {
607+
return field.split(':').length > 1
608+
}
609+
610+
function getRelationFilterSegments ( field ) {
611+
return field.split(':')
612+
}

package-lock.json

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"chalk": "^3.0.0",
2525
"eslint": "^6.8.0",
2626
"eslint-config-boss": "^1.0.6",
27-
"fortune": "^5.5.17",
27+
"fortune": "https://github.com/cecemel/fortune.git#64a6f63874b369fc2738c2f0a81e8fe8abd691fa",
2828
"fortune-http": "^1.2.26",
2929
"tapdance": "^5.1.1"
3030
},

test/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,21 @@ run((assert, comment) => {
332332
})
333333
})
334334

335+
run((assert, comment) => {
336+
comment(`filter fuzzy-match: Jane and John have
337+
a common friend called something like "soft".`)
338+
return test(`/users?${qs.stringify({
339+
'filter[friends:name][fuzzy-match]': 'soft'
340+
})}`, null, response => {
341+
assert(validate(response.body), 'response adheres to json api')
342+
assert(response.status === 200, 'status is correct')
343+
assert(~response.body.links.self.indexOf('/users'), 'link is correct')
344+
assert(deepEqual(
345+
response.body.data.map(record => record.attributes.name).sort(),
346+
[ 'Jane Doe', 'John Doe' ]), 'match is correct')
347+
})
348+
})
349+
335350
run((assert, comment) => {
336351
comment('dasherizes the camel cased fields')
337352
return test('/users/1', null, response => {

0 commit comments

Comments
 (0)