From e80461544faf7ca7e8f46ff9a60cf6fa1f75a1e9 Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Fri, 8 May 2020 18:45:18 +0200 Subject: [PATCH 1/6] Attempt to allow fuzzy-match filter in fortune-postgres Notes ----- I am unsure what field of options may be used. The options.query already seemed to be used, so started looking for another field. As I understand it, Adapter.find leaves the possibility of extra fields on the options object unspecified. Therefore, I added the fuzzyMatch field. --- lib/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/index.js b/lib/index.js index 865e963..6388ebd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -310,6 +310,14 @@ module.exports = Adapter => class PostgreSQLAdapter extends Adapter { } } + for (const field in options.fuzzyMatch) { + let value = options.fuzzyMatch[field] + //We assume the request has been validaded on another level. (so only strings, no arrays, etc...) + index++ + parameters.push(inputValue(value)) + where.push(`"${field}" ~* $${index}`) + } + where = where.length ? `where ${where.join(' and ')}` : '' for (const field in options.sort) { From d48765069fd079333e2e3d5b47bdceac3e456d62 Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Wed, 13 May 2020 17:19:15 +0200 Subject: [PATCH 2/6] WIP: fuzzy-matching on relationship As a try out, I extend the fuzzy matching on relationship - Abstracted away relation filter parsing - Change filter[author.posts] to filter[author:posts]. Ember (considered baseline use case) doesn't like parsing the former format in its queryParams parser. --- lib/helpers.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++++- lib/index.js | 38 +++++++++++++++++++++++++--- 2 files changed, 100 insertions(+), 5 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 6807b59..9f0459e 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -21,7 +21,7 @@ const postgresTypeMap = new WeakMap([ module.exports = { primaryKeyTypes, postgresTypeMap, inputValue, - inputRecord, outputRecord, getCode + inputRecord, outputRecord, getCode, constructFilterRelationQuery, constructTypesPathToChild, isRelationFilter, getRelationFilterSegments } @@ -117,3 +117,68 @@ function outputBuffer (value) { if (Buffer.isBuffer(value)) return value return buffer(value.slice(2), 'hex') } + + +function constructFilterRelationQuery( typeMap, + recordTypes, + typesPathToParent, + pathSegmentsToParent, + whereOperation, + value, + query = '' ){ + + if( !typesPathToParent.length ){ + return query + } + + const pathSegment = pathSegmentsToParent[0] + const currentType = typesPathToParent[0] + if(!query){ + //TODO: can I always assume id here? + query = `SELECT id FROM "${typeMap[currentType] || currentType}" WHERE ${whereOperation(pathSegment, value)} \n` + } + else { + const recordType = recordTypes[currentType] + if(!recordType[pathSegment].isArray) { + query = `SELECT id FROM "${typeMap[currentType] || currentType}" WHERE "${pathSegment}" IN ( ${query} ) \n` + } + else{ + query = `SELECT id FROM "${typeMap[currentType] || currentType}" WHERE "${pathSegment}" && ARRAY( ${query} ) \n` + } + } + + return constructFilterRelationQuery( typeMap, + recordTypes, + typesPathToParent.slice(1), + pathSegmentsToParent.slice(1), + whereOperation, + value, + query) +} + + +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 isRelationFilter( field ){ + return field.split(':').length > 1 +} + + +function getRelationFilterSegments( field ){ + return field.split(':') +} diff --git a/lib/index.js b/lib/index.js index 6388ebd..9547bf0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -312,10 +312,40 @@ module.exports = Adapter => class PostgreSQLAdapter extends Adapter { for (const field in options.fuzzyMatch) { let value = options.fuzzyMatch[field] - //We assume the request has been validaded on another level. (so only strings, no arrays, etc...) - index++ - parameters.push(inputValue(value)) - where.push(`"${field}" ~* $${index}`) + //simple filter + if( ! helpers.isRelationFilter(field) ){ + //We assume the request has been validaded on another level. (so only strings, no arrays, etc...) + index++ + parameters.push(inputValue(value)) + where.push(`"${field}" ~* $${index}`) + } + //relationship filter + else { + const whereOperation = (field, value) => { + index++ + parameters.push(inputValue(value)) + return `"${field}" ~* $${index}` + } + const relationFilterSegments = helpers.getRelationFilterSegments(field); + const typesPath = helpers.constructTypesPathToChild( recordTypes, recordTypes[type], relationFilterSegments, [] ) + const query = helpers.constructFilterRelationQuery( typeMap, + recordTypes, + typesPath.slice().reverse(), + relationFilterSegments.slice().reverse(), + whereOperation, + value, + '' + ) + const startField = relationFilterSegments[0] + const isArray = fields[startField][isArrayKey] + if(isArray){ + where.push(`"${startField}" && ARRAY(${query})`) + } + else { + where.push(`"${startField}" in (${query})`) + } + + } } where = where.length ? `where ${where.join(' and ')}` : '' From ecb8a84a8b183dd949bb302893955f9508685b7e Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Wed, 17 Jun 2020 12:57:38 +0200 Subject: [PATCH 3/6] Make sure actual table setup is used to construct queries See PR discussion remark https://github.com/fortunejs/fortune-postgres/pull/35#discussion_r429749827 --- lib/helpers.js | 14 ++++++++------ lib/index.js | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 9f0459e..71ad3ae 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -119,7 +119,8 @@ function outputBuffer (value) { } -function constructFilterRelationQuery( typeMap, +function constructFilterRelationQuery( tableSetupConfiguration, + typeMap, recordTypes, typesPathToParent, pathSegmentsToParent, @@ -131,23 +132,24 @@ function constructFilterRelationQuery( typeMap, return query } + const primaryKey = tableSetupConfiguration.keys.primary const pathSegment = pathSegmentsToParent[0] const currentType = typesPathToParent[0] if(!query){ - //TODO: can I always assume id here? - query = `SELECT id FROM "${typeMap[currentType] || currentType}" WHERE ${whereOperation(pathSegment, value)} \n` + query = `SELECT ${primaryKey} FROM "${typeMap[currentType] || currentType}" WHERE ${whereOperation(pathSegment, value)} \n` } else { const recordType = recordTypes[currentType] if(!recordType[pathSegment].isArray) { - query = `SELECT id FROM "${typeMap[currentType] || currentType}" WHERE "${pathSegment}" IN ( ${query} ) \n` + query = `SELECT ${primaryKey} FROM "${typeMap[currentType] || currentType}" WHERE "${pathSegment}" IN ( ${query} ) \n` } else{ - query = `SELECT id FROM "${typeMap[currentType] || currentType}" WHERE "${pathSegment}" && ARRAY( ${query} ) \n` + query = `SELECT ${primaryKey} FROM "${typeMap[currentType] || currentType}" WHERE "${pathSegment}" && ARRAY( ${query} ) \n` } } - return constructFilterRelationQuery( typeMap, + return constructFilterRelationQuery( tableSetupConfiguration, + typeMap, recordTypes, typesPathToParent.slice(1), pathSegmentsToParent.slice(1), diff --git a/lib/index.js b/lib/index.js index 9547bf0..a86fab4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -328,7 +328,8 @@ module.exports = Adapter => class PostgreSQLAdapter extends Adapter { } const relationFilterSegments = helpers.getRelationFilterSegments(field); const typesPath = helpers.constructTypesPathToChild( recordTypes, recordTypes[type], relationFilterSegments, [] ) - const query = helpers.constructFilterRelationQuery( typeMap, + const query = helpers.constructFilterRelationQuery( { keys: this.keys }, + typeMap, recordTypes, typesPath.slice().reverse(), relationFilterSegments.slice().reverse(), From 473b20fb4d823ef4d23e307f902d6edf224fc398 Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Wed, 17 Jun 2020 14:40:37 +0200 Subject: [PATCH 4/6] Concised version of if/else branch see: https://github.com/fortunejs/fortune-postgres/pull/35#discussion_r429750241 --- lib/helpers.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 71ad3ae..87d01f5 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -140,12 +140,8 @@ function constructFilterRelationQuery( tableSetupConfiguration, } else { const recordType = recordTypes[currentType] - if(!recordType[pathSegment].isArray) { - query = `SELECT ${primaryKey} FROM "${typeMap[currentType] || currentType}" WHERE "${pathSegment}" IN ( ${query} ) \n` - } - else{ - query = `SELECT ${primaryKey} FROM "${typeMap[currentType] || currentType}" WHERE "${pathSegment}" && ARRAY( ${query} ) \n` - } + const isArray = recordType[pathSegment].isArray; + query = `SELECT ${primaryKey} FROM "${typeMap[currentType] || currentType}" WHERE "${pathSegment}" ${isArray ? '&& ARRAY' : 'IN'} ( ${query} ) \n` } return constructFilterRelationQuery( tableSetupConfiguration, From 36539eace2b920b3218044e65ceda6d3b05f02b4 Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Wed, 17 Jun 2020 15:10:24 +0200 Subject: [PATCH 5/6] automated linter fixes --- lib/helpers.js | 27 +++++++++++++-------------- lib/index.js | 14 ++++++-------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 87d01f5..b96dc0f 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -119,28 +119,27 @@ function outputBuffer (value) { } -function constructFilterRelationQuery( tableSetupConfiguration, +function constructFilterRelationQuery ( tableSetupConfiguration, typeMap, recordTypes, typesPathToParent, pathSegmentsToParent, whereOperation, value, - query = '' ){ - - if( !typesPathToParent.length ){ + query = '' ) { + if ( !typesPathToParent.length ) return query - } + const primaryKey = tableSetupConfiguration.keys.primary const pathSegment = pathSegmentsToParent[0] const currentType = typesPathToParent[0] - if(!query){ + if (!query) query = `SELECT ${primaryKey} FROM "${typeMap[currentType] || currentType}" WHERE ${whereOperation(pathSegment, value)} \n` - } + else { const recordType = recordTypes[currentType] - const isArray = recordType[pathSegment].isArray; + const isArray = recordType[pathSegment].isArray query = `SELECT ${primaryKey} FROM "${typeMap[currentType] || currentType}" WHERE "${pathSegment}" ${isArray ? '&& ARRAY' : 'IN'} ( ${query} ) \n` } @@ -155,16 +154,16 @@ function constructFilterRelationQuery( tableSetupConfiguration, } -function constructTypesPathToChild( recordTypes, parent, remainingPathSegments, typesPath ){ - if( !remainingPathSegments.length ){ +function constructTypesPathToChild ( recordTypes, parent, remainingPathSegments, typesPath ) { + if ( !remainingPathSegments.length ) return typesPath - } + const segment = remainingPathSegments[0] const nextType = parent[segment].link //complex type - if( nextType ){ + if ( nextType ) { typesPath.push( nextType ) parent = recordTypes[nextType] } @@ -172,11 +171,11 @@ function constructTypesPathToChild( recordTypes, parent, remainingPathSegments, } -function isRelationFilter( field ){ +function isRelationFilter ( field ) { return field.split(':').length > 1 } -function getRelationFilterSegments( field ){ +function getRelationFilterSegments ( field ) { return field.split(':') } diff --git a/lib/index.js b/lib/index.js index a86fab4..07239c1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -311,9 +311,9 @@ module.exports = Adapter => class PostgreSQLAdapter extends Adapter { } for (const field in options.fuzzyMatch) { - let value = options.fuzzyMatch[field] + const value = options.fuzzyMatch[field] //simple filter - if( ! helpers.isRelationFilter(field) ){ + if ( ! helpers.isRelationFilter(field) ) { //We assume the request has been validaded on another level. (so only strings, no arrays, etc...) index++ parameters.push(inputValue(value)) @@ -326,7 +326,7 @@ module.exports = Adapter => class PostgreSQLAdapter extends Adapter { parameters.push(inputValue(value)) return `"${field}" ~* $${index}` } - const relationFilterSegments = helpers.getRelationFilterSegments(field); + const relationFilterSegments = helpers.getRelationFilterSegments(field) const typesPath = helpers.constructTypesPathToChild( recordTypes, recordTypes[type], relationFilterSegments, [] ) const query = helpers.constructFilterRelationQuery( { keys: this.keys }, typeMap, @@ -339,13 +339,11 @@ module.exports = Adapter => class PostgreSQLAdapter extends Adapter { ) const startField = relationFilterSegments[0] const isArray = fields[startField][isArrayKey] - if(isArray){ + if (isArray) where.push(`"${startField}" && ARRAY(${query})`) - } - else { - where.push(`"${startField}" in (${query})`) - } + else + where.push(`"${startField}" in (${query})`) } } From 6b14637d451dd51e9a3fedcc154439e23d08e131 Mon Sep 17 00:00:00 2001 From: Felix Ruiz de Arcaute Date: Wed, 17 Jun 2020 17:23:03 +0200 Subject: [PATCH 6/6] fix linting --- lib/helpers.js | 21 ++++++++++++++++----- lib/index.js | 33 ++++++++++++++++++++++----------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index b96dc0f..24a7a4c 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -21,7 +21,8 @@ const postgresTypeMap = new WeakMap([ module.exports = { primaryKeyTypes, postgresTypeMap, inputValue, - inputRecord, outputRecord, getCode, constructFilterRelationQuery, constructTypesPathToChild, isRelationFilter, getRelationFilterSegments + inputRecord, outputRecord, getCode, constructFilterRelationQuery, + constructTypesPathToChild, isRelationFilter, getRelationFilterSegments } @@ -135,12 +136,20 @@ function constructFilterRelationQuery ( tableSetupConfiguration, const pathSegment = pathSegmentsToParent[0] const currentType = typesPathToParent[0] if (!query) - query = `SELECT ${primaryKey} FROM "${typeMap[currentType] || currentType}" WHERE ${whereOperation(pathSegment, value)} \n` + query = ` + SELECT ${primaryKey} FROM "${typeMap[currentType] || currentType}" + WHERE ${whereOperation(pathSegment, value)} \n + ` + else { const recordType = recordTypes[currentType] const isArray = recordType[pathSegment].isArray - query = `SELECT ${primaryKey} FROM "${typeMap[currentType] || currentType}" WHERE "${pathSegment}" ${isArray ? '&& ARRAY' : 'IN'} ( ${query} ) \n` + query = `SELECT ${primaryKey} + FROM "${typeMap[currentType] || currentType}" + WHERE "${pathSegment}" + ${isArray ? '&& ARRAY' : 'IN'} ( ${query} ) \n + ` } return constructFilterRelationQuery( tableSetupConfiguration, @@ -154,7 +163,8 @@ function constructFilterRelationQuery ( tableSetupConfiguration, } -function constructTypesPathToChild ( recordTypes, parent, remainingPathSegments, typesPath ) { +function constructTypesPathToChild ( recordTypes, parent, + remainingPathSegments, typesPath ) { if ( !remainingPathSegments.length ) return typesPath @@ -167,7 +177,8 @@ function constructTypesPathToChild ( recordTypes, parent, remainingPathSegments, typesPath.push( nextType ) parent = recordTypes[nextType] } - return constructTypesPathToChild(recordTypes, parent, remainingPathSegments.slice(1), typesPath ) + return constructTypesPathToChild(recordTypes, parent, + remainingPathSegments.slice(1), typesPath ) } diff --git a/lib/index.js b/lib/index.js index 07239c1..39324d9 100644 --- a/lib/index.js +++ b/lib/index.js @@ -314,29 +314,40 @@ module.exports = Adapter => class PostgreSQLAdapter extends Adapter { const value = options.fuzzyMatch[field] //simple filter if ( ! helpers.isRelationFilter(field) ) { - //We assume the request has been validaded on another level. (so only strings, no arrays, etc...) + // We assume the request has been validaded on another level. + // (so only strings, no arrays, etc...) index++ parameters.push(inputValue(value)) where.push(`"${field}" ~* $${index}`) } //relationship filter else { + // eslint-disable-next-line no-loop-func const whereOperation = (field, value) => { index++ parameters.push(inputValue(value)) return `"${field}" ~* $${index}` } const relationFilterSegments = helpers.getRelationFilterSegments(field) - const typesPath = helpers.constructTypesPathToChild( recordTypes, recordTypes[type], relationFilterSegments, [] ) - const query = helpers.constructFilterRelationQuery( { keys: this.keys }, - typeMap, - recordTypes, - typesPath.slice().reverse(), - relationFilterSegments.slice().reverse(), - whereOperation, - value, - '' - ) + + const typesPath = helpers + .constructTypesPathToChild( recordTypes, + recordTypes[type], + relationFilterSegments, + [] ) + + const segmentsInversed = relationFilterSegments.slice().reverse() + const query = helpers + .constructFilterRelationQuery( { keys: this.keys }, + typeMap, + recordTypes, + typesPath.slice().reverse(), + segmentsInversed, + whereOperation, + value, + '' + ) + const startField = relationFilterSegments[0] const isArray = fields[startField][isArrayKey] if (isArray)