From c4068647aed3acffb4d7c3d7ce996267a7695065 Mon Sep 17 00:00:00 2001 From: awjh-ibm Date: Wed, 13 Mar 2019 14:25:16 +0000 Subject: [PATCH] [FAB-14537] components ref components Change-Id: I8123fbfe00e97630b3a6256f1b0b1349c8d9199b Signed-off-by: awjh-ibm --- fabric-contract-api/lib/annotations/object.js | 2 +- fabric-contract-api/lib/annotations/utils.js | 10 +- .../test/unit/annotations/utils.js | 13 ++ .../lib/contract-spi/chaincodefromcontract.js | 29 +-- fabric-shim/lib/contract-spi/datamarshall.js | 35 ++-- .../test/unit/contract-spi/datamarshall.js | 54 ++++-- test/fv/annotations.js | 13 +- .../src/test_contract/expected-metadata.json | 168 ++++++++++++------ .../src/test_contract/test_contract.ts | 68 +++++-- 9 files changed, 252 insertions(+), 140 deletions(-) diff --git a/fabric-contract-api/lib/annotations/object.js b/fabric-contract-api/lib/annotations/object.js index 8c5160c1..d8012d54 100644 --- a/fabric-contract-api/lib/annotations/object.js +++ b/fabric-contract-api/lib/annotations/object.js @@ -58,7 +58,7 @@ module.exports.Property = function Property (name, type) { properties.push({ name, - schema: utils.generateSchema(type) + schema: utils.generateSchema(type, false) }); Reflect.defineMetadata('fabric:object-properties', properties, target); diff --git a/fabric-contract-api/lib/annotations/utils.js b/fabric-contract-api/lib/annotations/utils.js index 7cbe22cb..c585b774 100644 --- a/fabric-contract-api/lib/annotations/utils.js +++ b/fabric-contract-api/lib/annotations/utils.js @@ -39,29 +39,29 @@ module.exports.findByValue = function findByValue(arr, primaryField, id) { return null; }; -const generateSchema = (type) => { +const generateSchema = (type, fullPath = true) => { if (isPrimitive(type)) { return { type: type.toLowerCase() }; } else if (isArray(type)) { - const subType = getSubArray(type); + const subType = getSubArray(type, fullPath); return { type: 'array', - items: generateSchema(subType) + items: generateSchema(subType, fullPath) }; } else if (isMap(type)) { const subType = getSubMap(type); return { type: 'object', - additionalProperties: generateSchema(subType) + additionalProperties: generateSchema(subType, fullPath) }; } return { - $ref: refPath + type + $ref: (fullPath ? refPath : '') + type }; }; module.exports.generateSchema = generateSchema; diff --git a/fabric-contract-api/test/unit/annotations/utils.js b/fabric-contract-api/test/unit/annotations/utils.js index b674cf6a..c255e12e 100644 --- a/fabric-contract-api/test/unit/annotations/utils.js +++ b/fabric-contract-api/test/unit/annotations/utils.js @@ -114,6 +114,12 @@ describe('utils', () => { }); }); + it ('should return a ref path for a non array and non primitive type and not use full path', () => { + expect(utils.generateSchema('Duck', false)).to.deep.equal({ + $ref: 'Duck' + }); + }); + it ('should recurse for array types', () => { expect(utils.generateSchema('Duck[]')).to.deep.equal({ type: 'array', @@ -129,6 +135,13 @@ describe('utils', () => { } }); + expect(utils.generateSchema('Array', false)).to.deep.equal({ + type: 'array', + items: { + $ref: 'Duck' + } + }); + expect(utils.generateSchema('Array')).to.deep.equal({ type: 'array', items: { diff --git a/fabric-shim/lib/contract-spi/chaincodefromcontract.js b/fabric-shim/lib/contract-spi/chaincodefromcontract.js index be0b5441..bad3cf36 100644 --- a/fabric-shim/lib/contract-spi/chaincodefromcontract.js +++ b/fabric-shim/lib/contract-spi/chaincodefromcontract.js @@ -83,33 +83,6 @@ class ChaincodeFromContract { * for arguments. */ _compileSchemas() { - - const schemaList = []; - for (const name in this.metadata.components.schemas) { - const s = this.metadata.components.schemas[name]; - const props = {}; - s.properties.forEach((e) => { - props[e.name] = e; - }); - - s.properties = props; - schemaList.push(s); - } - - if (schemaList.length > 0) { - // provide the list of schemas (of the complex types) to AJV, identified by name - - const ajv = this._ajv(schemaList); - // create validators for each complex type - // have lowercases the ids - this.contractImplementations.schemas = {}; - schemaList.forEach((e) => { - const id = e.$id; - this.contractImplementations.schemas[id] = {}; - this.contractImplementations.schemas[id].validator = ajv.getSchema(id); - }); - - } // final step is to setup up data marhsall instances const requestedSerializer = this.serializers.transaction; for (const contractName in this.contractImplementations) { @@ -121,7 +94,7 @@ class ChaincodeFromContract { } /* istanbul ignore next */ _dataMarshall(requestedSerializer) { - return new DataMarshall(requestedSerializer, this.serializers.serializers, this.contractImplementations.schemas); + return new DataMarshall(requestedSerializer, this.serializers.serializers, this.metadata.components.schemas); } /* istanbul ignore next */ _ajv(schemaList) { diff --git a/fabric-shim/lib/contract-spi/datamarshall.js b/fabric-shim/lib/contract-spi/datamarshall.js index bdf5ca0c..39b587ef 100644 --- a/fabric-shim/lib/contract-spi/datamarshall.js +++ b/fabric-shim/lib/contract-spi/datamarshall.js @@ -24,8 +24,8 @@ module.exports = class DataMarshall { * @param {String} requestedSerializer name of the requested serializer * @param {Object} serializers mapping of names to the implementation of the serializers */ - constructor(requestedSerializer, serializers, schemas) { - logger.debug('New DataMarshaller', requestedSerializer, serializers, schemas); + constructor(requestedSerializer, serializers, components) { + logger.debug('New DataMarshaller', requestedSerializer, serializers, components); let cnstr = serializers[requestedSerializer]; if (typeof cnstr === 'string') { cnstr = require(cnstr); @@ -41,7 +41,7 @@ module.exports = class DataMarshall { }); // the complex type schemas from the metadata - this.schemas = schemas; + this.components = components; } /** @@ -105,25 +105,28 @@ module.exports = class DataMarshall { logger.debug(`${loggerPrefix} Expected parameter ${expected}`); logger.debug(`${loggerPrefix} Supplied parameter ${supplied}`); // check the type - const schema = expected.schema; - - let validator; - - if (schema.type) { - validator = this.ajv.compile(schema); - } else if (schema.$ref) { - const n = schema.$ref.lastIndexOf('/'); - const typeName = schema.$ref.substring(n + 1); - validator = this.schemas[typeName].validator; - } else { - throw new Error(`Incorrect type information ${JSON.stringify(schema)}`); + const schema = { + properties: { + prop: expected.schema + }, + components: { + schemas: this.components + } + }; + + if (!expected.schema.type && !expected.schema.$ref) { + throw new Error(`Incorrect type information ${JSON.stringify(expected.schema)}`); } + const validator = this.ajv.compile(schema); + const {value, validateData} = this.fromWireBuffer(supplied, expected.schema, loggerPrefix); const valid = validator(validateData); if (!valid) { - const errors = JSON.stringify(validator.errors); + const errors = JSON.stringify(validator.errors.map((err) => { + return err.message; + })); logger.debug(`${loggerPrefix} ${errors}`); throw new Error(`Unable to validate parameter due to ${errors}`); } diff --git a/fabric-shim/test/unit/contract-spi/datamarshall.js b/fabric-shim/test/unit/contract-spi/datamarshall.js index 9af1757a..2387287d 100644 --- a/fabric-shim/test/unit/contract-spi/datamarshall.js +++ b/fabric-shim/test/unit/contract-spi/datamarshall.js @@ -142,7 +142,7 @@ describe('datamarshall.js', () => { let dm; beforeEach(() => { - dm = new DataMarshall('jsonSerializer', defaultSerialization.serializers); + dm = new DataMarshall('jsonSerializer', defaultSerialization.serializers, {}); dm.fromWireBuffer = sinon.stub() .onFirstCall().returns({value: 'some value', validateData: 'some validate data'}) @@ -198,14 +198,14 @@ describe('datamarshall.js', () => { ]}; const validateStub = sinon.stub().returns(false); - validateStub.errors = ['list', 'of', 'reasons', 'why', 'params', 'were', 'wrong']; + validateStub.errors = [{message: 'list'}, {message: 'of'}, {message: 'reasons'}, {message: 'why'}, {message: 'params'}, {message: 'were'}, {message: 'wrong'}]; dm.ajv.compile = sinon.stub().returns(validateStub); expect(() => { dm.handleParameters(fn, ['"one"'], 'logging prefix'); - }).to.throw(`Unable to validate parameter due to ${JSON.stringify(validateStub.errors)}`); + }).to.throw(`Unable to validate parameter due to ${JSON.stringify(validateStub.errors.map((err) => { return err.message; }))}`); // eslint-disable-line sinon.assert.calledWith(dm.fromWireBuffer, '"one"', {type: 'string'}, 'logging prefix'); - sinon.assert.calledWith(dm.ajv.compile, {type:'string'}); + sinon.assert.calledWith(dm.ajv.compile, {components: {schemas: {}}, properties: {prop: {type: 'string'}}}); sinon.assert.calledWith(validateStub, 'some validate data'); }); @@ -214,22 +214,31 @@ describe('datamarshall.js', () => { { name:'one', schema:{ - $ref:'#components/someComponent' + $ref:'#/components/schemas/someComponent' } } ]}; const validateStub = sinon.stub().returns(false); - validateStub.errors = ['list', 'of', 'reasons', 'why', 'params', 'were', 'wrong']; + validateStub.errors = [{message: 'list'}, {message: 'of'}, {message: 'reasons'}, {message: 'why'}, {message: 'params'}, {message: 'were'}, {message: 'wrong'}]; + dm.ajv.compile = sinon.stub().returns(validateStub); - dm.schemas = {}; - dm.schemas.someComponent = {}; - dm.schemas.someComponent.validator = validateStub; + dm.components = { + someComponent: { + $id: 'someComponent', + type: 'object', + properties: { + name: { + type: 'string' + } + } + } + }; expect(() => { dm.handleParameters(fn, ['"one"'], 'logging prefix'); - }).to.throw(`Unable to validate parameter due to ${JSON.stringify(validateStub.errors)}`); - sinon.assert.calledWith(dm.fromWireBuffer, '"one"', {$ref:'#components/someComponent'}, 'logging prefix'); + }).to.throw(`Unable to validate parameter due to ${JSON.stringify(validateStub.errors.map((err) => { return err.message; }))}`); // eslint-disable-line + sinon.assert.calledWith(dm.fromWireBuffer, '"one"', {$ref:'#/components/schemas/someComponent'}, 'logging prefix'); sinon.assert.calledWith(validateStub, 'some validate data'); }); @@ -244,7 +253,7 @@ describe('datamarshall.js', () => { { name:'two', schema:{ - $ref: '#components/someComponent' + $ref: '#/components/schemas/someComponent' } } ]}; @@ -253,18 +262,27 @@ describe('datamarshall.js', () => { dm.ajv.compile = sinon.stub().returns(validateStub); - dm.schemas = {}; - dm.schemas.someComponent = {}; - dm.schemas.someComponent.validator = validateStub; + dm.components = { + someComponent: { + $id: 'someComponent', + type: 'object', + properties: { + name: { + type: 'string' + } + } + } + }; const returned = dm.handleParameters(fn, ['"one"', '"two"'], 'logging prefix'); sinon.assert.calledTwice(dm.fromWireBuffer); sinon.assert.calledWith(dm.fromWireBuffer, '"one"', {type: 'string'}, 'logging prefix'); - sinon.assert.calledWith(dm.fromWireBuffer, '"two"', {$ref: '#components/someComponent'}, 'logging prefix'); + sinon.assert.calledWith(dm.fromWireBuffer, '"two"', {$ref: '#/components/schemas/someComponent'}, 'logging prefix'); - sinon.assert.calledOnce(dm.ajv.compile); - sinon.assert.calledWith(dm.ajv.compile, {type:'string'}); + sinon.assert.calledTwice(dm.ajv.compile); + sinon.assert.calledWith(dm.ajv.compile, {components: {schemas: dm.components}, properties: {prop: {type: 'string'}}}); + sinon.assert.calledWith(dm.ajv.compile, {components: {schemas: dm.components}, properties: {prop: {$ref: '#/components/schemas/someComponent'}}}); sinon.assert.calledTwice(validateStub); sinon.assert.calledWith(validateStub, 'some validate data'); diff --git a/test/fv/annotations.js b/test/fv/annotations.js index 70898aa0..2381fad7 100644 --- a/test/fv/annotations.js +++ b/test/fv/annotations.js @@ -5,7 +5,7 @@ const chai = require('chai'); chai.use(require('chai-as-promised')); const expect = chai.expect; const utils = require('./utils'); -const {SHORT_INC, LONG_STEP} = utils.TIMEOUTS; +const {SHORT_INC, SHORT_STEP, LONG_STEP} = utils.TIMEOUTS; describe('Typescript chaincode', () => { const suite = 'annotations'; @@ -18,10 +18,17 @@ describe('Typescript chaincode', () => { describe('Scenario', () => { it('should write an asset', async function () { this.timeout(SHORT_INC); - await utils.invoke(suite, 'TestContract:createAsset', ['GLD', 'GOLD_BAR', '100']); + await utils.invoke(suite, 'TestContract:createAsset', ['GLD', 'GOLD_BAR', '100', 'EXTRA_ID', '50']); const payload = JSON.parse(await utils.query(suite, 'TestContract:getAsset', ['GLD'])); - expect(payload).to.eql({id: 'GLD', name: 'GOLD_BAR', value: 100}); + expect(payload).to.eql({id: 'GLD', name: 'GOLD_BAR', value: 100, extra: {id: 'EXTRA_ID', value: 50}}); + }); + + it ('should update an asset', async function() { + this.timeout(SHORT_STEP); + await utils.invoke(suite, 'TestContract:updateAsset', [JSON.stringify({id: 'GLD', name: 'GOLD_BAR', value: 200, extra: {id: 'EXTRA_ID', value: 100}})]); + const payload = JSON.parse(await utils.query(suite, 'TestContract:getAsset', ['GLD'])); + expect(payload).to.eql({id: 'GLD', name: 'GOLD_BAR', value: 200, extra: {id: 'EXTRA_ID', value: 100}}); }); it('should handle the getMetadata', async function () { diff --git a/test/fv/annotations/src/test_contract/expected-metadata.json b/test/fv/annotations/src/test_contract/expected-metadata.json index c8a9d716..784ecde9 100644 --- a/test/fv/annotations/src/test_contract/expected-metadata.json +++ b/test/fv/annotations/src/test_contract/expected-metadata.json @@ -1,118 +1,170 @@ { "$schema": "https://fabric-shim.github.io/release-1.4/contract-schema.json", - "components": { - "schemas": { - "Asset": { - "$id": "Asset", - "additionalProperties": false, - "properties": [ - { - "name": "id", - "schema": { - "type": "string" - } - }, - { - "name": "name", - "schema": { - "type": "string" - } - }, - { - "name": "value", - "schema": { - "type": "number" - } - } - ], - "type": "object" - } - } - }, "contracts": { "TestContract": { + "name": "TestContract", "contractInstance": { "name": "TestContract", - "default":true - }, - "info": { - "title": "", - "version": "" + "default": true }, - "name": "TestContract", "transactions": [ { - "name": "createAsset", + "tag": [ + "submitTx" + ], "parameters": [ { - "description": "", "name": "id", + "description": "", "schema": { "type": "string" } }, { - "description": "", "name": "name", + "description": "", "schema": { "type": "string" } }, { - "description": "", "name": "value", + "description": "", + "schema": { + "type": "number" + } + }, + { + "name": "extraID", + "description": "", + "schema": { + "type": "string" + } + }, + { + "name": "extraValue", + "description": "", "schema": { "type": "number" } } ], - "tag": [ - "submitTx" - ] + "name": "createAsset" }, { - "name": "getAsset", + "tag": [ + "submitTx" + ], "parameters": [ { + "name": "asset", "description": "", - "name": "id", "schema": { - "type": "string" + "$ref": "#/components/schemas/Asset" } } ], + "name": "updateAsset" + }, + { "returns": [ { "name": "success", "schema": { - "type": "string" + "$ref": "#/components/schemas/Asset" } } ], - "tag": [ - "submitTx" + "name": "getAsset", + "tag": [], + "parameters": [ + { + "name": "id", + "description": "", + "schema": { + "type": "string" + } + } ] } - ] + ], + "info": { + "title": "", + "version": "" + } }, "org.hyperledger.fabric": { + "name": "org.hyperledger.fabric", "contractInstance": { "name": "org.hyperledger.fabric" }, - "info": { - "title": "", - "version": "" - }, - "name": "org.hyperledger.fabric", "transactions": [ { "name": "GetMetadata" } - ] + ], + "info": { + "title": "", + "version": "" + } } }, "info": { - "title": "ts_chaincode", - "version": "1.0.0" + "version": "1.0.0", + "title": "ts_chaincode" + }, + "components": { + "schemas": { + "SomethingThatCouldBeAProperty": { + "$id": "SomethingThatCouldBeAProperty", + "type": "object", + "additionalProperties": false, + "properties": [ + { + "name": "id", + "schema": { + "type": "string" + } + }, + { + "name": "value", + "schema": { + "type": "number" + } + } + ] + }, + "Asset": { + "$id": "Asset", + "type": "object", + "additionalProperties": false, + "properties": [ + { + "name": "id", + "schema": { + "type": "string" + } + }, + { + "name": "name", + "schema": { + "type": "string" + } + }, + { + "name": "value", + "schema": { + "type": "number" + } + }, + { + "name": "extra", + "schema": { + "$ref": "SomethingThatCouldBeAProperty" + } + } + ] + } + } } -} +} \ No newline at end of file diff --git a/test/fv/annotations/src/test_contract/test_contract.ts b/test/fv/annotations/src/test_contract/test_contract.ts index 9bd6ef3c..25beb4ca 100644 --- a/test/fv/annotations/src/test_contract/test_contract.ts +++ b/test/fv/annotations/src/test_contract/test_contract.ts @@ -1,5 +1,38 @@ import { Contract, Context, Transaction, Returns, Object, Property } from 'fabric-contract-api'; +@Object() +class SomethingThatCouldBeAProperty { + @Property() + public id: string; + + @Property() + public value: number; + + + + constructor(id: string, value: number) { + this.id = id; + this.value = value; + } + + serialize():string { + return JSON.stringify({ + id: this.id, + value: this.value + }); + } + + static deserialize(stringifed: string):SomethingThatCouldBeAProperty { + const json = JSON.parse(stringifed); + + if (!json.hasOwnProperty('id') || !json.hasOwnProperty('value')) { + throw new Error('Was not JSON formatted asset'); + } + + return new SomethingThatCouldBeAProperty(json.id, json.value); + } +} + @Object() class Asset { static stateIdentifier: string = 'asset'; @@ -13,28 +46,33 @@ class Asset { @Property() public value: number; - constructor(id: string, name: string, value: number) { + @Property() + public extra: SomethingThatCouldBeAProperty; + + constructor(id: string, name: string, value: number, extra: SomethingThatCouldBeAProperty) { this.id = id; this.name = name; this.value = value; + this.extra = extra; } serialize():string { return JSON.stringify({ id: this.id, name: this.name, - value: this.value + value: this.value, + extra: this.extra.serialize() }); } static deserialize(stringifed: string):Asset { const json = JSON.parse(stringifed); - if (!json.hasOwnProperty('id') || !json.hasOwnProperty('name')) { + if (!json.hasOwnProperty('id') || !json.hasOwnProperty('name')|| !json.hasOwnProperty('value')) { throw new Error('Was not JSON formatted asset'); } - return new Asset(json.id, json.name, json.value); + return new Asset(json.id, json.name, json.value, SomethingThatCouldBeAProperty.deserialize(json.extra)); } } @@ -44,21 +82,29 @@ export default class TestContract extends Contract { } @Transaction() - public async createAsset(ctx: Context, id: string, name: string, value: number) { - const asset = new Asset(id, name, value); + public async createAsset(ctx: Context, id: string, name: string, value: number, extraID: string, extraValue: number) { + const asset = new Asset(id, name, value, new SomethingThatCouldBeAProperty(extraID, extraValue)); await ctx.stub.putState(ctx.stub.createCompositeKey(Asset.stateIdentifier, [asset.id]), Buffer.from(asset.serialize())) } @Transaction() - @Returns("string") - public async getAsset(ctx: Context, id: string) { + public async updateAsset(ctx: Context, asset: Asset) { + const existingAsset = await this.getAsset(ctx, asset.id); - const json = await ctx.stub.getState(ctx.stub.createCompositeKey(Asset.stateIdentifier, [id])) + existingAsset.value = asset.value; + existingAsset.extra.value = asset.extra.value; - Asset.deserialize(json.toString()); + await ctx.stub.putState(ctx.stub.createCompositeKey(Asset.stateIdentifier, [asset.id]), Buffer.from(existingAsset.serialize())) + } + + @Transaction(false) + @Returns("Asset") + public async getAsset(ctx: Context, id: string): Promise { + + const json = await ctx.stub.getState(ctx.stub.createCompositeKey(Asset.stateIdentifier, [id])) - return json.toString(); + return Asset.deserialize(json.toString()); } public async ignoreMe(ctx: Context, id: string) {