diff --git a/.changeset/loose-dryers-lead.md b/.changeset/loose-dryers-lead.md new file mode 100644 index 0000000..8625175 --- /dev/null +++ b/.changeset/loose-dryers-lead.md @@ -0,0 +1,6 @@ +--- +'@journeyapps-labs/micro-codecs': patch +'@journeyapps-labs/micro-schema': patch +--- + +Validate ObjectId and ResourceId.id match bson ObjectId format. diff --git a/packages/codecs/src/parsers.ts b/packages/codecs/src/parsers.ts index 038636e..ea1db3f 100644 --- a/packages/codecs/src/parsers.ts +++ b/packages/codecs/src/parsers.ts @@ -4,7 +4,7 @@ import * as t from 'ts-codec'; export const ObjectIdParser = t.createParser(codecs.ObjectId._tag, (_, { target }) => { switch (target) { case t.TransformTarget.Encoded: { - return { type: 'string' }; + return { type: 'string', bsonObjectId: true }; } case t.TransformTarget.Decoded: { return { bsonType: 'ObjectId' }; @@ -18,7 +18,7 @@ export const ResourceIdParser = t.createParser(codecs. return { type: 'object', properties: { - id: { type: 'string' } + id: { type: 'string', bsonObjectId: true } }, required: ['id'] }; diff --git a/packages/codecs/tests/__snapshots__/parsers.test.ts.snap b/packages/codecs/tests/__snapshots__/parsers.test.ts.snap index ae79196..ca4d255 100644 --- a/packages/codecs/tests/__snapshots__/parsers.test.ts.snap +++ b/packages/codecs/tests/__snapshots__/parsers.test.ts.snap @@ -128,6 +128,7 @@ exports[`parsers > should correctly generate ObjectId schemas 1`] = ` "definitions": {}, }, "encoded": { + "bsonObjectId": true, "definitions": {}, "type": "string", }, @@ -152,6 +153,7 @@ exports[`parsers > should correctly generate ResourceId schemas 1`] = ` "definitions": {}, "properties": { "id": { + "bsonObjectId": true, "type": "string", }, }, diff --git a/packages/schema/package.json b/packages/schema/package.json index 55a4ea8..b94e577 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -25,6 +25,7 @@ "@journeyapps-labs/micro-errors": "workspace:^", "ajv": "^8.17.1", "better-ajv-errors": "^2.0.2", + "bson": "^6.10.4", "ts-codec": "^1.3.0", "zod": "^4.1.12" }, diff --git a/packages/schema/src/json-schema/keywords.ts b/packages/schema/src/json-schema/keywords.ts index 06ee104..fb5882b 100644 --- a/packages/schema/src/json-schema/keywords.ts +++ b/packages/schema/src/json-schema/keywords.ts @@ -1,4 +1,20 @@ import * as ajv from 'ajv'; +import { ObjectId } from 'bson'; + +export const ObjectIdKeyword: ajv.KeywordDefinition = { + keyword: 'bsonObjectId', + metaSchema: { type: 'boolean' }, + error: { + message: 'should be a valid ObjectId string' + }, + code(context) { + const fn = context.gen.scopeValue('func', { + ref: ObjectId.isValid, + code: ajv._`require('bson').ObjectId.isValid` + }); + context.fail(ajv._`!${fn}(${context.data})`); + } +}; export const BufferNodeType: ajv.KeywordDefinition = { keyword: 'nodeType', diff --git a/packages/schema/src/validators/schema-validator.ts b/packages/schema/src/validators/schema-validator.ts index 270dc4c..a3249fa 100644 --- a/packages/schema/src/validators/schema-validator.ts +++ b/packages/schema/src/validators/schema-validator.ts @@ -38,7 +38,7 @@ export const createSchemaValidator = ( try { const ajv = new AJV({ allErrors: !(params.fail_fast ?? false), - keywords: [keywords.BufferNodeType], + keywords: [keywords.BufferNodeType, keywords.ObjectIdKeyword], ...(params.ajv || {}) }); diff --git a/packages/schema/tests/schema-validation.test.ts b/packages/schema/tests/schema-validation.test.ts index 0a48344..ab6b693 100644 --- a/packages/schema/tests/schema-validation.test.ts +++ b/packages/schema/tests/schema-validation.test.ts @@ -2,6 +2,7 @@ import { describe, test, it, expect } from 'vitest'; import base_schema from './fixtures/schema'; import * as micro_schema from '../src'; +import * as codecs from '@journeyapps-labs/micro-codecs'; const base_validator = micro_schema.createSchemaValidator(base_schema); @@ -154,6 +155,22 @@ describe('json-schema-validation', () => { expect(res2).toMatchSnapshot(); }); + it('should validate bsonObjectId fields for ObjectId', () => { + const validator = micro_schema.createTsCodecValidator(codecs.ObjectId); + + expect(validator.validate('507f1f77bcf86cd799439011').valid).toBe(true); + expect(validator.validate('not-an-objectid').valid).toBe(false); + expect(validator.validate('507f1f77bcf86cd79943901').valid).toBe(false); + }); + + it('should validate bsonObjectId fields for ResourceId', () => { + const validator = micro_schema.createTsCodecValidator(codecs.ResourceId); + + expect(validator.validate({ id: '507f1f77bcf86cd799439011' }).valid).toBe(true); + expect(validator.validate({ id: 'not-an-objectid' }).valid).toBe(false); + expect(validator.validate({ id: '507f1f77bcf86cd79943901' }).valid).toBe(false); + }); + it('should fail to compile invalid node types', () => { try { micro_schema.createSchemaValidator({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e0ef0c..7de29ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: better-ajv-errors: specifier: ^2.0.2 version: 2.0.2(ajv@8.17.1) + bson: + specifier: ^6.10.4 + version: 6.10.4 ts-codec: specifier: ^1.3.0 version: 1.3.0