From 274bfe67e0875b2de711f6d92669638bc71b16df Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Mon, 29 Jun 2026 07:48:08 +0200 Subject: [PATCH] fix: Emit min/max values for numbers --- CHANGELOG.md | 1 + lib/compile/csdl2openapi.js | 4 + test/lib/compile/csdl2openapi.test.js | 101 ++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d34de98..8b73b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Deprecated ### Removed ### Fixed +- `@assert.range` (and `@Validation.Minimum`/`@Validation.Maximum`) on properties and action parameters of type `Edm.Int32`, `Edm.Int16`, `Edm.SByte`, and `Edm.Byte` now correctly produces `minimum`/`maximum` in the generated OpenAPI schema ### Security ## [1.5.0] - 2026-06-13 diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index 6c1ae31..136ee07 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -2578,6 +2578,8 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot if (s.$ref) s = { allOf: [s] }; if (isAnyOfSchema(s) && isNumberSchema(s.anyOf[0])) { s.anyOf[0].maximum = element[meta.voc.Validation.Maximum]; + } else if (isNumberSchema(s)) { + s.maximum = element[meta.voc.Validation.Maximum]; } // TODO: this implies that we could be handling an AnyOfSchema here. So exclusiveMinimum is attach to that? Or to its first element, as above? // @ts-expect-error @@ -2588,6 +2590,8 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot if (s.$ref) s = { allOf: [s] }; if (isAnyOfSchema(s) && isNumberSchema(s.anyOf[0])) { s.anyOf[0].minimum = element[meta.voc.Validation.Minimum]; + } else if (isNumberSchema(s)) { + s.minimum = element[meta.voc.Validation.Minimum]; } // TODO: see above // @ts-expect-error diff --git a/test/lib/compile/csdl2openapi.test.js b/test/lib/compile/csdl2openapi.test.js index eec0126..8507c4f 100644 --- a/test/lib/compile/csdl2openapi.test.js +++ b/test/lib/compile/csdl2openapi.test.js @@ -2892,6 +2892,107 @@ it("AllowedValues on various Edm types", () => { assert.deepStrictEqual(messages, [], "messages"); }); +it("Minimum and Maximum on plain integer and float types (non-anyOf schemas)", () => { + const csdl = { + $Reference: { + dummy: { + $Include: [ + { $Namespace: "Org.OData.Validation.V1", $Alias: "Validation" }, + ], + }, + }, + $EntityContainer: "svc.Container", + svc: { + Container: { sing: { $Type: "svc.entity" } }, + entity: { + $Kind: "EntityType", + int32: { $Type: "Edm.Int32" }, + int16: { $Type: "Edm.Int16" }, + sbyte: { $Type: "Edm.SByte" }, + byte: { $Type: "Edm.Byte" }, + single: { $Type: "Edm.Single" }, + int32ExclusiveMin: { $Type: "Edm.Int32" }, + int32ExclusiveMax: { $Type: "Edm.Int32" }, + }, + $Annotations: { + "svc.entity/int32": { + "@Validation.Minimum": 1, + "@Validation.Maximum": 500, + }, + "svc.entity/int16": { + "@Validation.Minimum": 0, + "@Validation.Maximum": 100, + }, + "svc.entity/sbyte": { + "@Validation.Minimum": -128, + "@Validation.Maximum": 127, + }, + "svc.entity/byte": { + "@Validation.Minimum": 0, + "@Validation.Maximum": 255, + }, + "svc.entity/single": { + "@Validation.Minimum": 0, + "@Validation.Maximum": 1, + }, + "svc.entity/int32ExclusiveMin": { + "@Validation.Minimum@Validation.Exclusive": true, + "@Validation.Minimum": 0, + }, + "svc.entity/int32ExclusiveMax": { + "@Validation.Maximum@Validation.Exclusive": true, + "@Validation.Maximum": 100, + }, + }, + }, + }; + + const openapi = lib.csdl2openapi(csdl, {}); + const props = openapi.components.schemas["svc.entity"].properties; + + assert.deepStrictEqual( + props.int32, + { type: "integer", format: "int32", minimum: 1, maximum: 500 }, + "int32 minimum and maximum" + ); + assert.deepStrictEqual( + props.int16, + { type: "integer", format: "int16", minimum: 0, maximum: 100 }, + "int16 minimum and maximum" + ); + assert.deepStrictEqual( + props.sbyte, + { type: "integer", format: "int8", minimum: -128, maximum: 127 }, + "sbyte minimum and maximum" + ); + assert.deepStrictEqual( + props.byte, + { type: "integer", format: "uint8", minimum: 0, maximum: 255 }, + "byte minimum and maximum" + ); + assert.deepStrictEqual( + props.single, + { + anyOf: [ + { type: "number", format: "float", minimum: 0, maximum: 1 }, + { type: "string" }, + ], + example: 3.14, + }, + "single anyOf[0] minimum and maximum" + ); + assert.strictEqual( + props.int32ExclusiveMin.exclusiveMinimum, + true, + "int32 exclusiveMinimum" + ); + assert.strictEqual( + props.int32ExclusiveMax.exclusiveMaximum, + true, + "int32 exclusiveMaximum" + ); +}); + it("Error Logging when name and title are missing", () => { const csdl = { name: undefined, title: undefined }; const actual = lib.csdl2openapi(csdl, {});