Skip to content

Commit

Permalink
BREAKING Add field option if different than name
Browse files Browse the repository at this point in the history
For most, this should _not_ be a breaking change.

It's only breaking if you directly interact with the schema (i.e.,
`querier.schema`) and depend on the underlying data structures of
`filters` or `sorts`. `mapFilterFieldsToOperators` has also been renamed
to `mapFilterNamesToOperators`.
  • Loading branch information
jstayton committed Dec 3, 2020
1 parent f51163d commit c5de516
Show file tree
Hide file tree
Showing 12 changed files with 317 additions and 99 deletions.
47 changes: 31 additions & 16 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ Filtering is specified under the `filter` key in the query string. A number of
formats are supported:

```
?filter[field]=value
?filter[field][operator]=value
?filter[name]=value
?filter[name][operator]=value
```

- `operator` can be optional if the adapter specifies a default operator. If
Expand Down Expand Up @@ -187,7 +187,7 @@ In the querier's `defineSchema(schema)` function, a filter can be
added/whitelisted by calling:

```js
schema.filter(field, operatorOrOperators, (options = {}))
schema.filter(name, operatorOrOperators, (options = {}))
```

For example:
Expand All @@ -198,12 +198,18 @@ class UserQuerier extends BaseQuerier {

defineSchema(schema) {
// ...
schema.filter('id', 'in')
schema.filter('id', 'in', { field: 'users.id' })
schema.filter('status', ['=', '!='])
}
}
```

#### Options

| Name | Description | Type | Default |
| ------- | -------------------------------------------------------------------------------------- | ------ | ----------- |
| `field` | The underlying field (i.e., database column) to use if different than the filter name. | String | Filter name |

### Customizing the Query

Most of the time, you can rely on the adapter to automatically apply the
Expand All @@ -212,13 +218,13 @@ you may need to bypass the adapter and work with the query builder / ORM
directly.

This can easily be done by defining a function in your querier class to handle
the `field[operator]` combination. For example:
the `name[operator]` combination. For example:

```js
class UserQuerier extends BaseQuerier {
// ...

'filter:id[in]'(builder, { field, operator, value }) {
'filter:id[in]'(builder, { name, field, operator, value }) {
return builder.where(field, operator, value)
}
}
Expand All @@ -229,7 +235,7 @@ As you can see, you simply call the appropriate function(s) on the query builder

Now, this example is overly simplistic, and probably already handled
appropriately by the adapter. It becomes more useful, for example, when you have
a field that doesn't map directly to a field in your database, like a search
a filter that doesn't map directly to a field in your database, like a search
query:

```js
Expand Down Expand Up @@ -276,6 +282,7 @@ class UserQuerier extends BaseQuerier {

get filterDefaults() {
return {
name: null,
field: null,
operator: null,
value: null,
Expand All @@ -294,13 +301,13 @@ Sorting is specified under the `sort` key in the query string. A number of
formats are supported:

```
?sort=field
?sort[]=field
?sort[field]=order
?sort=name
?sort[]=name
?sort[name]=order
```

- `order` can be `asc` or `desc` (case-insensitive), and defaults to `asc`.
- `sort[]` and `sort[field]` support multiple fields, just be aware that the two
- `sort[]` and `sort[name]` support multiple sorts, just be aware that the two
formats can't be mixed.

### Defining the Schema
Expand All @@ -309,7 +316,7 @@ In the querier's `defineSchema(schema)` function, a sort can be
added/whitelisted by calling:

```js
schema.sort(field, (options = {}))
schema.sort(name, (options = {}))
```

For example:
Expand All @@ -321,10 +328,17 @@ class UserQuerier extends BaseQuerier {
defineSchema(schema) {
// ...
schema.sort('name')
schema.sort('status', { field: 'current_status' })
}
}
```

#### Options

| Name | Description | Type | Default |
| ------- | ------------------------------------------------------------------------------------ | ------ | --------- |
| `field` | The underlying field (i.e., database column) to use if different than the sort name. | String | Sort name |

### Customizing the Query

Most of the time, you can rely on the adapter to automatically apply the
Expand All @@ -333,13 +347,13 @@ you may need to bypass the adapter and work with the query builder / ORM
directly.

This can easily be done by defining a function in your querier class to handle
the `field`. For example:
the `name`. For example:

```js
class UserQuerier extends BaseQuerier {
// ...

'sort:name'(builder, { field, order }) {
'sort:name'(builder, { name, field, order }) {
return builder.orderBy(field, order)
}
}
Expand All @@ -350,7 +364,7 @@ As you can see, you simply call the appropriate function(s) on the query builder

Now, this example is overly simplistic, and probably already handled
appropriately by the adapter. It becomes more useful, for example, when you have
a field that doesn't map directly to a field in your database:
a sort that doesn't map directly to a field in your database:

```js
class UserQuerier extends BaseQuerier {
Expand Down Expand Up @@ -392,6 +406,7 @@ class UserQuerier extends BaseQuerier {

get sortDefaults() {
return {
name: null,
field: null,
order: 'asc',
}
Expand Down Expand Up @@ -481,7 +496,7 @@ You only need to return the keys you want to override.

QueryQL and the configured adapter validate the query structure and value types
for free, without any additional configuration. You don't have to worry about
the client misspelling a field name or using an unsupported filter operator – a
the client misspelling a name or using an unsupported filter operator – a
`ValidationError` will be thrown if they do.

Still, it's often helpful to add your own app-specific validation. For example,
Expand Down
4 changes: 2 additions & 2 deletions src/adapters/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ class BaseAdapter {
throw new NotImplementedError()
}

'filter:*'(/* builder, { field, operator, value } */) {
'filter:*'(/* builder, { name, field, operator, value } */) {
throw new NotImplementedError()
}

sort(/* builder, { field, order } */) {
sort(/* builder, { name, field, order } */) {
throw new NotImplementedError()
}

Expand Down
45 changes: 28 additions & 17 deletions src/parsers/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@ const flattenMap = require('../services/flatten_map')
class FilterParser extends BaseParser {
static get DEFAULTS() {
return {
name: null,
field: null,
operator: null,
value: null,
}
}

buildKey({ field, operator }) {
return `${this.queryKey}:${field}[${operator}]`
buildKey({ name, operator }) {
return `${this.queryKey}:${name}[${operator}]`
}

defineValidation(schema) {
const defaultOperator = this.defaults.operator
const mapFieldsToOperators = Object.entries(
this.schema.mapFilterFieldsToOperators()
const mapNamesToOperators = Object.entries(
this.schema.mapFilterNamesToOperators()
)

const values = [
Expand All @@ -32,7 +33,7 @@ class FilterParser extends BaseParser {
]

return schema.object().keys(
mapFieldsToOperators.reduce((accumulator, [field, operators]) => {
mapNamesToOperators.reduce((accumulator, [field, operators]) => {
const operatorObject = schema
.object()
.pattern(schema.string().valid(...operators), values)
Expand All @@ -54,19 +55,29 @@ class FilterParser extends BaseParser {
})
}

parseObject(field, value) {
return Object.keys(value).map((operator) => ({
...this.defaults,
field,
operator,
value: value[operator],
}))
parseObject(name, value) {
return Object.keys(value).map((operator) => {
const { options } = this.schema.filters.get(`${name}[${operator}]`)

return {
...this.defaults,
name,
field: options.field || name,
operator,
value: value[operator],
}
})
}

parseNonObject(field, value) {
parseNonObject(name, value) {
const { options } = this.schema.filters.get(
`${name}[${this.defaults.operator}]`
)

return {
...this.defaults,
field,
name,
field: options.field || name,
value,
}
}
Expand All @@ -81,11 +92,11 @@ class FilterParser extends BaseParser {
const entries = Object.entries(this.query)
const filters = []

for (const [field, value] of entries) {
for (const [name, value] of entries) {
if (is.object(value)) {
filters.push(...this.parseObject(field, value))
filters.push(...this.parseObject(name, value))
} else {
filters.push(this.parseNonObject(field, value))
filters.push(this.parseNonObject(name, value))
}
}

Expand Down
42 changes: 28 additions & 14 deletions src/parsers/sort.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ const flattenMap = require('../services/flatten_map')
class SortParser extends BaseParser {
static get DEFAULTS() {
return {
name: null,
field: null,
order: 'asc',
}
}

buildKey({ field }) {
return `${this.queryKey}:${field}`
buildKey({ name }) {
return `${this.queryKey}:${name}`
}

defineValidation(schema) {
Expand Down Expand Up @@ -44,26 +45,39 @@ class SortParser extends BaseParser {
})
}

parseString(field) {
parseString(name) {
const { options } = this.schema.sorts.get(name)

return {
...this.defaults,
field,
name,
field: options.field || name,
}
}

parseArray(fields) {
return fields.map((field) => ({
...this.defaults,
field,
}))
parseArray(names) {
return names.map((name) => {
const { options } = this.schema.sorts.get(name)

return {
...this.defaults,
name,
field: options.field || name,
}
})
}

parseObject(query) {
return Object.entries(query).map(([field, order]) => ({
...this.defaults,
field,
order,
}))
return Object.entries(query).map(([name, order]) => {
const { options } = this.schema.sorts.get(name)

return {
...this.defaults,
name,
field: options.field || name,
order,
}
})
}

parse() {
Expand Down
20 changes: 10 additions & 10 deletions src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ class Schema {
this.page(false)
}

filter(field, operatorOrOperators, options = {}) {
filter(name, operatorOrOperators, options = {}) {
const operators = Array.isArray(operatorOrOperators)
? operatorOrOperators
: [operatorOrOperators]

for (const operator of operators) {
this.filters.set(`${field}[${operator}]`, {
field,
this.filters.set(`${name}[${operator}]`, {
name,
operator,
options,
})
Expand All @@ -23,9 +23,9 @@ class Schema {
return this
}

sort(field, options = {}) {
this.sorts.set(field, {
field,
sort(name, options = {}) {
this.sorts.set(name, {
name,
options,
})

Expand All @@ -48,15 +48,15 @@ class Schema {
return this
}

mapFilterFieldsToOperators() {
mapFilterNamesToOperators() {
const filters = Array.from(this.filters.values())

return filters.reduce((accumulator, filter) => {
if (!accumulator[filter.field]) {
accumulator[filter.field] = []
if (!accumulator[filter.name]) {
accumulator[filter.name] = []
}

accumulator[filter.field].push(filter.operator)
accumulator[filter.name].push(filter.operator)

return accumulator
}, {})
Expand Down
Loading

1 comment on commit c5de516

@nampas
Copy link

@nampas nampas commented on c5de516 Jan 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jstayton Thanks for this! We had a bunch of custom code accomplishing the same thing, happy to rip it all out now :)

Please sign in to comment.