From bc9e33df81956ceae7ad13aac8fb40a8c8d49250 Mon Sep 17 00:00:00 2001 From: Simon Stone Date: Wed, 11 Apr 2018 22:30:48 +0100 Subject: [PATCH] Enable simple JSON.stringify serialization of resources (resolves #3092) (#3600) * Enable simple JSON.stringify serialization of resources (resolves #3092) Signed-off-by: Simon Stone * Fix the default serializer options for registry actions Signed-off-by: Simon Stone --- packages/composer-common/api.txt | 4 + packages/composer-common/changelog.txt | 4 + .../lib/businessnetworkdefinition.js | 21 +- .../composer-common/lib/model/resource.js | 10 + packages/composer-common/lib/modelmanager.js | 33 +- packages/composer-common/lib/serializer.js | 35 +- .../test/businessnetworkdefinition.js | 4 +- .../composer-common/test/model/resource.js | 60 +++- packages/composer-common/test/modelmanager.js | 29 +- packages/composer-common/test/serializer.js | 46 +++ packages/composer-runtime/lib/api.js | 3 +- .../composer-runtime/lib/api/serializer.js | 11 +- packages/composer-runtime/lib/context.js | 2 + packages/composer-runtime/lib/registry.js | 311 +++++++----------- packages/composer-runtime/test/api.js | 4 +- .../composer-runtime/test/api/serializer.js | 28 +- packages/composer-runtime/test/context.js | 8 + packages/composer-runtime/test/registry.js | 8 +- .../composer-tests-functional/scripts/http.js | 38 ++- .../systest/data/transactions.http.cto | 21 +- .../systest/data/transactions.http.js | 49 ++- .../systest/transactions.http.js | 49 ++- .../jekylldocs/integrating/call-out.md | 10 +- 23 files changed, 505 insertions(+), 283 deletions(-) diff --git a/packages/composer-common/api.txt b/packages/composer-common/api.txt index dacde7b87e..c4735c221a 100644 --- a/packages/composer-common/api.txt +++ b/packages/composer-common/api.txt @@ -201,6 +201,7 @@ class Relationship extends Identifiable { class Resource extends Identifiable { + String toString() + boolean isResource() + + Object toJSON() } class Typed { + string getType() @@ -237,9 +238,12 @@ class ModelManager { + ParticipantDeclaration[] getParticipantDeclarations(Boolean) + EnumDeclaration[] getEnumDeclarations(Boolean) + ConceptDeclaration[] getConceptDeclarations(Boolean) + + Factory getFactory() + + Serializer getSerializer() } class Serializer { + void constructor(Factory,ModelManager) + + void setDefaultOptions(Object) + Object toJSON(Resource,Object,boolean,boolean,boolean,boolean) throws Error + Resource fromJSON(Object,Object,boolean,boolean) } diff --git a/packages/composer-common/changelog.txt b/packages/composer-common/changelog.txt index 5e181fbd6a..2b06ce966e 100644 --- a/packages/composer-common/changelog.txt +++ b/packages/composer-common/changelog.txt @@ -11,6 +11,10 @@ # # Note that the latest public API is documented using JSDocs and is available in api.txt. # + +Version 0.19.1 {f6814276d318be3b4ed0f6212bc77c6f} 2018-04-06 +- Enable simple JSON.stringify serialization of resources + Version 0.18.2 {354087f4e85b14e02e7c8284cca2583f} 2018-03-20 - Add CouchDB index compiler diff --git a/packages/composer-common/lib/businessnetworkdefinition.js b/packages/composer-common/lib/businessnetworkdefinition.js index 6c4f8cd806..351e34f81c 100644 --- a/packages/composer-common/lib/businessnetworkdefinition.js +++ b/packages/composer-common/lib/businessnetworkdefinition.js @@ -16,27 +16,26 @@ const AclFile = require('./acl/aclfile'); const AclManager = require('./aclmanager'); -const QueryFile = require('./query/queryfile'); -const QueryManager = require('./querymanager'); const BusinessNetworkMetadata = require('./businessnetworkmetadata'); -const Factory = require('./factory'); const fs = require('fs'); const fsPath = require('path'); const Introspector = require('./introspect/introspector'); const JSZip = require('jszip'); const Logger = require('./log/logger'); -const ModelManager = require('./modelmanager'); const minimatch = require('minimatch'); +const ModelManager = require('./modelmanager'); +const QueryFile = require('./query/queryfile'); +const QueryManager = require('./querymanager'); const ScriptManager = require('./scriptmanager'); const semver = require('semver'); -const Serializer = require('./serializer'); -const nodeUtil = require('util'); +const thenify = require('thenify'); +const util = require('util'); + const ENCODING = 'utf8'; const LOG = Logger.getLog('BusinessNetworkDefinition'); - -const thenify = require('thenify'); const mkdirp = thenify(require('mkdirp')); + /** define a help function that will filter out files * that are inside a node_modules directory under the path * we are processing @@ -116,12 +115,12 @@ class BusinessNetworkDefinition { } this.modelManager = new ModelManager(); + this.factory = this.modelManager.getFactory(); + this.serializer = this.modelManager.getSerializer(); this.aclManager = new AclManager(this.modelManager); this.queryManager = new QueryManager(this.modelManager); this.scriptManager = new ScriptManager(this.modelManager); this.introspector = new Introspector(this.modelManager); - this.factory = new Factory(this.modelManager); - this.serializer = new Serializer(this.factory, this.modelManager); this.metadata = new BusinessNetworkMetadata(packageJson,readme); LOG.exit(method); @@ -718,7 +717,7 @@ class BusinessNetworkDefinition { mode: createFileMode }; - const writeFile = nodeUtil.promisify(fs.writeFile); + const writeFile = util.promisify(fs.writeFile); const promises = []; diff --git a/packages/composer-common/lib/model/resource.js b/packages/composer-common/lib/model/resource.js index 2bb62c2663..8a43a2db5b 100644 --- a/packages/composer-common/lib/model/resource.js +++ b/packages/composer-common/lib/model/resource.js @@ -71,6 +71,16 @@ class Resource extends Identifiable { return true; } + /** + * Serialize this resource into a JavaScript object suitable for serialization to JSON, + * using the default options for the serializer. If you need to set additional options + * for the serializer, use the {@link Serializer#toJSON} method instead. + * @return {Object} A JavaScript object suitable for serialization to JSON. + */ + toJSON() { + return this.getModelManager().getSerializer().toJSON(this); + } + } module.exports = Resource; diff --git a/packages/composer-common/lib/modelmanager.js b/packages/composer-common/lib/modelmanager.js index 9e981f53fc..7c01528bf4 100644 --- a/packages/composer-common/lib/modelmanager.js +++ b/packages/composer-common/lib/modelmanager.js @@ -14,18 +14,19 @@ 'use strict'; +const DefaultModelFileLoader = require('./introspect/loaders/defaultmodelfileloader'); +const Factory = require('./factory'); const Globalize = require('./globalize'); - const IllegalModelException = require('./introspect/illegalmodelexception'); -const ModelUtil = require('./modelutil'); +const Logger = require('./log/logger'); const ModelFile = require('./introspect/modelfile'); -const TypeNotFoundException = require('./typenotfoundexception'); - -const DefaultModelFileLoader = require('./introspect/loaders/defaultmodelfileloader'); const ModelFileDownloader = require('./introspect/loaders/modelfiledownloader'); - -const LOG = require('./log/logger').getLog('ModelManager'); +const ModelUtil = require('./modelutil'); +const Serializer = require('./serializer'); const SYSTEM_MODELS = require('./systemmodel'); +const TypeNotFoundException = require('./typenotfoundexception'); + +const LOG = Logger.getLog('ModelManager'); /** * Manages the Composer model files. @@ -61,6 +62,8 @@ class ModelManager { LOG.entry('constructor'); this.modelFiles = {}; this.addSystemModels(); + this.factory = new Factory(this); + this.serializer = new Serializer(this.factory, this); LOG.exit('constructor'); } @@ -539,6 +542,22 @@ class ModelManager { }, []); } + /** + * Get a factory for creating new instances of types defined in this model manager. + * @return {Factory} A factory for creating new instances of types defined in this model manager. + */ + getFactory() { + return this.factory; + } + + /** + * Get a serializer for serializing instances of types defined in this model manager. + * @return {Serializer} A serializer for serializing instances of types defined in this model manager. + */ + getSerializer() { + return this.serializer; + } + } module.exports = ModelManager; diff --git a/packages/composer-common/lib/serializer.js b/packages/composer-common/lib/serializer.js index a517628cbb..c39a515970 100644 --- a/packages/composer-common/lib/serializer.js +++ b/packages/composer-common/lib/serializer.js @@ -26,6 +26,10 @@ const TransactionDeclaration = require('./introspect/transactiondeclaration'); const TypedStack = require('./serializer/typedstack'); const JSONWriter = require('./codegen/jsonwriter'); +const baseDefaultOptions = { + validate: true +}; + /** * Serialize Resources instances to/from various formats for long-term storage * (e.g. on the blockchain). @@ -34,6 +38,7 @@ const JSONWriter = require('./codegen/jsonwriter'); * @memberof module:composer-common */ class Serializer { + /** * Create a Serializer. * Note: Only to be called by framework code. Applications should @@ -52,6 +57,16 @@ class Serializer { this.factory = factory; this.modelManager = modelManager; + this.defaultOptions = Object.assign({}, baseDefaultOptions); + } + + /** + * Set the default options for the serializer. + * @param {Object} newDefaultOptions The new default options for the serializer. + */ + setDefaultOptions(newDefaultOptions) { + // Combine the specified default options with the base default + this.defaultOptions = Object.assign({}, baseDefaultOptions, newDefaultOptions); } /** @@ -60,14 +75,14 @@ class Serializer { * peristent storage. *

* @param {Resource} resource - The instance to convert to JSON - * @param {Object} options - the optional serialization options. - * @param {boolean} options.validate - validate the structure of the Resource + * @param {Object} [options] - the optional serialization options. + * @param {boolean} [options.validate] - validate the structure of the Resource * with its model prior to serialization (default to true) - * @param {boolean} options.convertResourcesToRelationships - Convert resources that + * @param {boolean} [options.convertResourcesToRelationships] - Convert resources that * are specified for relationship fields into relationships, false by default. - * @param {boolean} options.permitResourcesForRelationships - Permit resources in the + * @param {boolean} [options.permitResourcesForRelationships] - Permit resources in the * place of relationships (serializing them as resources), false by default. - * @param {boolean} options.deduplicateResources - Generate $id for resources and + * @param {boolean} [options.deduplicateResources] - Generate $id for resources and * if a resources appears multiple times in the object graph only the first instance is * serialized in full, subsequent instances are replaced with a reference to the $id * @return {Object} - The Javascript Object that represents the resource @@ -88,10 +103,7 @@ class Serializer { const classDeclaration = this.modelManager.getType( resource.getFullyQualifiedType() ); // validate the resource against the model - options = options || {}; - if(options.validate === undefined) { - options.validate = true; - } + options = options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions; if(options.validate) { const validator = new ResourceValidator(options); classDeclaration.accept(validator, parameters); @@ -143,10 +155,7 @@ class Serializer { const classDeclaration = this.modelManager.getType(jsonObject.$class); // default the options. - options = options || {}; - if(options.validate === undefined) { - options.validate = true; - } + options = options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions; // create a new instance, using the identifier field name as the ID. let resource; diff --git a/packages/composer-common/test/businessnetworkdefinition.js b/packages/composer-common/test/businessnetworkdefinition.js index 6cfb3fa775..d4bd21e78b 100644 --- a/packages/composer-common/test/businessnetworkdefinition.js +++ b/packages/composer-common/test/businessnetworkdefinition.js @@ -119,11 +119,11 @@ describe('BusinessNetworkDefinition', () => { }); it('should be able to retrieve acl manager', () => { - businessNetworkDefinition.getAclManager.should.not.be.null; + businessNetworkDefinition.getAclManager().should.not.be.null; }); it('should be able to retrieve query manager', () => { - businessNetworkDefinition.getQueryManager.should.not.be.null; + businessNetworkDefinition.getQueryManager().should.not.be.null; }); it('should be able to retrieve identifier', () => { diff --git a/packages/composer-common/test/model/resource.js b/packages/composer-common/test/model/resource.js index 0418a79a63..d6772600be 100644 --- a/packages/composer-common/test/model/resource.js +++ b/packages/composer-common/test/model/resource.js @@ -31,50 +31,76 @@ describe('Resource', function () { o String vin -->Person owner } + transaction ScrapCar { + -->Car car + } `; let modelManager = null; - let classDecl = null; - - before(function () { - modelManager = new ModelManager(); - }); beforeEach(function () { + modelManager = new ModelManager(); modelManager.addModelFile(levelOneModel); - classDecl = modelManager.getType('org.acme.l1.Person'); - }); - - afterEach(function () { - modelManager.clearModelFiles(); }); describe('#getClassDeclaration', function() { it('should return the class declaraction', function () { + const classDecl = modelManager.getType('org.acme.l1.Person'); const resource = new Resource(modelManager, classDecl, 'org.acme.l1', 'Person', '123' ); resource.getClassDeclaration().should.equal(classDecl); }); }); describe('#toJSON', () => { - it('should throw is toJSON is called', function () { - const resource = new Resource(modelManager, 'org.acme.l1', 'Person', '123' ); - (function () { - resource.toJSON(); - }).should.throw(/Use Serializer.toJSON to convert resource instances to JSON objects./); + it('should serialize an asset to a JavaScript object', function () { + const classDecl = modelManager.getType('org.acme.l1.Car'); + const resource = new Resource(modelManager, classDecl, 'org.acme.l1', 'Car', '456' ); + resource.vin = '456'; + resource.owner = modelManager.getFactory().newRelationship('org.acme.l1', 'Person', '123'); + resource.toJSON().should.deep.equal({ + $class: 'org.acme.l1.Car', + owner: 'resource:org.acme.l1.Person#123', + vin: '456' + }); + }); + + it('should serialize a participant to a JavaScript object', function () { + const classDecl = modelManager.getType('org.acme.l1.Person'); + const resource = new Resource(modelManager, classDecl, 'org.acme.l1', 'Person', '123' ); + resource.ssn = '123'; + resource.toJSON().should.deep.equal({ + $class: 'org.acme.l1.Person', + ssn: '123' + }); + }); + + it('should serialize a transaction to a JavaScript object', function () { + const classDecl = modelManager.getType('org.acme.l1.ScrapCar'); + const resource = new Resource(modelManager, classDecl, 'org.acme.l1', 'ScrapCar', '789' ); + resource.transactionId = '789'; + resource.timestamp = new Date(0); + resource.car = modelManager.getFactory().newRelationship('org.acme.l1', 'Car', '456'); + resource.toJSON().should.deep.equal({ + $class: 'org.acme.l1.ScrapCar', + car: 'resource:org.acme.l1.Car#456', + timestamp: '1970-01-01T00:00:00.000Z', + transactionId: '789' + }); }); }); describe('#isRelationship', () => { it('should be false', () => { - const resource = new Resource(modelManager, 'org.acme.l1', 'Person', '123' ); + const classDecl = modelManager.getType('org.acme.l1.Person'); + const resource = new Resource(modelManager, classDecl, 'org.acme.l1', 'Person', '123' ); resource.isRelationship().should.be.false; }); }); describe('#isResource', () => { it('should be true', () => { - const resource = new Resource(modelManager, 'org.acme.l1', 'Person', '123' ); + const classDecl = modelManager.getType('org.acme.l1.Person'); + const resource = new Resource(modelManager, classDecl, 'org.acme.l1', 'Person', '123' ); resource.isResource().should.be.true; }); }); diff --git a/packages/composer-common/test/modelmanager.js b/packages/composer-common/test/modelmanager.js index 7bf67fe838..cb5fbed86a 100644 --- a/packages/composer-common/test/modelmanager.js +++ b/packages/composer-common/test/modelmanager.js @@ -15,17 +15,18 @@ 'use strict'; const AssetDeclaration = require('../lib/introspect/assetdeclaration'); +const ConceptDeclaration = require('../lib/introspect/conceptdeclaration'); const EnumDeclaration = require('../lib/introspect/enumdeclaration'); +const EventDeclaration = require('../lib/introspect/eventdeclaration'); +const Factory = require('../lib/factory'); +const fs = require('fs'); const ModelFile = require('../lib/introspect/modelfile'); +const ModelFileDownloader = require('../lib/introspect/loaders/modelfiledownloader'); const ModelManager = require('../lib/modelmanager'); const ParticipantDeclaration = require('../lib/introspect/participantdeclaration'); -const EventDeclaration = require('../lib/introspect/eventdeclaration'); -const TypeNotFoundException = require('../lib/typenotfoundexception'); +const Serializer = require('../lib/serializer'); const TransactionDeclaration = require('../lib/introspect/transactiondeclaration'); -const ConceptDeclaration = require('../lib/introspect/conceptdeclaration'); -const ModelFileDownloader = require('../lib/introspect/loaders/modelfiledownloader'); - -const fs = require('fs'); +const TypeNotFoundException = require('../lib/typenotfoundexception'); const chai = require('chai'); const should = chai.should(); @@ -945,4 +946,20 @@ concept Bar { }); }); + describe('#getFactory', () => { + + it('should return a factory', () => { + modelManager.getFactory().should.be.an.instanceOf(Factory); + }); + + }); + + describe('#getSerializer', () => { + + it('should return a serializer', () => { + modelManager.getSerializer().should.be.an.instanceOf(Serializer); + }); + + }); + }); diff --git a/packages/composer-common/test/serializer.js b/packages/composer-common/test/serializer.js index b26e6f5cea..b5b91b2aad 100644 --- a/packages/composer-common/test/serializer.js +++ b/packages/composer-common/test/serializer.js @@ -153,6 +153,26 @@ describe('Serializer', () => { }).should.throw(/Generated invalid JSON/); }); + it('should not validate if the default options specifies the validate flag set to false', () => { + serializer.setDefaultOptions({ validate: false }); + let resource = factory.newResource('org.acme.sample', 'SampleAsset', '1'); + let json = serializer.toJSON(resource); + json.should.deep.equal({ + $class: 'org.acme.sample.SampleAsset', + assetId: '1' + }); + }); + + it('should validate if the default options specifies the validate flag set to false but the input options specify true', () => { + serializer.setDefaultOptions({ validate: false }); + let resource = factory.newResource('org.acme.sample', 'SampleAsset', '1'); + (() => { + serializer.toJSON(resource, { + validate: true + }); + }).should.throw(/missing required field/); + }); + }); describe('#fromJSON', () => { @@ -250,6 +270,32 @@ describe('Serializer', () => { should.equal(resource.value, undefined); }); + it('should not validate if the default options specifies the validate flag set to false', () => { + serializer.setDefaultOptions({ validate: false }); + let json = { + $class: 'org.acme.sample.SampleAsset', + assetId: '1', + owner: 'resource:org.acme.sample.SampleParticipant#alice@email.com' + }; + let resource = serializer.fromJSON(json); + resource.should.be.an.instanceOf(Resource); + resource.assetId.should.equal('1'); + resource.owner.should.be.an.instanceOf(Relationship); + should.equal(resource.value, undefined); + }); + + it('should validate if the default options specifies the validate flag set to false but the input options specify true', () => { + serializer.setDefaultOptions({ validate: false }); + let json = { + $class: 'org.acme.sample.SampleAsset', + assetId: '1', + owner: 'resource:org.acme.sample.SampleParticipant#alice@email.com' + }; + (() => { + serializer.fromJSON(json, { validate: true }); + }).should.throw(/missing required field/); + }); + }); }); diff --git a/packages/composer-runtime/lib/api.js b/packages/composer-runtime/lib/api.js index 344ecc7c5d..a6d02ee5ee 100644 --- a/packages/composer-runtime/lib/api.js +++ b/packages/composer-runtime/lib/api.js @@ -272,7 +272,8 @@ class Api { event.setIdentifier(context.getTransaction().getIdentifier() + '#' + context.getEventNumber()); event.timestamp = context.getTransaction().timestamp; let serializedEvent = serializer.toJSON(event, { - convertResourcesToRelationships: true + convertResourcesToRelationships: true, + permitResourcesForRelationships: false }); context.incrementEventNumber(); LOG.debug(method, event.getFullyQualifiedIdentifier(), serializedEvent); diff --git a/packages/composer-runtime/lib/api/serializer.js b/packages/composer-runtime/lib/api/serializer.js index 88d3110ce0..21a4f87bea 100644 --- a/packages/composer-runtime/lib/api/serializer.js +++ b/packages/composer-runtime/lib/api/serializer.js @@ -58,11 +58,15 @@ class Serializer { * are specified for relationship fields into relationships, false by default. * @param {boolean} [options.permitResourcesForRelationships] Permit resources in the * place of relationships (serializing them as resources), false by default. + * @param {boolean} [options.deduplicateResources] - Generate $id for resources and + * if a resources appears multiple times in the object graph only the first instance is + * serialized in full, subsequent instances are replaced with a reference to the $id * @return {Object} The JavaScript object that represents the resource * @throws {Error} If the specified resource is not an instance of * {@link Resource} or if it fails validation during serialization. */ - this.toJSON = function toJSON(resource, options) { + this.toJSON = function toJSON(resource, options = {}) { + options = Object.assign({}, { validate: true, convertResourcesToRelationships: false, permitResourcesForRelationships: false, deduplicateResources: false }, options); return serializer.toJSON(resource, options); }; @@ -81,11 +85,14 @@ class Serializer { * @param {Object} [options] The optional serialization options. * @param {boolean} [options.acceptResourcesForRelationships] Handle JSON objects * in the place of strings for relationships, false by default. + * @param {boolean} [options.validate] Validate the structure of the resource + * with its model prior to serialization, true by default. * @return {Resource} The resource. * @throws {Error} If the specified resource is not an instance of * {@link Resource} or if it fails validation during serialization. */ - this.fromJSON = function fromJSON(json, options) { + this.fromJSON = function fromJSON(json, options = {}) { + options = Object.assign({}, { acceptResourcesForRelationships: false, validate: true }, options); return serializer.fromJSON(json, options); }; diff --git a/packages/composer-runtime/lib/context.js b/packages/composer-runtime/lib/context.js index cf1d97dca2..d1e937d573 100644 --- a/packages/composer-runtime/lib/context.js +++ b/packages/composer-runtime/lib/context.js @@ -145,6 +145,8 @@ class Context { this.loggingService = this.container.getLoggingService(); } + this.getSerializer().setDefaultOptions({ permitResourcesForRelationships: true }); + LOG.exit(method); } diff --git a/packages/composer-runtime/lib/registry.js b/packages/composer-runtime/lib/registry.js index 7551f00173..0b04150ded 100644 --- a/packages/composer-runtime/lib/registry.js +++ b/packages/composer-runtime/lib/registry.js @@ -17,6 +17,12 @@ const EventEmitter = require('events'); const Resource = require('composer-common').Resource; +const baseDefaultOptions = { + convertResourcesToRelationships: true, + permitResourcesForRelationships: false, + forceAdd: false +}; + /** * A class for managing and persisting resources. * @protected @@ -64,28 +70,20 @@ class Registry extends EventEmitter { * @return {Promise} A promise that will be resolved with an array of {@link * Resource} objects when complete, or rejected with an error. */ - getAll() { - return this.dataCollection.getAll() - .then((objects) => { - return objects.reduce((promiseChain, resource) => { - return promiseChain.then((newResources) => { - let object = Registry.removeInternalProperties(resource); - try { - let resourceToCheckAccess = this.serializer.fromJSON(object); - return this.accessController.check(resourceToCheckAccess, 'READ') - .then(() => { - newResources.push(resourceToCheckAccess); - return newResources; - }).catch((e) => { - return newResources; - }); - } catch (err) { - return newResources; - } - - }); - }, Promise.resolve([])); - }); + async getAll() { + const objects = await this.dataCollection.getAll(); + const resources = []; + for (let object of objects) { + object = Registry.removeInternalProperties(object); + try { + const resource = this.serializer.fromJSON(object); + await this.accessController.check(resource, 'READ'); + resources.push(resource); + } catch (error) { + // Ignore the error; we don't have access. + } + } + return resources; } /** @@ -94,19 +92,16 @@ class Registry extends EventEmitter { * @return {Promise} A promise that will be resolved with a {@link Resource} * object when complete, or rejected with an error. */ - get(id) { - return this.dataCollection.get(id) - .then((object) => { - object = Registry.removeInternalProperties(object); - let result = this.serializer.fromJSON(object); - return this.accessController.check(result, 'READ') - .then(() => { - return result; - }) - .catch((error) => { - throw new Error(`Object with ID '${id}' in collection with ID '${this.type}:${this.id}' does not exist`); - }); - }); + async get(id) { + let object = await this.dataCollection.get(id); + object = Registry.removeInternalProperties(object); + try { + const resource = this.serializer.fromJSON(object); + await this.accessController.check(resource, 'READ'); + return resource; + } catch (error) { + throw new Error(`Object with ID '${id}' in collection with ID '${this.type}:${this.id}' does not exist`); + } } /** @@ -115,25 +110,20 @@ class Registry extends EventEmitter { * @return {Promise} A promise that will be resolved with a boolean * indicating whether the asset exists. */ - exists(id) { - return this.dataCollection.exists(id) - .then((exists) => { - if (!exists) { - return false; - } - return this.dataCollection.get(id) - .then((object) => { - object = Registry.removeInternalProperties(object); - let result = this.serializer.fromJSON(object); - return this.accessController.check(result, 'READ'); - }) - .then(() => { - return true; - }) - .catch((error) => { - return false; - }); - }); + async exists(id) { + const exists = await this.dataCollection.exists(id); + if (!exists) { + return false; + } + let object = await this.dataCollection.get(id); + object = Registry.removeInternalProperties(object); + try { + const resource = this.serializer.fromJSON(object); + await this.accessController.check(resource, 'READ'); + return true; + } catch (error) { + return false; + } } /** @@ -152,16 +142,11 @@ class Registry extends EventEmitter { * @param {boolean} [options.convertResourcesToRelationships] Permit resources * in the place of relationships, defaults to false. * @param {boolean} [options.forceAdd] Forces adding the object even if it present (default to false) - * @return {Promise} A promise that will be resolved when complete, or rejected - * with an error. */ - addAll(resources, options) { - options = options || { forceAdd: false }; - return resources.reduce((result, resource) => { - return result.then(() => { - return this.add(resource, options); - }); - }, Promise.resolve()); + async addAll(resources, options = {}) { + for (const resource of resources) { + await this.add(resource, options); + } } /** @@ -171,31 +156,21 @@ class Registry extends EventEmitter { * @param {boolean} [options.convertResourcesToRelationships] Permit resources * in the place of relationships, defaults to false. * @param {boolean} [options.forceAdd] Forces adding the object even if it present (default to false) - * @return {Promise} A promise that will be resolved when complete, or rejected - * with an error. */ - add(resource, options) { - - return this.testAdd(resource) - .then((result) => { - if (result){ - // uh-oh something is not permitted - throw result; - } - options = options || { forceAdd: false }; - let id = resource.getIdentifier(); - let object = this.serializer.toJSON(resource, { - convertResourcesToRelationships: options.convertResourcesToRelationships - }); - object = this.addInternalProperties(object); - return this.dataCollection.add(id, object, options.forceAdd); - }) - .then(() => { - this.emit('resourceadded', { - registry: this, - resource: resource - }); - }); + async add(resource, options = {}) { + const error = await this.testAdd(resource); + if (error) { + throw error; + } + const id = resource.getIdentifier(); + options = Object.assign({}, baseDefaultOptions, options); + let object = this.serializer.toJSON(resource, options); + object = this.addInternalProperties(object); + await this.dataCollection.add(id, object, options.forceAdd); + this.emit('resourceadded', { + registry: this, + resource: resource + }); } /** @@ -206,30 +181,20 @@ class Registry extends EventEmitter { * @return {Promise} A promise that will be resolved with null if this resource could be added, or resolved with the * error that would have been thrown. */ - testAdd(resource) { - - return Promise.resolve().then(() => { - if (!(resource instanceof Resource)) { - throw new Error('Expected a Resource or Concept.'); } - else if (this.type !== resource.getClassDeclaration().getSystemType()){ - throw new Error('Cannot add type: ' + resource.getClassDeclaration().getSystemType() + ' to ' + this.type); - } - }) - .then(() => { - return this.accessController.check(resource, 'CREATE'); - }) - .then(()=>{ + async testAdd(resource) { + if (!(resource instanceof Resource)) { + return new Error('Expected a Resource or Concept.'); } + else if (this.type !== resource.getClassDeclaration().getSystemType()){ + return new Error('Cannot add type: ' + resource.getClassDeclaration().getSystemType() + ' to ' + this.type); + } + try { + await this.accessController.check(resource, 'CREATE'); return null; - }) - .catch((error)=>{ - // access has not been granted, so return the error that would have been thrown in the - // promise + } catch (error) { return error; - }); - + } } - /** * An event signalling that a resource has been updated in this registry. * @event Registry#resourceupdated @@ -246,16 +211,11 @@ class Registry extends EventEmitter { * @param {Object} [options] Options for processing the resources. * @param {boolean} [options.convertResourcesToRelationships] Permit resources * in the place of relationships, defaults to false. - * @return {Promise} A promise that will be resolved when complete, or rejected - * with an error. */ - updateAll(resources, options) { - options = options || {}; - return resources.reduce((result, resource) => { - return result.then(() => { - return this.update(resource, options); - }); - }, Promise.resolve()); + async updateAll(resources, options = {}) { + for (const resource of resources) { + await this.update(resource, options); + } } /** @@ -264,45 +224,27 @@ class Registry extends EventEmitter { * @param {Object} [options] Options for processing the resources. * @param {boolean} [options.convertResourcesToRelationships] Permit resources * in the place of relationships, defaults to false. - * @return {Promise} A promise that will be resolved when complete, or rejected - * with an error. */ - update(resource, options) { - let id; - let object; - - return Promise.resolve().then(() => { - if (!(resource instanceof Resource)) { - throw new Error('Expected a Resource or Concept.'); } - else if (this.type !== resource.getClassDeclaration().getSystemType()){ - throw new Error('Cannot update type: ' + resource.getClassDeclaration().getSystemType() + ' to ' + this.type); - } - options = options || {}; - id = resource.getIdentifier(); - object = this.serializer.toJSON(resource, { - convertResourcesToRelationships: options.convertResourcesToRelationships - }); - object = this.addInternalProperties(object); - - return this.dataCollection.get(id); - }) - .then((oldResource) => { - return this.serializer.fromJSON(oldResource); - }) - .then((oldResource) => { - // We must perform access control checks on the old version of the resource! - return this.accessController.check(oldResource, 'UPDATE') - .then(() => { - return this.dataCollection.update(id, object); - }) - .then(() => { - this.emit('resourceupdated', { - registry: this, - oldResource: oldResource, - newResource: resource - }); - }); - }); + async update(resource, options = {}) { + if (!(resource instanceof Resource)) { + throw new Error('Expected a Resource or Concept.'); } + else if (this.type !== resource.getClassDeclaration().getSystemType()){ + throw new Error('Cannot update type: ' + resource.getClassDeclaration().getSystemType() + ' to ' + this.type); + } + const id = resource.getIdentifier(); + options = Object.assign({}, baseDefaultOptions, options); + let object = this.serializer.toJSON(resource, options); + object = this.addInternalProperties(object); + const oldObject = await this.dataCollection.get(id); + const oldResource = this.serializer.fromJSON(oldObject); + // We must perform access control checks on the old version of the resource! + await this.accessController.check(oldResource, 'UPDATE'); + await this.dataCollection.update(id, object); + this.emit('resourceupdated', { + registry: this, + oldResource: oldResource, + newResource: resource + }); } /** @@ -317,53 +259,34 @@ class Registry extends EventEmitter { /** * Remove all of the specified resources from this registry. * @param {string[]|Resource[]} resources The resources to remove from this registry. - * @return {Promise} A promise that will be resolved when complete, or rejected - * with an error. */ - removeAll(resources) { - return resources.reduce((result, resource) => { - return result.then(() => { - return this.remove(resource); - }); - }, Promise.resolve()); + async removeAll(resources) { + for (const resource of resources) { + await this.remove(resource); + } } /** * Remove the specified resource from this registry. * @param {string|Resource} resource The resource to remove from this registry. - * @return {Promise} A promise that will be resolved when complete, or rejected - * with an error. */ - remove(resource) { - return Promise.resolve() - .then(() => { - // If the resource is a string, then we need to retrieve - // the resource using its ID from the registry. We need to - // do this to figure out the type of the resource for - // access control. - if (resource instanceof Resource) { - return resource; - } else { - return this.dataCollection.get(resource) - .then((object) => { - object = Registry.removeInternalProperties(object); - return this.serializer.fromJSON(object); - }); - } - }) - .then((resource) => { - let id = resource.getIdentifier(); - return this.accessController.check(resource, 'DELETE') - .then(() => { - return this.dataCollection.remove(id); - }) - .then(() => { - this.emit('resourceremoved', { - registry: this, - resourceID: id - }); - }); - }); + async remove(resource) { + if (!(resource instanceof Resource)) { + // If the resource is a string, then we need to retrieve + // the resource using its ID from the registry. We need to + // do this to figure out the type of the resource for + // access control. + let object = await this.dataCollection.get(resource); + object = Registry.removeInternalProperties(object); + resource = this.serializer.fromJSON(object); + } + const id = resource.getIdentifier(); + await this.accessController.check(resource, 'DELETE'); + await this.dataCollection.remove(id); + this.emit('resourceremoved', { + registry: this, + resourceID: id + }); } /** diff --git a/packages/composer-runtime/test/api.js b/packages/composer-runtime/test/api.js index c92627d2e4..7e9ce36e4b 100644 --- a/packages/composer-runtime/test/api.js +++ b/packages/composer-runtime/test/api.js @@ -204,7 +204,7 @@ describe('Api', () => { return api.post('url', transaction, {options: true}) .should.eventually.have.property('foo') .then(() => { - sinon.assert.calledWith(spy, transaction, { options: true, validate: true }); + sinon.assert.calledWith(spy, transaction, { options: true }); sinon.assert.calledOnce(mockHTTPService.post); sinon.assert.calledWith(mockHTTPService.post, 'url', { $class: 'org.doge.DogeTransaction', @@ -233,7 +233,7 @@ describe('Api', () => { it('should emit the event using the event service', () => { api.emit(event); sinon.assert.calledOnce(spy); - sinon.assert.calledWith(spy, event, { convertResourcesToRelationships: true, validate: true }); + sinon.assert.calledWith(spy, event, { convertResourcesToRelationships: true, permitResourcesForRelationships: false }); sinon.assert.calledOnce(mockEventService.emit); sinon.assert.calledWith(mockEventService.emit, { $class: 'org.doge.DogeEvent', diff --git a/packages/composer-runtime/test/api/serializer.js b/packages/composer-runtime/test/api/serializer.js index c033c1d209..d994f9bd27 100644 --- a/packages/composer-runtime/test/api/serializer.js +++ b/packages/composer-runtime/test/api/serializer.js @@ -51,20 +51,40 @@ describe('Serializer', () => { describe('#toJSON', () => { - it('should proxy to the serializer', () => { - mockSerializer.toJSON.withArgs(mockResource, { option1: true }).returns({ thing1: 'value 1' }); + it('should proxy to the serializer without options', () => { + mockSerializer.toJSON.withArgs(mockResource, { validate: true, convertResourcesToRelationships: false, permitResourcesForRelationships: false, deduplicateResources: false }).returns({ thing1: 'value 1' }); + serializer.toJSON(mockResource).should.deep.equal({ thing1: 'value 1' }); + }); + + it('should proxy to the serializer with options', () => { + mockSerializer.toJSON.withArgs(mockResource, { validate: true, convertResourcesToRelationships: false, permitResourcesForRelationships: false, deduplicateResources: false, option1: true }).returns({ thing1: 'value 1' }); serializer.toJSON(mockResource, { option1: true }).should.deep.equal({ thing1: 'value 1' }); }); + it('should proxy to the serializer with options containing default overrides', () => { + mockSerializer.toJSON.withArgs(mockResource, { validate: true, convertResourcesToRelationships: true, permitResourcesForRelationships: false, deduplicateResources: false }).returns({ thing1: 'value 1' }); + serializer.toJSON(mockResource, { convertResourcesToRelationships: true }).should.deep.equal({ thing1: 'value 1' }); + }); + }); describe('#fromJSON', () => { - it('should proxy to the serializer', () => { - mockSerializer.fromJSON.withArgs({ thing1: 'value 1' }, { option1: true }).returns(mockResource); + it('should proxy to the serializer without options', () => { + mockSerializer.fromJSON.withArgs({ thing1: 'value 1' }, { acceptResourcesForRelationships: false, validate: true }).returns(mockResource); + serializer.fromJSON({ thing1: 'value 1' }).should.equal(mockResource); + }); + + it('should proxy to the serializer with options', () => { + mockSerializer.fromJSON.withArgs({ thing1: 'value 1' }, { acceptResourcesForRelationships: false, validate: true, option1: true }).returns(mockResource); serializer.fromJSON({ thing1: 'value 1' }, { option1: true }).should.equal(mockResource); }); + it('should proxy to the serializer with options containing default overrides', () => { + mockSerializer.fromJSON.withArgs({ thing1: 'value 1' }, { acceptResourcesForRelationships: true, validate: true }).returns(mockResource); + serializer.fromJSON({ thing1: 'value 1' }, { acceptResourcesForRelationships: true }).should.equal(mockResource); + }); + }); }); diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js index d56caddd8a..0215aee66b 100644 --- a/packages/composer-runtime/test/context.js +++ b/packages/composer-runtime/test/context.js @@ -303,6 +303,14 @@ describe('Context', () => { }); }); + it('should initialize the context with the runtime serializer defaults', () => { + let mockContainer = sinon.createStubInstance(Container); + return context.initialize({ container: mockContainer }) + .then(() => { + context.getSerializer().defaultOptions.should.deep.equal({ permitResourcesForRelationships: true, validate: true }); + }); + }); + }); describe('#getServices', () => { diff --git a/packages/composer-runtime/test/registry.js b/packages/composer-runtime/test/registry.js index 1107a2db98..f244064fa2 100644 --- a/packages/composer-runtime/test/registry.js +++ b/packages/composer-runtime/test/registry.js @@ -212,8 +212,8 @@ describe('Registry', () => { it('should determine whether a specific resource exists in the registry', () => { return registry.exists('doge1') + .should.eventually.be.true .then((exists) => { - exists.should.equal.true; sinon.assert.calledOnce(mockAccessController.check); sinon.assert.calledWith(mockAccessController.check, mockResource, 'READ'); }); @@ -221,15 +221,13 @@ describe('Registry', () => { it('should determine whether a specific resource does not exist in the registry', () => { return registry.exists('doge2') - .then((exists) => { - exists.should.equal.false; - }); + .should.eventually.be.false; }); it('should not throw or leak information about resources that cannot be accessed', () => { mockAccessController.check.withArgs(mockResource, 'READ').rejects(new AccessException(mockResource, 'READ', mockParticipant)); return registry.exists('doge1') - .should.eventually.equal(false) + .should.eventually.be.false .then(() => { sinon.assert.calledOnce(mockAccessController.check); sinon.assert.calledWith(mockAccessController.check, mockResource, 'READ'); diff --git a/packages/composer-tests-functional/scripts/http.js b/packages/composer-tests-functional/scripts/http.js index ce02835672..ec40081b45 100644 --- a/packages/composer-tests-functional/scripts/http.js +++ b/packages/composer-tests-functional/scripts/http.js @@ -32,23 +32,20 @@ app.use(function(req, res, next) { }); ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach((method) => { + app[method.toLowerCase()]('/api/basic', (req, res) => { res.status(200).json({ method: req.method }); }); -}); -['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach((method) => { app[method.toLowerCase()]('/api/error', (req, res) => { res.status(500).json({ method: req.method, error: 'such error' }); }); -}); -['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach((method) => { app[method.toLowerCase()]('/api/assetin', (req, res) => { assert.deepStrictEqual(req.body, { $class: 'systest.transactions.http.DummyAsset', @@ -60,9 +57,37 @@ app.use(function(req, res, next) { method: req.method }); }); -}); -['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach((method) => { + app[method.toLowerCase()]('/api/assetwithrelationshipin', (req, res) => { + assert.deepStrictEqual(req.body, { + $class: 'systest.transactions.http.DummyAsset', + assetId: '1234', + integerValue: 12345678, + stringValue: 'hello world', + participant: 'resource:systest.transactions.http.DummyParticipant#1234' + }); + res.status(200).json({ + method: req.method + }); + }); + + app[method.toLowerCase()]('/api/assetwithresolvedrelationshipin', (req, res) => { + assert.deepStrictEqual(req.body, { + $class: 'systest.transactions.http.DummyAsset', + assetId: '1234', + integerValue: 12345678, + stringValue: 'hello world', + participant: { + $class: 'systest.transactions.http.DummyParticipant', + participantId: '1234', + stringValue: 'hello world' + } + }); + res.status(200).json({ + method: req.method + }); + }); + app[method.toLowerCase()]('/api/assetout', (req, res) => { res.status(200).json({ method: req.method, @@ -74,6 +99,7 @@ app.use(function(req, res, next) { } }); }); + }); process.on('SIGINT', function () { diff --git a/packages/composer-tests-functional/systest/data/transactions.http.cto b/packages/composer-tests-functional/systest/data/transactions.http.cto index 0a4052de81..956d65e850 100644 --- a/packages/composer-tests-functional/systest/data/transactions.http.cto +++ b/packages/composer-tests-functional/systest/data/transactions.http.cto @@ -22,10 +22,23 @@ transaction Error { o String method } -transaction AssetIn { +transaction AssetInWithSerializer { o String method } +transaction AssetInWithoutSerializer { + o String method +} + +transaction AssetWithRelationshipInWithoutSerializer { + o String method +} + +transaction AssetWithResolvedRelationshipInWithoutSerializer { + o String method + o DummyAsset asset +} + transaction AssetOut { o String method } @@ -34,4 +47,10 @@ asset DummyAsset identified by assetId { o String assetId o String stringValue o Integer integerValue + --> DummyParticipant participant optional +} + +participant DummyParticipant identified by participantId { + o String participantId + o String stringValue } \ No newline at end of file diff --git a/packages/composer-tests-functional/systest/data/transactions.http.js b/packages/composer-tests-functional/systest/data/transactions.http.js index 6e1b1bae98..517f236b5e 100644 --- a/packages/composer-tests-functional/systest/data/transactions.http.js +++ b/packages/composer-tests-functional/systest/data/transactions.http.js @@ -56,11 +56,11 @@ async function handleError(tx) { } /** - * Handle an asset in transaction. - * @param {systest.transactions.http.AssetIn} tx The transaction. + * Handle an asset in transaction by using the serializer. + * @param {systest.transactions.http.AssetInWithSerializer} tx The transaction. * @transaction */ -async function handleAssetIn(tx) { +async function handleAssetInWithSerializer(tx) { const factory = getFactory(); const asset = factory.newResource('systest.transactions.http', 'DummyAsset', '1234'); asset.stringValue = 'hello world'; @@ -71,6 +71,49 @@ async function handleAssetIn(tx) { assert.equal(data.method, tx.method); } +/** + * Handle an asset in transaction without using the serializer. + * @param {systest.transactions.http.AssetInWithoutSerializer} tx The transaction. + * @transaction + */ +async function handleAssetInWithoutSerializer(tx) { + const factory = getFactory(); + const asset = factory.newResource('systest.transactions.http', 'DummyAsset', '1234'); + asset.stringValue = 'hello world'; + asset.integerValue = 12345678; + const data = await request({ uri: `${url}/assetin`, method: tx.method, json: asset }); + assert.equal(data.method, tx.method); +} + +/** + * Handle an asset with a relationship in transaction without using the serializer. + * @param {systest.transactions.http.AssetWithRelationshipInWithoutSerializer} tx The transaction. + * @transaction + */ +async function handleAssetWithRelationshipInWithoutSerializer(tx) { + const factory = getFactory(); + const participant = factory.newResource('systest.transactions.http', 'DummyParticipant', '1234'); + participant.stringValue = 'hello world'; + const participantRegistry = await getParticipantRegistry('systest.transactions.http.DummyParticipant'); + await participantRegistry.add(participant); + const asset = factory.newResource('systest.transactions.http', 'DummyAsset', '1234'); + asset.stringValue = 'hello world'; + asset.integerValue = 12345678; + asset.participant = factory.newRelationship('systest.transactions.http', 'DummyParticipant', '1234'); + const data = await request({ uri: `${url}/assetwithrelationshipin`, method: tx.method, json: asset }); + assert.equal(data.method, tx.method); +} + +/** + * Handle an asset with a resolved relationship in transaction without using the serializer. + * @param {systest.transactions.http.AssetWithResolvedRelationshipInWithoutSerializer} tx The transaction. + * @transaction + */ +async function handleAssetWithResolvedRelationshipInWithoutSerializer(tx) { + const data = await request({ uri: `${url}/assetwithresolvedrelationshipin`, method: tx.method, json: tx.asset }); + assert.equal(data.method, tx.method); +} + /** * Handle an asset out transaction. * @param {systest.transactions.http.AssetOut} tx The transaction. diff --git a/packages/composer-tests-functional/systest/transactions.http.js b/packages/composer-tests-functional/systest/transactions.http.js index d84e0ee435..3c5de8cdb6 100644 --- a/packages/composer-tests-functional/systest/transactions.http.js +++ b/packages/composer-tests-functional/systest/transactions.http.js @@ -91,15 +91,60 @@ describe('Transaction (HTTP specific) system tests', function() { // Can't send a request body for a GET or HEAD request. ['POST', 'PUT', 'DELETE', 'PATCH'].forEach((method) => { - it(`should handle a ${method} call that sends an asset in the request body`, async () => { + it(`should handle a ${method} call that sends an asset in the request body by using the serializer`, async () => { const factory = client.getBusinessNetwork().getFactory(); - const tx = factory.newTransaction('systest.transactions.http', 'AssetIn'); + const tx = factory.newTransaction('systest.transactions.http', 'AssetInWithSerializer'); tx.method = method; await client.submitTransaction(tx); }); }); + // Can't send a request body for a GET or HEAD request. + ['POST', 'PUT', 'DELETE', 'PATCH'].forEach((method) => { + + it(`should handle a ${method} call that sends an asset in the request body without using the serializer`, async () => { + const factory = client.getBusinessNetwork().getFactory(); + const tx = factory.newTransaction('systest.transactions.http', 'AssetInWithoutSerializer'); + tx.method = method; + await client.submitTransaction(tx); + }); + + }); + + // Can't send a request body for a GET or HEAD request. + ['POST', 'PUT', 'DELETE', 'PATCH'].forEach((method) => { + + it(`should handle a ${method} call that sends an asset with a relationship in the request body without using the serializer`, async () => { + const factory = client.getBusinessNetwork().getFactory(); + const tx = factory.newTransaction('systest.transactions.http', 'AssetWithRelationshipInWithoutSerializer'); + tx.method = method; + await client.submitTransaction(tx); + }); + + }); + + // Can't send a request body for a GET or HEAD request. + ['POST', 'PUT', 'DELETE', 'PATCH'].forEach((method) => { + + it(`should handle a ${method} call that sends an asset with a resolved relationship in the request body without using the serializer`, async () => { + const factory = client.getBusinessNetwork().getFactory(); + const participant = factory.newResource('systest.transactions.http', 'DummyParticipant', '1234'); + participant.stringValue = 'hello world'; + const participantRegistry = await client.getParticipantRegistry('systest.transactions.http.DummyParticipant'); + await participantRegistry.add(participant); + const asset = factory.newResource('systest.transactions.http', 'DummyAsset', '1234'); + asset.stringValue = 'hello world'; + asset.integerValue = 12345678; + asset.participant = factory.newRelationship('systest.transactions.http', 'DummyParticipant', '1234'); + const tx = factory.newTransaction('systest.transactions.http', 'AssetWithResolvedRelationshipInWithoutSerializer'); + tx.method = method; + tx.asset = asset; + await client.submitTransaction(tx); + }); + + }); + // Can't receive a response body for a HEAD request. ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach((method) => { diff --git a/packages/composer-website/jekylldocs/integrating/call-out.md b/packages/composer-website/jekylldocs/integrating/call-out.md index 8472e65f0c..d0924d8db1 100644 --- a/packages/composer-website/jekylldocs/integrating/call-out.md +++ b/packages/composer-website/jekylldocs/integrating/call-out.md @@ -98,20 +98,16 @@ Make an HTTP POST request to an HTTP server that includes the current participan */ async function buyStocks(transaction) { - // Get the current participant, and serialize them into a JavaScript object. + // Get the current participant. const participant = getCurrentParticipant(); - const serializer = getSerializer(); - const json = serializer.toJSON(participant); - const units = transaction.units; // Look up the current price of the CONGA stock, and extract the price. - // The option "json" sends the serialized participant as the HTTP request body, + // The option "json" sends the participant as the HTTP request body, // and automatically parses JSON from the HTTP response. - const stock = await request.post({ uri: 'http://stocks.org/CONGA', json }); + const stock = await request.post({ uri: 'http://stocks.org/CONGA', json: participant }); const price = stock.price; // Get the current participant, and update their stock and balance. - const participant = getCurrentParticipant(); const units = transaction.units; participant.stockUnits += units; participant.balance -= price * units;