Skip to content

Commit

Permalink
refactor to wrap query builder in query(), removing need for query().…
Browse files Browse the repository at this point in the history
…finder. h/t @koskimas
  • Loading branch information
snlamm committed Aug 31, 2017
1 parent 6319b32 commit 531fe71
Showing 1 changed file with 88 additions and 56 deletions.
144 changes: 88 additions & 56 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,72 +2,77 @@ module.exports = Model => {

class FinderQueryBuilder extends Model.QueryBuilder {

get finder() {
let queryString = null

// The proxy adds a getter for a query string.
// Upon calling the proxy function, the string is parsed into `where` statements
const proxy = new Proxy((...args) => {
let whereTerm = 'where'
let offsetLetter = ''
let argCount = 0
const schema = this.modelClass().getJsonSchema()
const hasSchema = schema && schema.properties

// For queryStrings that end in 'OrFail', fail if no models are found ex. firstNameOrFail
if(queryString.slice(-6) === 'OrFail') {
this._failIfNotFound()
queryString = queryString.slice(0, -6)
}
_buildDynamicFinder(queryString) {
const buildResults = {
shouldErrorOnFail: false,
isFailedSchemaValidation: false,
searchFields: [ ]
}
let whereTerm = 'where'
let offsetLetter = ''

// Test for beginning 'or' statement ex. orFirstname
if(/^or[A-Z]/.test(queryString)) {
queryString = queryString[2].toLowerCase() + queryString.slice(3)
whereTerm = 'orWhere'
}
const schema = this.modelClass().getJsonSchema()
const hasSchema = schema && schema.properties

// Split on 'And' or 'Or', using capture groups to keep them in the result set
for(const term of queryString.split(/(?:(Or|And)([A-Z]))/)) {
if(term.length === 1) {
if(!hasSchema) {
throw new Error('Attempting to use dynamic finders without a jsonSchema. Please define it')
}

// Corrects for issue splitting on capture groups
offsetLetter = term
continue
} else if((term === 'And') || (term === 'Or')) {
whereTerm = term === 'And' ? 'where' : 'orWhere'
continue
}
// For queryStrings that end in 'OrFail', fail if no models are found ex. firstNameOrFail
if(queryString.slice(-6) === 'OrFail') {
buildResults.shouldErrorOnFail = true
queryString = queryString.slice(0, -6)
}

// Convert query string from camelCase to snake_case
const cameled = (offsetLetter.toLowerCase() + term)
const searchField = cameled.replace(/(.)([A-Z])/, '$1_$2').toLowerCase()

// If a jsonSchema is defined on the model, use it to validate that the queried fields exist
if(hasSchema) {
if((schema.properties[searchField] === void 0) && (schema.properties[cameled] === void 0)) {
throw new Error(
`Querying invalid field: ${searchField}. Please fix the query or update the jsonSchema.`
)
}
}
// Test for beginning 'or' statement ex. orFirstname
if(/^or[A-Z]/.test(queryString)) {
queryString = queryString[2].toLowerCase() + queryString.slice(3)
whereTerm = 'orWhere'
}

// Add the where() query
this[whereTerm](searchField, args[argCount ++])
// Split on 'And' or 'Or', using capture groups to keep them in the result set
for(const term of queryString.split(/(?:(Or|And)([A-Z]))/)) {
if(term.length === 1) {

// Corrects for issue splitting on capture groups
offsetLetter = term
continue
} else if((term === 'And') || (term === 'Or')) {
whereTerm = term === 'And' ? 'where' : 'orWhere'
continue
}

// returns the QueryBuilder to support further query chaining
return this
}, {
get: (object, prop) => {
queryString = prop
// Convert query string from camelCase to snake_case
const fullTerm = offsetLetter.toLowerCase() + term
const cameled = fullTerm[0].toLowerCase() + fullTerm.slice(1)
const searchField = cameled.replace(/(.)([A-Z])/, '$1_$2').toLowerCase()

// Return the proxy so it can then be called
return proxy
// If a jsonSchema is defined on the model, use it to validate that the queried fields exist.
if((schema.properties[searchField] === void 0) && (schema.properties[cameled] === void 0)) {
buildResults.isFailedSchemaValidation = true
return buildResults
}
})

// Returns the proxy to allow accces to the getter
return proxy
// Add the components for the where query
buildResults.searchFields.push([ whereTerm, searchField ])
}

return buildResults
}

_doDynamicFinder(shouldErrorOnFail, searchFields, ...args) {
if(shouldErrorOnFail) {
this._failIfNotFound()
}

let argCount = 0

// Add the where() queries
for(const [ whereTerm, searchField ] of searchFields) {
this[whereTerm](searchField, args[argCount ++])
}

return this
}

// Use throwIfNotFound on Objection >= 0.8.1. Else mimic its basic functionality.
Expand All @@ -94,5 +99,32 @@ module.exports = Model => {
return FinderQueryBuilder
}

static query(...args) {
const queryBuilder = super.query(...args)

return new Proxy(queryBuilder, {
get: (target, propKey) => {
if(target[propKey] !== void 0) {

This comment has been minimized.

Copy link
@koskimas

koskimas Aug 31, 2017

This actually should be propKey in target since some internal properties of QueryBuilder may be undefined at some point.

return target[propKey]
}

const {
isFailedSchemaValidation,
searchFields,
shouldErrorOnFail
} = target._buildDynamicFinder(propKey)

if(isFailedSchemaValidation) {
return void 0
}

return function(...args) {
return target._doDynamicFinder(shouldErrorOnFail, searchFields, ...args)
}
}
})
}

}

}

0 comments on commit 531fe71

Please sign in to comment.