From 99c1fa914977f7ec87084686a239a20e29a6ff77 Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Tue, 21 Apr 2026 17:36:51 +0200 Subject: [PATCH 1/4] Add validation to ResourceId and ObjectId --- packages/codecs/src/parsers.ts | 4 ++-- .../tests/__snapshots__/parsers.test.ts.snap | 2 ++ packages/schema/package.json | 1 + packages/schema/src/json-schema/keywords.ts | 16 ++++++++++++++++ .../schema/src/validators/schema-validator.ts | 2 +- packages/schema/tests/schema-validation.test.ts | 14 ++++++++++++++ pnpm-lock.yaml | 3 +++ 7 files changed, 39 insertions(+), 3 deletions(-) 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..be4e3bc 100644 --- a/packages/schema/tests/schema-validation.test.ts +++ b/packages/schema/tests/schema-validation.test.ts @@ -154,6 +154,20 @@ describe('json-schema-validation', () => { expect(res2).toMatchSnapshot(); }); + it('should validate bsonObjectId fields', () => { + const validator = micro_schema.createSchemaValidator({ + type: 'object', + properties: { + id: { type: 'string', bsonObjectId: true } + }, + required: ['id'] + }); + + 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 From ec40a0f40528dd252c0962d72db63559952c0dcb Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Thu, 23 Apr 2026 10:51:04 +0200 Subject: [PATCH 2/4] Changeset --- .changeset/loose-dryers-lead.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/loose-dryers-lead.md diff --git a/.changeset/loose-dryers-lead.md b/.changeset/loose-dryers-lead.md new file mode 100644 index 0000000..d37e695 --- /dev/null +++ b/.changeset/loose-dryers-lead.md @@ -0,0 +1,6 @@ +--- +'@journeyapps-labs/micro-codecs': minor +'@journeyapps-labs/micro-schema': minor +--- + +Validate ObjectId and ResourceId.id match bson ObjectId format. From 6d70901c88289ed5def7d43bfb95086012bd6cf2 Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Thu, 23 Apr 2026 11:15:57 +0200 Subject: [PATCH 3/4] Switch to patch releases --- .changeset/loose-dryers-lead.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/loose-dryers-lead.md b/.changeset/loose-dryers-lead.md index d37e695..8625175 100644 --- a/.changeset/loose-dryers-lead.md +++ b/.changeset/loose-dryers-lead.md @@ -1,6 +1,6 @@ --- -'@journeyapps-labs/micro-codecs': minor -'@journeyapps-labs/micro-schema': minor +'@journeyapps-labs/micro-codecs': patch +'@journeyapps-labs/micro-schema': patch --- Validate ObjectId and ResourceId.id match bson ObjectId format. From 1f4965db4611a2819eb12f8e13c7a08341924669 Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Thu, 23 Apr 2026 16:23:11 +0200 Subject: [PATCH 4/4] Test ResourceId and ObjectId codecs directly --- .../schema/tests/schema-validation.test.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/schema/tests/schema-validation.test.ts b/packages/schema/tests/schema-validation.test.ts index be4e3bc..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,14 +155,16 @@ describe('json-schema-validation', () => { expect(res2).toMatchSnapshot(); }); - it('should validate bsonObjectId fields', () => { - const validator = micro_schema.createSchemaValidator({ - type: 'object', - properties: { - id: { type: 'string', bsonObjectId: true } - }, - required: ['id'] - }); + 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);