Skip to content

Commit 1393bed

Browse files
authored
feat(schema): Add schema helper for handling Object ids (#3058)
1 parent 37fe5c4 commit 1393bed

10 files changed

Lines changed: 99 additions & 75 deletions

File tree

docs/api/databases/mongodb.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -404,25 +404,30 @@ import { keywordObjectId } from '@feathersjs/mongodb'
404404
const validator = new Ajv()
405405

406406
validator.addKeyword(keywordObjectId)
407+
```
408+
409+
### ObjectIdSchema
410+
411+
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:
412+
413+
```ts
414+
import { ObjectIdSchema } from '@feathersjs/typebox' // or '@feathersjs/schema'
407415
408416
const typeboxSchema = Type.Object({
409-
userId: Type.String({ objectid: true })
417+
userId: ObjectIdSchema()
410418
})
411419

412420
const jsonSchema = {
413421
type: 'object',
414422
properties: {
415-
userId: {
416-
type: 'string',
417-
objectid: true
418-
}
423+
userId: ObjectIdSchema()
419424
}
420425
}
421426
```
422427

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

425-
Usually a converted object id property can be treated like a string but in some cases when working with it on the server you may have to call `toString()` to get the proper type.
430+
The `ObjectIdSchema` helper will only work when the [`objectid` AJV keyword](#ajv-keyword) is registered.
426431

427432
</BlockQuote>
428433

packages/generators/src/authentication/templates/schema.json.tpl.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ const template = ({
1111
type,
1212
relative
1313
}: AuthenticationGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
14-
import { resolve, querySyntax, getValidator } from '@feathersjs/schema'
14+
import { resolve, querySyntax, getValidator } from '@feathersjs/schema'${
15+
type === 'mongodb'
16+
? `
17+
import { ObjectIdSchema } from '@feathersjs/schema'`
18+
: ''
19+
}
1520
import type { FromSchema } from '@feathersjs/schema'
1621
${localTemplate(authStrategies, `import { passwordHash } from '@feathersjs/authentication-local'`)}
1722
@@ -27,16 +32,7 @@ export const ${camelName}Schema = {
2732
additionalProperties: false,
2833
required: [ '${type === 'mongodb' ? '_id' : 'id'}'${localTemplate(authStrategies, ", 'email'")} ],
2934
properties: {
30-
${
31-
type === 'mongodb'
32-
? `_id: {
33-
type: 'string',
34-
objectid: true
35-
},`
36-
: `id: {
37-
type: 'number'
38-
},`
39-
}
35+
${type === 'mongodb' ? `_id: ObjectIdSchema(),` : `id: { type: 'number' },`}
4036
${authStrategies
4137
.map((name) =>
4238
name === 'local'

packages/generators/src/authentication/templates/schema.typebox.tpl.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ export const template = ({
1212
relative
1313
}: AuthenticationGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
1414
import { resolve } from '@feathersjs/schema'
15-
import { Type, getValidator, querySyntax } from '@feathersjs/typebox'
15+
import { Type, getValidator, querySyntax } from '@feathersjs/typebox'${
16+
type === 'mongodb'
17+
? `
18+
import { ObjectIdSchema } from '@feathersjs/typebox'`
19+
: ''
20+
}
1621
import type { Static } from '@feathersjs/typebox'
1722
${localTemplate(authStrategies, `import { passwordHash } from '@feathersjs/authentication-local'`)}
1823
@@ -23,7 +28,7 @@ import { dataValidator, queryValidator } from '${relative}/${
2328
2429
// Main data model schema
2530
export const ${camelName}Schema = Type.Object({
26-
${type === 'mongodb' ? '_id: Type.String({ objectid: true })' : 'id: Type.Number()'},
31+
${type === 'mongodb' ? '_id: ObjectIdSchema()' : 'id: Type.Number()'},
2732
${authStrategies
2833
.map((name) =>
2934
name === 'local'

packages/generators/src/service/templates/schema.json.tpl.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ const template = ({
1010
cwd,
1111
lib
1212
}: ServiceGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
13-
import { resolve, getValidator, querySyntax } from '@feathersjs/schema'
13+
import { resolve, getValidator, querySyntax } from '@feathersjs/schema'${
14+
type === 'mongodb'
15+
? `
16+
import { ObjectIdSchema } from '@feathersjs/schema'`
17+
: ''
18+
}
1419
import type { FromSchema } from '@feathersjs/schema'
1520
1621
import type { HookContext } from '${relative}/declarations'
@@ -25,19 +30,8 @@ export const ${camelName}Schema = {
2530
additionalProperties: false,
2631
required: [ '${type === 'mongodb' ? '_id' : 'id'}', 'text' ],
2732
properties: {
28-
${
29-
type === 'mongodb'
30-
? `_id: {
31-
type: 'string',
32-
objectid: true
33-
},`
34-
: `id: {
35-
type: 'number'
36-
},`
37-
}
38-
text: {
39-
type: 'string'
40-
}
33+
${type === 'mongodb' ? `_id: ObjectIdSchema(),` : `id: { type: 'number' },`}
34+
text: { type: 'string' }
4135
}
4236
} as const
4337
export type ${upperName} = FromSchema<typeof ${camelName}Schema>

packages/generators/src/service/templates/schema.typebox.tpl.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ const template = ({
1111
lib
1212
}: ServiceGeneratorContext) => /* ts */ `// // For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
1313
import { resolve } from '@feathersjs/schema'
14-
import { Type, getValidator, querySyntax } from '@feathersjs/typebox'
14+
import { Type, getValidator, querySyntax } from '@feathersjs/typebox'${
15+
type === 'mongodb'
16+
? `
17+
import { ObjectIdSchema } from '@feathersjs/typebox'`
18+
: ''
19+
}
1520
import type { Static } from '@feathersjs/typebox'
1621
1722
import type { HookContext } from '${relative}/declarations'
@@ -21,7 +26,7 @@ import { dataValidator, queryValidator } from '${relative}/${
2126
2227
// Main data model schema
2328
export const ${camelName}Schema = Type.Object({
24-
${type === 'mongodb' ? '_id: Type.String({ objectid: true })' : 'id: Type.Number()'},
29+
${type === 'mongodb' ? '_id: ObjectIdSchema()' : 'id: Type.Number()'},
2530
text: Type.String()
2631
}, { $id: '${upperName}', additionalProperties: false })
2732
export type ${upperName} = Static<typeof ${camelName}Schema>

packages/schema/src/hooks/validate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const validateQuery = <H extends HookContext>(schema: Schema<any> | Valid
2222
} catch (error: any) {
2323
throw error.ajv ? new BadRequest(error.message, error.errors) : error
2424
}
25-
25+
2626
if (typeof next === 'function') {
2727
return next()
2828
}

packages/schema/src/json-schema.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,6 @@ export const queryProperties = <
144144
Object.keys(definitions).reduce((res, key) => {
145145
const result = res as any
146146
const definition = definitions[key]
147-
const { $ref } = definition as any
148-
149-
if ($ref) {
150-
throw new Error(`Can not create query syntax schema for reference property '${key}'`)
151-
}
152147

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

@@ -227,3 +222,11 @@ export const querySyntax = <
227222
...props
228223
} as const
229224
}
225+
226+
export const ObjectIdSchema = () =>
227+
({
228+
anyOf: [
229+
{ type: 'string', objectid: true },
230+
{ type: 'object', properties: {}, additionalProperties: false }
231+
]
232+
} as const)

packages/schema/test/json-schema.test.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
11
import Ajv from 'ajv'
22
import assert from 'assert'
3+
import { ObjectId as MongoObjectId } from 'mongodb'
34
import { FromSchema } from '../src'
4-
import { queryProperties, querySyntax } from '../src/json-schema'
5+
import { querySyntax, ObjectIdSchema } from '../src/json-schema'
56

67
describe('@feathersjs/schema/json-schema', () => {
7-
it('queryProperties errors for unsupported query types', () => {
8-
assert.throws(
9-
() =>
10-
queryProperties({
11-
something: {
12-
$ref: 'something'
13-
}
14-
}),
15-
{
16-
message: "Can not create query syntax schema for reference property 'something'"
17-
}
18-
)
19-
})
20-
218
it('querySyntax works with no properties', async () => {
229
const schema = {
2310
type: 'object',
@@ -69,4 +56,27 @@ describe('@feathersjs/schema/json-schema', () => {
6956

7057
assert.ok(validator(q))
7158
})
59+
60+
// Test ObjectId validation
61+
it('ObjectId', async () => {
62+
const schema = {
63+
type: 'object',
64+
properties: {
65+
_id: ObjectIdSchema()
66+
}
67+
}
68+
69+
const validator = new Ajv({
70+
strict: false
71+
}).compile(schema)
72+
const validated = await validator({
73+
_id: '507f191e810c19729de860ea'
74+
})
75+
assert.ok(validated)
76+
77+
const validated2 = await validator({
78+
_id: new MongoObjectId()
79+
})
80+
assert.ok(validated2)
81+
})
7282
})

packages/typebox/src/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,6 @@ export const queryProperties = <
129129
const result = res as any
130130
const value = definition.properties[key]
131131

132-
if (value.$ref) {
133-
throw new Error(`Can not create query syntax schema for reference property '${key}'`)
134-
}
135-
136132
result[key] = queryProperty(value, extensions[key])
137133

138134
return result
@@ -182,3 +178,6 @@ export const querySyntax = <
182178
options
183179
)
184180
}
181+
182+
export const ObjectIdSchema = () =>
183+
Type.Union([Type.String({ objectid: true }), Type.Object({}, { additionalProperties: false })])

packages/typebox/test/index.test.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import assert from 'assert'
2+
import { ObjectId as MongoObjectId } from 'mongodb'
23
import { Ajv } from '@feathersjs/schema'
34
import {
45
querySyntax,
@@ -7,7 +8,7 @@ import {
78
defaultAppConfiguration,
89
getDataValidator,
910
getValidator,
10-
queryProperties
11+
ObjectIdSchema
1112
} from '../src'
1213

1314
describe('@feathersjs/schema/typebox', () => {
@@ -39,20 +40,6 @@ describe('@feathersjs/schema/typebox', () => {
3940
assert.ok(!validated)
4041
})
4142

42-
it('queryProperties errors for unsupported query types', () => {
43-
assert.throws(
44-
() =>
45-
queryProperties(
46-
Type.Object({
47-
something: Type.Ref(Type.Object({}, { $id: 'something' }))
48-
})
49-
),
50-
{
51-
message: "Can not create query syntax schema for reference property 'something'"
52-
}
53-
)
54-
})
55-
5643
it('querySyntax works with no properties', async () => {
5744
const schema = querySyntax(Type.Object({}))
5845

@@ -113,6 +100,26 @@ describe('@feathersjs/schema/typebox', () => {
113100
assert.ok(validated)
114101
})
115102

103+
// Test ObjectId validation
104+
it('ObjectId', async () => {
105+
const schema = Type.Object({
106+
_id: ObjectIdSchema()
107+
})
108+
109+
const validator = new Ajv({
110+
strict: false
111+
}).compile(schema)
112+
const validated = await validator({
113+
_id: '507f191e810c19729de860ea'
114+
})
115+
assert.ok(validated)
116+
117+
const validated2 = await validator({
118+
_id: new MongoObjectId()
119+
})
120+
assert.ok(validated2)
121+
})
122+
116123
it('validators', () => {
117124
assert.strictEqual(typeof getDataValidator(Type.Object({}), new Ajv()), 'object')
118125
assert.strictEqual(typeof getValidator(Type.Object({}), new Ajv()), 'function')

0 commit comments

Comments
 (0)