Skip to content

Commit f8bb525

Browse files
committed
docs(mongodb): update examples to latest CLI code
1 parent 212847b commit f8bb525

File tree

1 file changed

+183
-89
lines changed

1 file changed

+183
-89
lines changed

docs/api/databases/mongodb.md

Lines changed: 183 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -441,75 +441,122 @@ app.service('users').hooks({
441441

442442
Which will allows queries like `/users?_id=507f1f77bcf86cd799439011&age=25`.
443443

444-
## Validating MongoDB Data
444+
## Validate Data
445445

446-
### Using Resolvers
446+
There are two ways
447447

448-
The simplest way to convert ObjectIds is to make a resolver.
448+
Since MongoDB uses special binary types for stored data, Feathers uses Schemas to validate MongoDB data and resolvers to convert types. Attributes on MongoDB services often require specitying two schema formats:
449+
450+
- Object-type formats for data pulled from the database and passed between services on the API server. The MongoDB driver for Node converts binary data into objects, like `ObjectId` and `Date`.
451+
- String-type formats for data passed from the client.
452+
453+
You can convert values to their binary types to take advantage of performance enhancements in MongoDB. The following sections show how to use Schemas and Resolvers for each MongoDB binary format.
454+
455+
### Shared Validators
456+
457+
The following example shows how to create two custom validator/type utilities:
458+
459+
- An ObjectId validator which handles strings or objects. Place the following code inside `src/schema/shared.ts`.
460+
- A Date validator
449461

450462
```ts
451-
import { ObjectId } from 'mongodb'
463+
import { Type } from '@feathersjs/typebox'
464+
465+
export const ObjectId = () => Type.Union([
466+
Type.String({ format: 'objectid' }),
467+
Type.Object({}, { additionalProperties: true }),
468+
])
469+
export const NullableObjectId = () => Type.Union([ObjectId(), Type.Null()])
470+
471+
export const Date = () => Type.Union([
472+
Type.String({ format: 'date-time' }),
473+
Type.Date(),
474+
])
475+
```
452476

453-
// Resolver for the basic data model (e.g. creating new entries)
454-
export const commentsDataResolver = resolve<commentsData, HookContext>({
455-
schema: commentsDataSchema,
456-
validate: false,
457-
properties: {
458-
text: { type: 'string' },
459-
userId: async (value) => {
460-
return value ? new ObjectId(value) : value
461-
}
462-
}
463-
})
477+
Technically, `ObjectId` in the above example could be improved since it allows any object to pass validation. A query with a bogus id fail only when it reaches the database. Since objects can't be passed from the client, it's a situation which only occurs with our server code, so it will suffice.
478+
479+
### ObjectIds
480+
481+
With the [shared validators](#shared-validators) in place, you can specify an `ObjectId` type in your TypeBox Schemas:
482+
483+
```ts
484+
import { Type } from '@feathersjs/typebox'
485+
import { ObjectId } from '../../schemas/shared'
486+
487+
const userSchema = Type.Object(
488+
{
489+
_id: ObjectId(),
490+
orgIds: Type.Array( ObjectId() ),
491+
},
492+
{ $id: 'User', additionalProperties: false }
493+
)
494+
```
495+
496+
### Dates
497+
498+
The standard format for transmitting dates between client and server is [ISO8601](https://www.rfc-editor.org/rfc/rfc3339#section-5.6), which is a string representation of a date.
499+
500+
While it's possible to use `$gt` (greater than) and `$lt` (less than) queries on string values, performance will be faster for dates stored as date objects. This means you'd use the same code as the previous example, followed by a resolver or a hook to convert the values in `context.data` and `context.query` into actual Dates.
501+
502+
With the [shared validators](#shared-validators) in place, you can use the custom `Date` type in your TypeBox Schemas:
503+
504+
```ts
505+
import { Type } from '@feathersjs/typebox'
506+
import { ObjectId, Date } from '../../schemas/shared'
507+
508+
const userSchema = Type.Object(
509+
{
510+
_id: ObjectId(),
511+
createdAt: Date(),
512+
},
513+
{ $id: 'User', additionalProperties: false }
514+
)
464515
```
465516

466-
### Using a Custom AJV Instance
517+
## Convert Data
467518

468-
All [Feathers schemas](/api/schema/schema) share an implicit AJV instance by default.
519+
It's possible to convert data by either customizing AJV or using resolvers. Converting with AJV is currently the simplest solution, but it uses extended AJV features, outside of the JSON Schema standard.
469520

470-
It's possible to validate MongoDB ObjectIds and dates with AJV, as well. This is more complicated than using resolvers, but can also handle the full query syntax. You can create a custom AJV instance with extra formatters attached.
521+
### Convert With AJV
471522

472-
#### Custom AJV Instance
523+
The FeathersJS CLI creates validators in the `src/schema/validators` file. It's possible to enable the AJV validators to convert certain values using AJV keywords. This example works for both [TypeBox](/api/schema/typebox) and [JSON Schema](/api/schema/schema):
473524

474-
Here's an example of a custom AJV instance, which could be placed in `src/schemas/ajv.ts` and referenced by all other services.
525+
#### Create Converters
475526

476527
```ts
477-
import Ajv, { AnySchemaObject } from 'ajv'
478-
import addFormats from 'ajv-formats'
528+
import type { AnySchemaObject } from 'ajv'
529+
import { Ajv } from '@feathersjs/schema'
479530
import { ObjectId } from 'mongodb'
480531

481-
export { type Infer, validateData, validateQuery, schema, queryProperty } from '@feathersjs/schema'
482-
483-
// Reusable `convert` keyword.
532+
// `convert` keyword.
484533
const keywordConvert = {
485534
keyword: 'convert',
486535
type: 'string',
487536
compile(schemaVal: boolean, parentSchema: AnySchemaObject) {
488537
if (!schemaVal) return () => true
489538

490-
// Update date-time string to Date object
539+
// Convert date-time string to Date
491540
if (['date-time', 'date'].includes(parentSchema.format)) {
492541
return function (value: string, obj: any) {
493542
const { parentData, parentDataProperty } = obj
494-
console.log(value)
495543
parentData[parentDataProperty] = new Date(value)
496544
return true
497545
}
498546
}
499-
// Update objectid string to ObjectId
547+
// Convert objectid string to ObjectId
500548
else if (parentSchema.format === 'objectid') {
501549
return function (value: string, obj: any) {
502550
const { parentData, parentDataProperty } = obj
503-
// Update date-time string to Date object
504551
parentData[parentDataProperty] = new ObjectId(value)
505552
return true
506553
}
507554
}
508555
return () => true
509-
}
556+
},
510557
} as const
511558

512-
// Reusable `ObjectId` Formatter
559+
// `objectid` formatter
513560
const formatObjectId = {
514561
type: 'string',
515562
validate: (id: string | ObjectId) => {
@@ -518,83 +565,84 @@ const formatObjectId = {
518565
return false
519566
}
520567
return false
521-
}
568+
},
522569
} as const
523570

524-
// Create a custom AJV
525-
export const ajv = new Ajv({
526-
coerceTypes: true,
527-
useDefaults: true,
528-
schemas: []
529-
})
530-
addFormats(ajv)
531-
ajv.addKeyword(keywordConvert)
532-
ajv.addFormat('objectid', formatObjectId)
533-
534-
// Create a custom AJV instance that doesn't coerce types
535-
export const ajvNoCoerce = new Ajv({
536-
coerceTypes: false,
537-
useDefaults: true,
538-
schemas: []
539-
})
540-
addFormats(ajvNoCoerce)
541-
ajvNoCoerce.addKeyword(keywordConvert)
542-
ajvNoCoerce.addFormat('objectid', formatObjectId)
571+
export function addConverters(validator: Ajv) {
572+
validator.addKeyword(keywordConvert)
573+
validator.addFormat('objectid', formatObjectId)
574+
}
543575
```
544576

545-
#### Pass the Custom AJV Instance to `schema`
577+
#### Apply to AJV
578+
579+
You can then add converters to the generated validators by importing the `addConverters` utility in the `validators.ts` file:
580+
581+
```ts{3,30-32}
582+
import { Ajv, addFormats } from '@feathersjs/schema'
583+
import type { FormatsPluginOptions } from '@feathersjs/schema'
584+
import { addConverters } from './converters'
585+
586+
const formats: FormatsPluginOptions = [
587+
'date-time',
588+
'time',
589+
'date',
590+
'email',
591+
'hostname',
592+
'ipv4',
593+
'ipv6',
594+
'uri',
595+
'uri-reference',
596+
'uuid',
597+
'uri-template',
598+
'json-pointer',
599+
'relative-json-pointer',
600+
'regex',
601+
]
546602
547-
Once created, all service schema files should use the custom AJV instance. Here's an example:
603+
export const dataValidator = addFormats(new Ajv({}), formats)
548604
549-
```ts
550-
// Schema for the data that is being returned
551-
export const connectionsResultSchema = schema(
552-
{
553-
$id: 'ConnectionsResult',
554-
type: 'object',
555-
additionalProperties: false,
556-
required: ['_id'],
557-
properties: {
558-
...common,
559-
_id: {
560-
anyOf: [
561-
{ type: 'string', format: 'objectid', convert: true },
562-
{ type: 'object' } // ObjectId
563-
]
564-
},
565-
createdAt: { type: 'string', format: 'date-time', convert: true }
566-
}
567-
} as const,
568-
ajv
605+
export const queryValidator = addFormats(
606+
new Ajv({
607+
coerceTypes: true,
608+
}),
609+
formats,
569610
)
611+
612+
addConverters(dataValidator)
613+
addConverters(queryValidator)
570614
```
571615

572-
## Common Mistakes
616+
#### Update Shared Validators
573617

574-
Here are a couple of errors you might run into while using validators.
618+
We can now update the [shared validators](#shared-validators) to also convert data.
575619

576-
### unknown keyword: "convert"
620+
```ts{5}
621+
import { Type } from '@feathersjs/typebox'
577622
578-
You'll see an error like `"Error: strict mode: unknown keyword: "convert"` in a few scenarios:
579-
580-
- You fail to [Pass the Custom AJV Instance to every `schema`](#pass-the-custom-ajv-instance-to-schema). If you're using a custom AJV instance, be sure to provide it to **every** place where you call `schema()`.
581-
- You try to use custom keywords in your schema without registering them, first.
582-
- You make a typo in your schema. For example, it's common to forget to accidentally mis-document arrays and collapse the item `properties` up one level.
623+
export const ObjectId = () => Type.Union([
624+
Type.String({ format: 'objectid', convert: true }),
625+
Type.Object({}, { additionalProperties: true }),
626+
])
627+
export const NullableObjectId = () => Type.Union([ObjectId(), Type.Null()])
583628
584-
### unknown format "date-time"
629+
export const Date = () => Type.Union([
630+
Type.String({ format: 'date-time', convert: true }),
631+
Type.Date(),
632+
])
633+
```
585634

586-
You'll see an error like `Error: unknown format "date-time" ignored in schema at path "#/properties/createdAt"` in a few scenarios.
635+
Now when we use the shared validators (as shown in the [ObjectIds](#objectids) and [Dates](#dates) sections) AJV will also convert the data to the correct type.
587636

588-
- You're attempting to use a formatter not built into AJV.
589-
- You fail to [Pass the Custom AJV Instance to every `schema`](#pass-the-custom-ajv-instance-to-schema). If you're using a custom AJV instance, be sure to provide it to **every** place where you call `schema()`.
637+
### Convert with Resolvers
590638

591-
## Search
639+
In place of [converting data with AJV](#convert-with-ajv), you can also use resolvers to convert data.
592640

593-
## ObjectId resolvers
641+
#### ObjectId Resolvers
594642

595643
MongoDB uses object ids as primary keys and for references to other documents. To a client they are represented as strings and to convert between strings and object ids, the following [property resolver](../schema/resolvers.md) helpers can be used.
596644

597-
### resolveObjectId
645+
#### resolveObjectId
598646

599647
`resolveObjectId` resolves a property as an object id. It can be used as a direct property resolver or called with the original value.
600648

@@ -621,7 +669,7 @@ export const messageDataResolver = resolve<Message, HookContext>({
621669
})
622670
```
623671

624-
### resolveQueryObjectId
672+
#### resolveQueryObjectId
625673

626674
`resolveQueryObjectId` allows to query for object ids. It supports conversion from a string to an object id as well as conversion for values from the [$in, $nin and $ne query syntax](./querying.md).
627675

@@ -634,3 +682,49 @@ export const messageQueryResolver = resolve<MessageQuery, HookContext>({
634682
}
635683
})
636684
```
685+
686+
## Common Mistakes
687+
688+
Here are a couple of errors you might run into while using validators.
689+
690+
### unknown keyword: "convert"
691+
692+
You'll see an error like `"Error: strict mode: unknown keyword: "convert"` in a few scenarios:
693+
694+
- You fail to [Pass the Custom AJV Instance to every `schema`](#pass-the-custom-ajv-instance-to-schema). If you're using a custom AJV instance, be sure to provide it to **every** place where you call `schema()`.
695+
- You try to use custom keywords in your schema without registering them, first.
696+
- You make a typo in your schema. For example, it's common to forget to accidentally mis-document arrays and collapse the item `properties` up one level.
697+
698+
### unknown format "date-time"
699+
700+
You'll see an error like `Error: unknown format "date-time" ignored in schema at path "#/properties/createdAt"` in a few scenarios.
701+
702+
- You're attempting to use a formatter not built into AJV.
703+
- You fail to [Pass the Custom AJV Instance to every `schema`](#pass-the-custom-ajv-instance-to-schema). If you're using a custom AJV instance, be sure to provide it to **every** place where you call `schema()`.
704+
705+
## Search
706+
707+
There are two ways to perform search queries with MongoDB:
708+
709+
- Perform basic Regular Expression matches using the `$regex` filter.
710+
- Perform full-text search using the `$search` filter.
711+
712+
### Basic Regex Search
713+
714+
You can perform basic search using regular expressions with the `$regex` operator. Here's an example query.
715+
716+
```js
717+
{
718+
text: { $regex: 'feathersjs', $options: 'igm' },
719+
}
720+
```
721+
722+
TODO: Show how to customize the query syntax to allow the `$regex` and `$options` operators.
723+
724+
### Full-Text Search
725+
726+
See the MongoDB documentation for instructions on performing full-text search using the `$search` operator:
727+
728+
- Perform [full-text queries on self-hosted MongoDB](https://www.mongodb.com/docs/manual/core/link-text-indexes/).
729+
- Perform [full-text queries on MongoDB Atlas](https://www.mongodb.com/docs/atlas/atlas-search/) (MongoDB's first-party hosted database).
730+
- Perform [full-text queries with the MongoDB Pipeline](https://www.mongodb.com/docs/manual/tutorial/text-search-in-aggregation/)

0 commit comments

Comments
 (0)