Skip to content

Commit

Permalink
feat(schema): Add schema helper for handling Object ids (#3058)
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl committed Feb 15, 2023
1 parent 37fe5c4 commit 1393bed
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 75 deletions.
17 changes: 11 additions & 6 deletions docs/api/databases/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,25 +404,30 @@ import { keywordObjectId } from '@feathersjs/mongodb'
const validator = new Ajv()

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:

```ts
import { ObjectIdSchema } from '@feathersjs/typebox' // or '@feathersjs/schema'
const typeboxSchema = Type.Object({
userId: Type.String({ objectid: true })
userId: ObjectIdSchema()
})

const jsonSchema = {
type: 'object',
properties: {
userId: {
type: 'string',
objectid: true
}
userId: ObjectIdSchema()
}
}
```

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

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.
The `ObjectIdSchema` helper will only work when the [`objectid` AJV keyword](#ajv-keyword) is registered.

</BlockQuote>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ const template = ({
type,
relative
}: AuthenticationGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
import { resolve, querySyntax, getValidator } from '@feathersjs/schema'
import { resolve, querySyntax, getValidator } from '@feathersjs/schema'${
type === 'mongodb'
? `
import { ObjectIdSchema } from '@feathersjs/schema'`
: ''
}
import type { FromSchema } from '@feathersjs/schema'
${localTemplate(authStrategies, `import { passwordHash } from '@feathersjs/authentication-local'`)}
Expand All @@ -27,16 +32,7 @@ export const ${camelName}Schema = {
additionalProperties: false,
required: [ '${type === 'mongodb' ? '_id' : 'id'}'${localTemplate(authStrategies, ", 'email'")} ],
properties: {
${
type === 'mongodb'
? `_id: {
type: 'string',
objectid: true
},`
: `id: {
type: 'number'
},`
}
${type === 'mongodb' ? `_id: ObjectIdSchema(),` : `id: { type: 'number' },`}
${authStrategies
.map((name) =>
name === 'local'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ export const template = ({
relative
}: AuthenticationGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
import { resolve } from '@feathersjs/schema'
import { Type, getValidator, querySyntax } from '@feathersjs/typebox'
import { Type, getValidator, querySyntax } from '@feathersjs/typebox'${
type === 'mongodb'
? `
import { ObjectIdSchema } from '@feathersjs/typebox'`
: ''
}
import type { Static } from '@feathersjs/typebox'
${localTemplate(authStrategies, `import { passwordHash } from '@feathersjs/authentication-local'`)}
Expand All @@ -23,7 +28,7 @@ import { dataValidator, queryValidator } from '${relative}/${
// Main data model schema
export const ${camelName}Schema = Type.Object({
${type === 'mongodb' ? '_id: Type.String({ objectid: true })' : 'id: Type.Number()'},
${type === 'mongodb' ? '_id: ObjectIdSchema()' : 'id: Type.Number()'},
${authStrategies
.map((name) =>
name === 'local'
Expand Down
22 changes: 8 additions & 14 deletions packages/generators/src/service/templates/schema.json.tpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ const template = ({
cwd,
lib
}: ServiceGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
import { resolve, getValidator, querySyntax } from '@feathersjs/schema'
import { resolve, getValidator, querySyntax } from '@feathersjs/schema'${
type === 'mongodb'
? `
import { ObjectIdSchema } from '@feathersjs/schema'`
: ''
}
import type { FromSchema } from '@feathersjs/schema'
import type { HookContext } from '${relative}/declarations'
Expand All @@ -25,19 +30,8 @@ export const ${camelName}Schema = {
additionalProperties: false,
required: [ '${type === 'mongodb' ? '_id' : 'id'}', 'text' ],
properties: {
${
type === 'mongodb'
? `_id: {
type: 'string',
objectid: true
},`
: `id: {
type: 'number'
},`
}
text: {
type: 'string'
}
${type === 'mongodb' ? `_id: ObjectIdSchema(),` : `id: { type: 'number' },`}
text: { type: 'string' }
}
} as const
export type ${upperName} = FromSchema<typeof ${camelName}Schema>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ const template = ({
lib
}: ServiceGeneratorContext) => /* ts */ `// // For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
import { resolve } from '@feathersjs/schema'
import { Type, getValidator, querySyntax } from '@feathersjs/typebox'
import { Type, getValidator, querySyntax } from '@feathersjs/typebox'${
type === 'mongodb'
? `
import { ObjectIdSchema } from '@feathersjs/typebox'`
: ''
}
import type { Static } from '@feathersjs/typebox'
import type { HookContext } from '${relative}/declarations'
Expand All @@ -21,7 +26,7 @@ import { dataValidator, queryValidator } from '${relative}/${
// Main data model schema
export const ${camelName}Schema = Type.Object({
${type === 'mongodb' ? '_id: Type.String({ objectid: true })' : 'id: Type.Number()'},
${type === 'mongodb' ? '_id: ObjectIdSchema()' : 'id: Type.Number()'},
text: Type.String()
}, { $id: '${upperName}', additionalProperties: false })
export type ${upperName} = Static<typeof ${camelName}Schema>
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/src/hooks/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const validateQuery = <H extends HookContext>(schema: Schema<any> | Valid
} catch (error: any) {
throw error.ajv ? new BadRequest(error.message, error.errors) : error
}

if (typeof next === 'function') {
return next()
}
Expand Down
13 changes: 8 additions & 5 deletions packages/schema/src/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,6 @@ export const queryProperties = <
Object.keys(definitions).reduce((res, key) => {
const result = res as any
const definition = definitions[key]
const { $ref } = definition as any

if ($ref) {
throw new Error(`Can not create query syntax schema for reference property '${key}'`)
}

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

Expand Down Expand Up @@ -227,3 +222,11 @@ export const querySyntax = <
...props
} as const
}

export const ObjectIdSchema = () =>
({
anyOf: [
{ type: 'string', objectid: true },
{ type: 'object', properties: {}, additionalProperties: false }
]
} as const)
40 changes: 25 additions & 15 deletions packages/schema/test/json-schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
import Ajv from 'ajv'
import assert from 'assert'
import { ObjectId as MongoObjectId } from 'mongodb'
import { FromSchema } from '../src'
import { queryProperties, querySyntax } from '../src/json-schema'
import { querySyntax, ObjectIdSchema } from '../src/json-schema'

describe('@feathersjs/schema/json-schema', () => {
it('queryProperties errors for unsupported query types', () => {
assert.throws(
() =>
queryProperties({
something: {
$ref: 'something'
}
}),
{
message: "Can not create query syntax schema for reference property 'something'"
}
)
})

it('querySyntax works with no properties', async () => {
const schema = {
type: 'object',
Expand Down Expand Up @@ -69,4 +56,27 @@ describe('@feathersjs/schema/json-schema', () => {

assert.ok(validator(q))
})

// Test ObjectId validation
it('ObjectId', async () => {
const schema = {
type: 'object',
properties: {
_id: ObjectIdSchema()
}
}

const validator = new Ajv({
strict: false
}).compile(schema)
const validated = await validator({
_id: '507f191e810c19729de860ea'
})
assert.ok(validated)

const validated2 = await validator({
_id: new MongoObjectId()
})
assert.ok(validated2)
})
})
7 changes: 3 additions & 4 deletions packages/typebox/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,6 @@ export const queryProperties = <
const result = res as any
const value = definition.properties[key]

if (value.$ref) {
throw new Error(`Can not create query syntax schema for reference property '${key}'`)
}

result[key] = queryProperty(value, extensions[key])

return result
Expand Down Expand Up @@ -182,3 +178,6 @@ export const querySyntax = <
options
)
}

export const ObjectIdSchema = () =>
Type.Union([Type.String({ objectid: true }), Type.Object({}, { additionalProperties: false })])
37 changes: 22 additions & 15 deletions packages/typebox/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from 'assert'
import { ObjectId as MongoObjectId } from 'mongodb'
import { Ajv } from '@feathersjs/schema'
import {
querySyntax,
Expand All @@ -7,7 +8,7 @@ import {
defaultAppConfiguration,
getDataValidator,
getValidator,
queryProperties
ObjectIdSchema
} from '../src'

describe('@feathersjs/schema/typebox', () => {
Expand Down Expand Up @@ -39,20 +40,6 @@ describe('@feathersjs/schema/typebox', () => {
assert.ok(!validated)
})

it('queryProperties errors for unsupported query types', () => {
assert.throws(
() =>
queryProperties(
Type.Object({
something: Type.Ref(Type.Object({}, { $id: 'something' }))
})
),
{
message: "Can not create query syntax schema for reference property 'something'"
}
)
})

it('querySyntax works with no properties', async () => {
const schema = querySyntax(Type.Object({}))

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

// Test ObjectId validation
it('ObjectId', async () => {
const schema = Type.Object({
_id: ObjectIdSchema()
})

const validator = new Ajv({
strict: false
}).compile(schema)
const validated = await validator({
_id: '507f191e810c19729de860ea'
})
assert.ok(validated)

const validated2 = await validator({
_id: new MongoObjectId()
})
assert.ok(validated2)
})

it('validators', () => {
assert.strictEqual(typeof getDataValidator(Type.Object({}), new Ajv()), 'object')
assert.strictEqual(typeof getValidator(Type.Object({}), new Ajv()), 'function')
Expand Down

0 comments on commit 1393bed

Please sign in to comment.