Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs/api/databases/knex.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,21 @@ In addition to the [common querying mechanism](./querying.md), this adapter also
```ts
const messageQuerySchema = Type.Intersect(
[
// This will additionally allow querying for `{ name: { $ilike: 'Dav%' } }`
querySyntax(messageQueryProperties, {
name: {
$ilike: Type.String()
$like: Type.String(),
$notlike: Type.String(),
$ilike: Type.String() // PostgreSQL
}
}),
// Add additional query properties here
Type.Object({})
Type.Object({}, { additionalProperties: false })
],
{ additionalProperties: false }
)
```

More extension examples are in [querySyntax](../schema/typebox.md#querysyntax). `{ age: null }` and `{ age: { $ne: null } }` work when the query property type includes `null` (see the `age` field in the adapter tests).

### $like

Find all records where the value matches the given string pattern. The following query retrieves all messages that start with `Hello`:
Expand Down
19 changes: 16 additions & 3 deletions docs/api/databases/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,13 @@ new MongoDBService({

<BlockQuote type="warning" label="Important">

Note that in a normal application all MongoDB specific operators have to explicitly be added to the [TypeBox query schema](../schema/typebox.md#query-schemas) or [JSON query schema](../schema/schema.md#querysyntax).
Note that in a normal application all MongoDB specific operators have to explicitly be added to the [TypeBox query schema](../schema/typebox.md#querysyntax) or [JSON query schema](../schema/schema.md#querysyntax).

</BlockQuote>

There are two ways to perform search queries with MongoDB:

- Perform basic Regular Expression matches using the `$regex` filter.
- Perform basic Regular Expression matches using the `$regex` operator.
- Perform full-text search using the `$search` filter.

### Basic Regex Search
Expand All @@ -234,6 +234,19 @@ You can perform basic search using regular expressions with the `$regex` operato
}
```

Allow those operators on the properties that need them:

```ts
querySyntax(messageQueryProperties, {
text: {
$regex: Type.String(),
$options: Type.String()
}
})
```

If you also use [`validateQuery(schema, { skipSanitize: false })`](../schema/validators.md#keeping-adapter-sanitization), list them on the service as well: `operators: ['$regex', '$options']`.

### Full-Text Search

See the MongoDB documentation for instructions on performing full-text search using the `$search` operator:
Expand Down Expand Up @@ -456,7 +469,7 @@ validator.addKeyword(keywordObjectId)

### ObjectIdSchema

Both, `@feathersjs/typebox` and `@feathersjs/schema` export an `ObjectIdSchema` helper that creates a schema which can be both, a MongoDB ObjectId or a string that will be converted with the `objectid` keyword:
Both, `@feathersjs/typebox` and `@feathersjs/schema` export an `ObjectIdSchema` helper that creates a schema which can be a MongoDB ObjectId instance or a string that will be converted with the `objectid` keyword. Arbitrary objects — including query operator documents like `{ $ne: null }` or `{ $where: '…' }` — are not valid ObjectIds.

```ts
import { ObjectIdSchema } from '@feathersjs/typebox' // or '@feathersjs/schema'
Expand Down
6 changes: 2 additions & 4 deletions docs/api/schema/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ const userQuery: UserQuery = {
}
```

Additional special query properties [that are not already included in the query syntax](../databases/querying.md) like `$ilike` can be added like this:
Additional operators that are [not already in the common query syntax](../databases/querying.md) (`$like`, `$regex`, …) are added per property. Only add operators your adapter supports. See [TypeBox querySyntax](./typebox.md#querysyntax) for more examples.

```ts
import { querySyntax } from '@feathersjs/schema'
Expand All @@ -184,9 +184,7 @@ export const userQuerySchema = {
properties: {
...querySyntax(userSchema.properties, {
email: {
$ilike: {
type: 'string'
}
$ilike: { type: 'string' }
}
} as const)
}
Expand Down
24 changes: 16 additions & 8 deletions docs/api/schema/typebox.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,30 +101,38 @@ const messageQuerySchema = querySyntax(messageQueryProperties)
type MessageQuery = Static<typeof messageQuerySchema>
```

Additional special query properties [that are not already included in the query syntax](../databases/querying.md) like `$ilike` can be added like this:
Additional operators that are [not already in the common query syntax](../databases/querying.md) must be added per property. Only add operators your adapter actually supports.

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

// Schema for allowed query properties
const messageQueryProperties = Type.Pick(messageSchema, ['id', 'text', 'createdAt', 'userId'], {
additionalProperties: false
})

const messageQuerySchema = Type.Intersect(
[
// This will additionally allow querying for `{ name: { $ilike: 'Dav%' } }`
querySyntax(messageQueryProperties, {
name: {
$ilike: Type.String()
text: {
$like: Type.String(),
$notlike: Type.String(),
$ilike: Type.String(), // PostgreSQL
$regex: Type.String(),
$options: Type.String()
}
}),
// Add additional query properties here
Type.Object({})
Type.Object({}, { additionalProperties: false })
],
{ additionalProperties: false }
)
```

That allows `{ text: { $like: 'Hello%' } }` and `{ text: { $regex: 'feathers', $options: 'i' } }`.

`$ne: null` and `{ userId: null }` are allowed when the **query** property type includes `null` (for example `Type.Union([Type.Number(), Type.Null()])` or `Type.Union([ObjectIdSchema(), Type.Null()])`). That is a field type, not a new operator. Do not change the create/patch data schema unless you also want to store nulls.

Mongo `$meta` / `$slice` in object `$select` or `$sort` are not part of the common syntax. On the adapter sanitizer path, list them on the existing service `operators` option if you need them. `querySyntax` `$select` remains a string array.

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

```ts
Expand Down
2 changes: 1 addition & 1 deletion docs/api/schema/validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ This is intentional. Schema validation and the legacy sanitizer are alternative

- Prefer [`querySyntax`](./typebox.md#querysyntax) (or the [JSON schema helpers](./schema.md#query-helpers)) so only the common operators are allowed on each property.
- Set `additionalProperties: false` on query objects so unknown keys (including unexpected `$` operators) are rejected. Generated applications already do this.
- Only add extra operators (for example `$ilike` or `$regex`) when your adapter supports them and your application needs them.
- Only add extra operators (for example `$ilike` or `$regex`) when your adapter supports them and your application needs them. Copy-paste examples: [querySyntax](./typebox.md#querysyntax).
- Avoid permissive schemas such as `additionalProperties: true` or an open object on external query validation unless you intentionally want clients to send those keys.

<BlockQuote type="warning" label="TypeBox and JSON Schema defaults">
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/cli/service.schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const messageQueryValidator = getValidator(messageQuerySchema, queryValid
export const messageQueryResolver = resolve<MessageQuery, HookContext>({})
```

To add additional operators like `$like` see the [querySyntax](../../api/schema/typebox.md#querysyntax) documentation. You can also add your own query parameters in the `Type.Object({}, { additionalProperties: false })` definition.
To add additional operators like `$like` or `$regex`, see [querySyntax](../../api/schema/typebox.md#querysyntax). You can also add your own query parameters in the `Type.Object({}, { additionalProperties: false })` definition.

<BlockQuote type="warning" label="Important">

Expand Down
27 changes: 15 additions & 12 deletions packages/adapter-commons/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ const parse = (value: any) => (typeof value !== 'undefined' ? parseInt(value, 10
const isPlainObject = (value: any) => _.isObject(value) && value.constructor === {}.constructor

const validateQueryProperty = (query: any, operators: string[] = []): Query => {
if (Array.isArray(query)) {
return query.map((value) => validateQueryProperty(value, operators))
}

if (!isPlainObject(query)) {
return query
}
Expand All @@ -17,11 +21,7 @@ const validateQueryProperty = (query: any, operators: string[] = []): Query => {
throw new BadRequest(`Invalid query parameter ${key}`, query)
}

const value = query[key]

if (isPlainObject(value)) {
query[key] = validateQueryProperty(value, operators)
}
query[key] = validateQueryProperty(query[key], operators)
}

return {
Expand Down Expand Up @@ -91,41 +91,44 @@ export const OPERATORS = ['$in', '$nin', '$lt', '$lte', '$gt', '$gte', '$ne', '$

export const FILTERS: FilterSettings = {
$skip: (value: any) => parse(value),
$sort: (sort: any): { [key: string]: number } => {
$sort: (sort: any, { operators }: FilterQueryOptions): { [key: string]: any } => {
if (typeof sort !== 'object' || Array.isArray(sort)) {
return sort
}

return Object.keys(sort).reduce(
(result, key) => {
result[key] = typeof sort[key] === 'object' ? sort[key] : parse(sort[key])
result[key] =
typeof sort[key] === 'object' && sort[key] !== null
? validateQueryProperty(sort[key], operators)
: parse(sort[key])

return result
},
{} as { [key: string]: number }
{} as { [key: string]: any }
)
},
$limit: (_limit: any, { paginate }: FilterQueryOptions) => getLimit(_limit, paginate),
$select: (select: any) => {
$select: (select: any, { operators }: FilterQueryOptions) => {
if (Array.isArray(select)) {
return select.map((current) => `${current}`)
}

return select
return validateQueryProperty(select, operators)
},
$or: (or: any, { operators }: FilterQueryOptions) => {
if (Array.isArray(or)) {
return or.map((current) => validateQueryProperty(current, operators))
}

return or
return validateQueryProperty(or, operators)
},
$and: (and: any, { operators }: FilterQueryOptions) => {
if (Array.isArray(and)) {
return and.map((current) => validateQueryProperty(current, operators))
}

return and
return validateQueryProperty(and, operators)
}
}

Expand Down
Loading
Loading