Skip to content

Commit

Permalink
feat(schema): Allow to add additional operators to the query syntax (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl authored Dec 20, 2022
1 parent 9cea1a2 commit f324940
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 104 deletions.
91 changes: 63 additions & 28 deletions docs/api/schema/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,69 @@ export type Message = FromSchema<
Schema ships with a few helpers to automatically create schemas that comply with the [Feathers query syntax](../databases/querying.md) (like `$gt`, `$ne` etc.):
### querySyntax
`querySyntax(schema.properties, extensions)` initializes all properties the additional query syntax properties `$limit`, `$skip`, `$select` and `$sort`. `$select` and `$sort` will be typed so they only allow existing schema properties.
```ts
import { querySyntax } from '@feathersjs/schema'
import type { FromSchema } from '@feathersjs/schema'

export const userQuerySchema = {
$id: 'UserQuery',
type: 'object',
additionalProperties: false,
properties: {
...querySyntax(userSchema.properties)
}
} as const

export type UserQuery = FromSchema<typeof userQuerySchema>

const userQuery: UserQuery = {
$limit: 10,
$select: ['email', 'id'],
$sort: {
email: 1
}
}
```

Additional properties like `$ilike` can be added to the query syntax like this:

```ts
import { querySyntax } from '@feathersjs/schema'
import type { FromSchema } from '@feathersjs/schema'

export const userQuerySchema = {
$id: 'UserQuery',
type: 'object',
additionalProperties: false,
properties: {
...querySyntax(userSchema.properties, {
email: {
$ilike: {
type: 'string'
}
}
} as const)
}
} as const

export type UserQuery = FromSchema<typeof userQuerySchema>

const userQuery: UserQuery = {
$limit: 10,
$select: ['email', 'id'],
$sort: {
email: 1
},
email: {
$ilike: '%@example.com'
}
}
```

### queryProperty

`queryProperty` helper takes a definition for a single property and returns a schema that allows the default query operators. This helper supports the operators listed, below. Learn what each one means in the [common query operator](/api/databases/querying#operators) documentation.
Expand Down Expand Up @@ -182,34 +245,6 @@ You can learn how it works, [here](https://github.com/feathersjs/feathers/blob/d

`queryProperties(schema.properties)` takes the all properties of a schema and converts them into query schema properties (using `queryProperty`)

### querySyntax

`querySyntax(schema.properties)` initializes all properties the additional query syntax properties `$limit`, `$skip`, `$select` and `$sort`. `$select` and `$sort` will be typed so they only allow existing schema properties.

```ts
import { querySyntax } from '@feathersjs/schema'
import type { FromSchema } from '@feathersjs/schema'

export const userQuerySchema = {
$id: 'UserQuery',
type: 'object',
additionalProperties: false,
properties: {
...querySyntax(userSchema.properties)
}
} as const

export type UserQuery = FromSchema<typeof userQuerySchema>

const userQuery: UserQuery = {
$limit: 10,
$select: ['email', 'id'],
$sort: {
email: 1
}
}
```

## Validators

The following functions are available to get a [validator function](./validators.md) from a JSON schema definition.
Expand Down
39 changes: 28 additions & 11 deletions docs/api/schema/typebox.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
---
outline: deep
---

# TypeBox

`@feathersjs/typebox` allows to define JSON schemas with [TypeBox](https://github.com/sinclairzx81/typebox), a JSON schema type builder with static type resolution for TypeScript.
Expand Down Expand Up @@ -1482,7 +1478,7 @@ type MessageData = Static<typeof messageDataSchema>
### querySyntax
`querySyntax(definition)` returns a schema to validate the [Feathers query syntax](../databases/querying.md) for all properties in a TypeBox definition.
`querySyntax(definition, extensions, options)` returns a schema to validate the [Feathers query syntax](../databases/querying.md) for all properties in a TypeBox definition.
```ts
import { querySyntax } from '@feathersjs/typebox'
Expand All @@ -1496,7 +1492,7 @@ const messageQuerySchema = querySyntax(messageQueryProperties)
type MessageQuery = Static<typeof messageQuerySchema>
```
To allow additional query parameters for properties you can create a union type:
To allow additional query parameters like `$ilike`, `$regex` etc. for properties you can pass an object with the property names and additional types:
```ts
import { querySyntax } from '@feathersjs/typebox'
Expand All @@ -1505,14 +1501,35 @@ import { querySyntax } from '@feathersjs/typebox'
const messageQueryProperties = Type.Pick(messageSchema, ['id', 'text', 'createdAt', 'userId'], {
additionalProperties: false
})
const messageQuerySchema = Type.Union(
const messageQuerySchema = Type.Intersect(
[
// This will additioanlly allow querying for `{ name: { $ilike: 'Dav%' } }`
querySyntax(messageQueryProperties, {
name: {
$ilike: Type.String()
}
}),
// Add additional query properties here
Type.Object({})
],
{ additionalProperties: false }
)
```

To allow additional query properties outside of the query syntax use the intersection type:

```ts
import { querySyntax } from '@feathersjs/typebox'

// Schema for allowed query properties
const messageQueryProperties = Type.Pick(messageSchema, ['id', 'text', 'createdAt', 'userId'], {
additionalProperties: false
})
const messageQuerySchema = Type.Intersect(
[
querySyntax(messageQueryProperties),
// Allow to also query for `{ name: { $ilike: '%something' } }`
Type.Object({
name: Type.Object({
$ilike: Type.String()
})
isActive: Type.Boolean()
})
],
{ additionalProperties: false }
Expand Down
49 changes: 31 additions & 18 deletions packages/schema/src/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const getDataValidator = (
}
}

export type PropertyQuery<D extends JSONSchema> = {
export type PropertyQuery<D extends JSONSchema, X> = {
anyOf: [
D,
{
Expand All @@ -83,7 +83,7 @@ export type PropertyQuery<D extends JSONSchema> = {
type: 'array'
items: D
}
}
} & X
}
]
}
Expand All @@ -92,9 +92,13 @@ export type PropertyQuery<D extends JSONSchema> = {
* Create a Feathers query syntax compatible JSON schema definition for a property definition.
*
* @param def The property definition (e.g. `{ type: 'string' }`)
* @param extensions Additional properties to add to the query property schema
* @returns A JSON schema definition for the Feathers query syntax for this property.
*/
export const queryProperty = <T extends JSONSchema>(def: T) => {
export const queryProperty = <T extends JSONSchema, X extends { [key: string]: JSONSchema }>(
def: T,
extensions: X = {} as X
) => {
const definition = _.omit(def, 'default')
return {
anyOf: [
Expand All @@ -115,50 +119,59 @@ export const queryProperty = <T extends JSONSchema>(def: T) => {
$nin: {
type: 'array',
items: definition
}
},
...extensions
}
}
]
} as const
}

export const SUPPORTED_TYPES = ['string', 'number', 'integer', 'boolean', 'null']

/**
* Creates Feathers a query syntax compatible JSON schema for multiple properties.
*
* @param definitions A map of property definitions
* @param extensions Additional properties to add to the query property schema
* @returns The JSON schema definition for the Feathers query syntax for multiple properties
*/
export const queryProperties = <T extends { [key: string]: JSONSchema }>(definitions: T) =>
export const queryProperties = <
T extends { [key: string]: JSONSchema },
X extends { [K in keyof T]?: { [key: string]: JSONSchema } }
>(
definitions: T,
extensions: X = {} as X
) =>
Object.keys(definitions).reduce((res, key) => {
const result = res as any
const definition = definitions[key]
const { type, $ref } = definition as any
const { $ref } = definition as any

if ($ref || !SUPPORTED_TYPES.includes(type)) {
throw new Error(
`Can not create query syntax schema for property '${key}'. Only types ${SUPPORTED_TYPES.join(
', '
)} are allowed.`
)
if ($ref) {
throw new Error(`Can not create query syntax schema for reference property '${key}'`)
}

result[key] = queryProperty(definition)
result[key] = queryProperty(definition as JSONSchemaDefinition, extensions[key as keyof T])

return result
}, {} as { [K in keyof T]: PropertyQuery<T[K]> })
}, {} as { [K in keyof T]: PropertyQuery<T[K], X[K]> })

/**
* Creates a JSON schema for the complete Feathers query syntax including `$limit`, $skip`
* and `$sort` and `$select` for the allowed properties.
*
* @param definition The property definitions to create the query syntax schema for
* @param extensions Additional properties to add to the query property schema
* @returns A JSON schema for the complete query syntax
*/
export const querySyntax = <T extends { [key: string]: JSONSchema }>(definition: T) => {
export const querySyntax = <
T extends { [key: string]: JSONSchema },
X extends { [K in keyof T]?: { [key: string]: JSONSchema } }
>(
definition: T,
extensions: X = {} as X
) => {
const keys = Object.keys(definition)
const props = queryProperties(definition)
const props = queryProperties(definition, extensions)

return {
$limit: {
Expand Down
62 changes: 46 additions & 16 deletions packages/schema/test/json-schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Ajv from 'ajv'
import assert from 'assert'
import { FromSchema } from '../src'
import { queryProperties, querySyntax } from '../src/json-schema'

describe('@feathersjs/schema/json-schema', () => {
Expand All @@ -8,25 +9,11 @@ describe('@feathersjs/schema/json-schema', () => {
() =>
queryProperties({
something: {
type: 'object'
$ref: 'something'
}
}),
{
message:
"Can not create query syntax schema for property 'something'. Only types string, number, integer, boolean, null are allowed."
}
)

assert.throws(
() =>
queryProperties({
otherThing: {
type: 'array'
}
}),
{
message:
"Can not create query syntax schema for property 'otherThing'. Only types string, number, integer, boolean, null are allowed."
message: "Can not create query syntax schema for reference property 'something'"
}
)
})
Expand All @@ -39,4 +26,47 @@ describe('@feathersjs/schema/json-schema', () => {

new Ajv().compile(schema)
})

it('querySyntax with extensions', async () => {
const schema = {
name: {
type: 'string'
},
age: {
type: 'number'
}
} as const

const querySchema = {
type: 'object',
properties: querySyntax(schema, {
name: {
$ilike: {
type: 'string'
}
},
age: {
$value: {
type: 'null'
}
}
} as const)
} as const

type Query = FromSchema<typeof querySchema>

const q: Query = {
name: {
$ilike: 'hello'
},
age: {
$value: null,
$gte: 42
}
}

const validator = new Ajv({ strict: false }).compile(schema)

assert.ok(validator(q))
})
})
Loading

0 comments on commit f324940

Please # to comment.