diff --git a/docs/api/databases/mongodb.md b/docs/api/databases/mongodb.md index 341cf1c6fb..542f1dd3db 100644 --- a/docs/api/databases/mongodb.md +++ b/docs/api/databases/mongodb.md @@ -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() } } ```
-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.
diff --git a/packages/generators/src/authentication/templates/schema.json.tpl.ts b/packages/generators/src/authentication/templates/schema.json.tpl.ts index b4dc41f252..da7f2aa3c1 100644 --- a/packages/generators/src/authentication/templates/schema.json.tpl.ts +++ b/packages/generators/src/authentication/templates/schema.json.tpl.ts @@ -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'`)} @@ -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' diff --git a/packages/generators/src/authentication/templates/schema.typebox.tpl.ts b/packages/generators/src/authentication/templates/schema.typebox.tpl.ts index 0c80f0d527..909ec6ae5a 100644 --- a/packages/generators/src/authentication/templates/schema.typebox.tpl.ts +++ b/packages/generators/src/authentication/templates/schema.typebox.tpl.ts @@ -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'`)} @@ -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' diff --git a/packages/generators/src/service/templates/schema.json.tpl.ts b/packages/generators/src/service/templates/schema.json.tpl.ts index 272ffcaf3b..20951e9d06 100644 --- a/packages/generators/src/service/templates/schema.json.tpl.ts +++ b/packages/generators/src/service/templates/schema.json.tpl.ts @@ -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' @@ -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 diff --git a/packages/generators/src/service/templates/schema.typebox.tpl.ts b/packages/generators/src/service/templates/schema.typebox.tpl.ts index 7e8b90a5d8..66b6c38e26 100644 --- a/packages/generators/src/service/templates/schema.typebox.tpl.ts +++ b/packages/generators/src/service/templates/schema.typebox.tpl.ts @@ -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' @@ -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 diff --git a/packages/schema/src/hooks/validate.ts b/packages/schema/src/hooks/validate.ts index 2ca30e7900..7b76a8c8ab 100644 --- a/packages/schema/src/hooks/validate.ts +++ b/packages/schema/src/hooks/validate.ts @@ -22,7 +22,7 @@ export const validateQuery = (schema: Schema | Valid } catch (error: any) { throw error.ajv ? new BadRequest(error.message, error.errors) : error } - + if (typeof next === 'function') { return next() } diff --git a/packages/schema/src/json-schema.ts b/packages/schema/src/json-schema.ts index b951e68e21..78771e6e5d 100644 --- a/packages/schema/src/json-schema.ts +++ b/packages/schema/src/json-schema.ts @@ -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]) @@ -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) diff --git a/packages/schema/test/json-schema.test.ts b/packages/schema/test/json-schema.test.ts index 2e4450d925..a0bd2a7be5 100644 --- a/packages/schema/test/json-schema.test.ts +++ b/packages/schema/test/json-schema.test.ts @@ -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', @@ -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) + }) }) diff --git a/packages/typebox/src/index.ts b/packages/typebox/src/index.ts index eaabd2b907..3a0d90fb23 100644 --- a/packages/typebox/src/index.ts +++ b/packages/typebox/src/index.ts @@ -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 @@ -182,3 +178,6 @@ export const querySyntax = < options ) } + +export const ObjectIdSchema = () => + Type.Union([Type.String({ objectid: true }), Type.Object({}, { additionalProperties: false })]) diff --git a/packages/typebox/test/index.test.ts b/packages/typebox/test/index.test.ts index 5cc19f58ac..6d70a6bc19 100644 --- a/packages/typebox/test/index.test.ts +++ b/packages/typebox/test/index.test.ts @@ -1,4 +1,5 @@ import assert from 'assert' +import { ObjectId as MongoObjectId } from 'mongodb' import { Ajv } from '@feathersjs/schema' import { querySyntax, @@ -7,7 +8,7 @@ import { defaultAppConfiguration, getDataValidator, getValidator, - queryProperties + ObjectIdSchema } from '../src' describe('@feathersjs/schema/typebox', () => { @@ -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({})) @@ -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')