From b3e0b7bcdffde4451618e8a3c9d69d4071a66ca1 Mon Sep 17 00:00:00 2001 From: Siddharth-Dangi Date: Tue, 10 Nov 2026 00:48:45 +0530 Subject: [PATCH] fix(concerto-core): reject array identifying fields Identifying fields must uniquely identify a resource instance with a scalar value. Previously, ClassDeclaration.validate() only verified that identifying fields were strings (or scalar strings) and not optional, but did not check if the property was declared as an array (idField.isArray()). Consequently, models declaring array identifying fields (e.g. `o String[] names` or scalar string arrays, directly or through inheritance) loaded successfully at model load time, only to fail at runtime during instantiation or serialization. This change checks `idField.isArray()` in `ClassDeclaration.validate()` and throws an `IllegalModelException('Identifying fields cannot be arrays.')`. Signed-off-by: Siddharth-Dangi --- .../src/introspect/classdeclaration.ts | 4 ++ .../data/parser/assetdeclaration.arrayid.cto | 21 ++++++ .../test/introspect/assetdeclaration.js | 7 ++ .../test/introspect/classdeclaration.js | 67 +++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 packages/concerto-core/test/data/parser/assetdeclaration.arrayid.cto diff --git a/packages/concerto-core/src/introspect/classdeclaration.ts b/packages/concerto-core/src/introspect/classdeclaration.ts index e86fba57a9..d925a54a32 100644 --- a/packages/concerto-core/src/introspect/classdeclaration.ts +++ b/packages/concerto-core/src/introspect/classdeclaration.ts @@ -249,6 +249,10 @@ class ClassDeclaration extends Declaration { throw new IllegalModelException('Identifying fields cannot be optional.', this.modelFile, this.ast.location); } + if (idField.isArray()) { + throw new IllegalModelException('Identifying fields cannot be arrays.', this.modelFile, this.ast.location); + } + if(this.superType) { const superType = this.getModelFile().getType(this.superType); if (superType && superType.isIdentified() ) { diff --git a/packages/concerto-core/test/data/parser/assetdeclaration.arrayid.cto b/packages/concerto-core/test/data/parser/assetdeclaration.arrayid.cto new file mode 100644 index 0000000000..fcbb9495cd --- /dev/null +++ b/packages/concerto-core/test/data/parser/assetdeclaration.arrayid.cto @@ -0,0 +1,21 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace com.hyperledger.testing@1.0.0 + +import com.hyperledger.elsewhere@1.0.0.BaseAsset + +asset TestAsset identified by assetId { + o String[] assetId +} diff --git a/packages/concerto-core/test/introspect/assetdeclaration.js b/packages/concerto-core/test/introspect/assetdeclaration.js index 226d9ab6bf..c5dcbb85a5 100644 --- a/packages/concerto-core/test/introspect/assetdeclaration.js +++ b/packages/concerto-core/test/introspect/assetdeclaration.js @@ -98,6 +98,13 @@ describe('AssetDeclaration', () => { }).should.throw(/Identifying fields cannot be optional/); }); + it('should throw when identifying field is an array', () => { + let asset = loadAssetDeclaration('test/data/parser/assetdeclaration.arrayid.cto'); + (() => { + asset.validate(); + }).should.throw(/Identifying fields cannot be arrays/); + }); + it('should throw when field has been duplicated in the same class', () => { let asset = loadAssetDeclaration('test/data/parser/assetdeclaration.dupesimp.cto'); diff --git a/packages/concerto-core/test/introspect/classdeclaration.js b/packages/concerto-core/test/introspect/classdeclaration.js index 009cd4b841..2c549a435a 100644 --- a/packages/concerto-core/test/introspect/classdeclaration.js +++ b/packages/concerto-core/test/introspect/classdeclaration.js @@ -97,6 +97,73 @@ describe('ClassDeclaration', () => { const clazz = introspectUtils.loadLastDeclaration('test/data/parser/classdeclaration.scalararray.cto', ConceptDeclaration); clazz.validate(); }); + + it('should throw when a string array is used as an identifier', () => { + const mm = new ModelManager(); + (() => { + mm.addCTOModel(` +namespace com.testing@1.0.0 +asset Person identified by names { + o String[] names +} + `); + }).should.throw(/Identifying fields cannot be arrays/); + }); + + it('should throw when a scalar string array is used as an identifier', () => { + const mm = new ModelManager(); + (() => { + mm.addCTOModel(` +namespace com.testing@1.0.0 +scalar SSN extends String default="000-00-0000" regex=/\\d{3}-\\d{2}-\\d{4}/ +concept Person identified by ssnArray { + o SSN[] ssnArray +} + `); + }).should.throw(/Identifying fields cannot be arrays/); + }); + + it('should throw when an inherited string array is used as an identifier', () => { + const mm = new ModelManager(); + (() => { + mm.addCTOModel(` +namespace com.testing@1.0.0 +abstract asset Base { + o String[] ids +} +asset Child identified by ids extends Base { +} + `); + }).should.throw(/Identifying fields cannot be arrays/); + }); + + it('should throw when a participant is identified by an array', () => { + const mm = new ModelManager(); + (() => { + mm.addCTOModel(` +namespace com.testing@1.0.0 +participant Member identified by memberIds { + o String[] memberIds +} + `); + }).should.throw(/Identifying fields cannot be arrays/); + }); + + it('should throw when a 3rd-level subclass is identified by an array defined in a root class', () => { + const mm = new ModelManager(); + (() => { + mm.addCTOModel(` +namespace com.testing@1.0.0 +abstract concept Root { + o String[] codes +} +abstract concept Middle extends Root { +} +asset Leaf identified by codes extends Middle { +} + `); + }).should.throw(/Identifying fields cannot be arrays/); + }); }); describe('#accept', () => {