diff --git a/packages/composer-admin/jsdoc.conf b/packages/composer-admin/jsdoc.json similarity index 100% rename from packages/composer-admin/jsdoc.conf rename to packages/composer-admin/jsdoc.json diff --git a/packages/composer-admin/package.json b/packages/composer-admin/package.json index 4574433971..443bbff992 100644 --- a/packages/composer-admin/package.json +++ b/packages/composer-admin/package.json @@ -16,7 +16,7 @@ "postlint": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf" + "doc": "jsdoc --pedantic --recurse -c jsdoc.json" }, "repository": { "type": "git", diff --git a/packages/composer-cli/bond-network@0.1.1.bna b/packages/composer-cli/bond-network@0.1.1.bna new file mode 100644 index 0000000000..fe26bcae91 Binary files /dev/null and b/packages/composer-cli/bond-network@0.1.1.bna differ diff --git a/packages/composer-cli/trade-network@0.1.1.bna b/packages/composer-cli/trade-network@0.1.1.bna new file mode 100644 index 0000000000..29de341995 Binary files /dev/null and b/packages/composer-cli/trade-network@0.1.1.bna differ diff --git a/packages/composer-cli/trade-network@0.1.2.bna b/packages/composer-cli/trade-network@0.1.2.bna new file mode 100644 index 0000000000..7e67dd4768 Binary files /dev/null and b/packages/composer-cli/trade-network@0.1.2.bna differ diff --git a/packages/composer-client/jsdoc.conf b/packages/composer-client/jsdoc.json similarity index 100% rename from packages/composer-client/jsdoc.conf rename to packages/composer-client/jsdoc.json diff --git a/packages/composer-client/package.json b/packages/composer-client/package.json index 58be6c0276..86f0b5cfe6 100644 --- a/packages/composer-client/package.json +++ b/packages/composer-client/package.json @@ -13,7 +13,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test": "node ./scripts/api-changelog.js && nyc mocha --recursive -t 10000" diff --git a/packages/composer-common/index.js b/packages/composer-common/index.js index a565ef1492..82cf3aac9d 100644 --- a/packages/composer-common/index.js +++ b/packages/composer-common/index.js @@ -85,6 +85,7 @@ module.exports.OrderBy = require('./lib/query/orderby'); module.exports.ParticipantDeclaration = require('./lib/introspect/participantdeclaration'); module.exports.Property = require('./lib/introspect/property'); module.exports.Query = require('./lib/query/query'); +module.exports.QueryAnalyzer = require('./lib/query/queryanalyzer.js'); module.exports.QueryFile = require('./lib/query/queryfile'); module.exports.QueryManager = require('./lib/querymanager'); module.exports.Relationship = require('./lib/model/relationship'); diff --git a/packages/composer-common/jsdoc.conf b/packages/composer-common/jsdoc.json similarity index 100% rename from packages/composer-common/jsdoc.conf rename to packages/composer-common/jsdoc.json diff --git a/packages/composer-common/lib/codegen/fromcto/loopback/loopbackvisitor.js b/packages/composer-common/lib/codegen/fromcto/loopback/loopbackvisitor.js index d43866b056..5c348ed1a4 100644 --- a/packages/composer-common/lib/codegen/fromcto/loopback/loopbackvisitor.js +++ b/packages/composer-common/lib/codegen/fromcto/loopback/loopbackvisitor.js @@ -388,6 +388,36 @@ class LoopbackVisitor { } + /** + * Given a primitive Composer type returns the corresponding loopback type + * @param {string} type - the composer primitive type name + * @return {string} the loopback type + * @private + */ + static toLoopbackType(type) { + + let result = 'string'; + + switch (type) { + case 'String': + result = 'string'; + break; + case 'Double': + case 'Integer': + case 'Long': + result= 'number'; + break; + case 'DateTime': + result = 'date'; + break; + case 'Boolean': + result = 'boolean'; + break; + } + + return result; + } + /** * Visitor design pattern * @param {Field} field - the object being visited @@ -404,22 +434,7 @@ class LoopbackVisitor { // Render the type as JSON Schema. jsonSchema = {}; - switch (field.getType()) { - case 'String': - jsonSchema.type = 'string'; - break; - case 'Double': - case 'Integer': - case 'Long': - jsonSchema.type = 'number'; - break; - case 'DateTime': - jsonSchema.type = 'date'; - break; - case 'Boolean': - jsonSchema.type = 'boolean'; - break; - } + jsonSchema.type = LoopbackVisitor.toLoopbackType(field.getType()); // If this field has a default value, add it. if (field.getDefaultValue()) { diff --git a/packages/composer-common/lib/query/query.js b/packages/composer-common/lib/query/query.js index 717e38bd7e..3a3d91efd1 100644 --- a/packages/composer-common/lib/query/query.js +++ b/packages/composer-common/lib/query/query.js @@ -144,7 +144,6 @@ class Query { getSelect() { return this.select; } - } module.exports = Query; diff --git a/packages/composer-common/lib/query/queryanalyzer.js b/packages/composer-common/lib/query/queryanalyzer.js new file mode 100644 index 0000000000..c9e0276112 --- /dev/null +++ b/packages/composer-common/lib/query/queryanalyzer.js @@ -0,0 +1,434 @@ +/* + * 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. + */ + +'use strict'; + +const Limit = require('./limit'); +const Logger = require('../log/logger'); +const OrderBy = require('./orderby'); +const Query = require('./query'); +const Select = require('./select'); +const Skip = require('./skip'); +const Where = require('./where'); +const RelationshipDeclaration = require('../introspect/relationshipdeclaration'); +const LOG = Logger.getLog('QueryAnalyzer'); + +/** + * The query analyzer visits a query and extracts the names and types of all parameters + * @private + */ +class QueryAnalyzer { + + /** + * Create an Query from an Abstract Syntax Tree. The AST is the + * result of parsing. + * + * @param {Query} query - the composer query for process + * @throws {IllegalModelException} + */ + constructor(query) { + if (!query) { + throw new Error('Invalid query'); + } + + this.query = query; + } + + /** + * Extract the names and types of query parameters + * @return {object[]} The names and types of the query parameters + */ + analyze() { + const method = 'analyze'; + LOG.entry(method); + const result = this.query.accept(this, {}); + LOG.exit(method, result); + + return result; + } + + /** + * Visitor design pattern; handle all objects from the query manager. + * @param {Object} thing The object being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visit(thing, parameters) { + const method = 'visit'; + LOG.entry(method, thing, parameters); + let result = null; + if (thing instanceof Query) { + result = this.visitQuery(thing, parameters); + } else if (thing instanceof Select) { + result = this.visitSelect(thing, parameters); + } else if (thing instanceof Where) { + result = this.visitWhere(thing, parameters); + } else if (thing instanceof OrderBy) { + result = this.visitOrderBy(thing, parameters); + } else if (thing instanceof Limit) { + result = this.visitLimit(thing, parameters); + } else if (thing instanceof Skip) { + result = this.visitSkip(thing, parameters); + } else if (thing.type === 'BinaryExpression') { + result = this.visitBinaryExpression(thing, parameters); + } else if (thing.type === 'Identifier') { + result = this.visitIdentifier(thing, parameters); + } else if (thing.type === 'Literal') { + result = this.visitLiteral(thing, parameters); + } else if (thing.type === 'MemberExpression') { + result = this.visitMemberExpression(thing, parameters); + } else { + throw new Error('Unrecognised type: ' + typeof thing + ', value: ' + JSON.stringify(thing)); + } + LOG.exit(method, result); + return result; + } + + /** + * Visitor design pattern; handle a query by visiting the select statement. + * @param {Query} query The query being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitQuery(query, parameters) { + const method = 'visitQuery'; + LOG.entry(method, query, parameters); + + // Process the select statement, which will return a Mango query. + const select = query.getSelect(); + const requiredParameters = []; + parameters.requiredParameters = requiredParameters; + const parametersToUse = {}; + parameters.parametersToUse = parametersToUse; + const result = select.accept(this, parameters); + + LOG.exit(method, result); + return result; + } + + /** + * Visitor design pattern; handle a select statement. + * @param {Select} select The select statement being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitSelect(select, parameters) { + const method = 'visitSelect'; + LOG.entry(method, select, parameters); + + let results = []; + + // Handle the where clause, if it exists. + const where = select.getWhere(); + if (where) { + results = results.concat(where.accept(this, parameters)); + } + + // Handle the order by clause, if it exists. + const orderBy = select.getOrderBy(); + if (orderBy) { + results = results.concat(orderBy.accept(this, parameters)); + } + + // Handle the limit clause, if it exists. Note that the limit + // clause can reference a parameter. + const limit = select.getLimit(); + if (limit) { + results = results.concat(limit.accept(this, parameters)); + } + + // Handle the skip clause, if it exists. Note that the skip + // clause can reference a parameter. + const skip = select.getSkip(); + if (skip) { + results = results.concat(skip.accept(this, parameters)); + } + + LOG.exit(method, results); + return results; + } + + /** + * Visitor design pattern; handle a where statement. + * @param {Where} where The where statement being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitWhere(where, parameters) { + const method = 'visitWhere'; + LOG.entry(method, where, parameters); + + // Simply visit the AST, which will generate a selector. + // The root of the AST is probably a binary expression. + const result = this.visit(where.getAST(), parameters); + LOG.exit(method, result); + return result; + } + + /** + * Visitor design pattern; handle an order by statement. + * @param {OrderBy} orderBy The order by statement being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitOrderBy(orderBy, parameters) { + const method = 'visitOrderBy'; + LOG.entry(method, orderBy, parameters); + const result = []; + LOG.exit(method, result); + return result; + } + + /** + * Visitor design pattern; handle a limit statement. + * @param {Limit} limit The limit statement being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitLimit(limit, parameters) { + const method = 'visitLimit'; + LOG.entry(method, limit, parameters); + // Get the limit value from the AST. + const result = this.visit(limit.getAST(), parameters); + if (result.length > 0) { + result[0].type = 'Integer'; + } + LOG.exit(method, result); + return result; + } + + /** + * Visitor design pattern; handle a skip statement. + * @param {Skip} skip The skip statement being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitSkip(skip, parameters) { + const method = 'visitSkip'; + LOG.entry(method, skip, parameters); + // Get the skip value from the AST. + const result = this.visit(skip.getAST(), parameters); + + if (result.length > 0) { + result[0].type = 'Integer'; + } + LOG.exit(method, result); + return result; + } + + /** + * Visitor design pattern; handle a binary expression. + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitBinaryExpression(ast, parameters) { + const method = 'visitBinaryExpression'; + LOG.entry(method, ast, parameters); + + // Binary expressions are handled differently in Mango based on the type, + // so figure out the type and handle it appropriately. + const arrayCombinationOperators = ['AND', 'OR']; + let result; + if (arrayCombinationOperators.indexOf(ast.operator) !== -1) { + result = this.visitArrayCombinationOperator(ast, parameters); + } else { + result = this.visitConditionOperator(ast, parameters); + } + + LOG.exit(method, result); + return result; + } + + /** + * Visitor design pattern; handle an array combination operator. + * Array combination operators are operators that act on two or more pieces + * of data, such as 'AND' and 'OR'. + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitArrayCombinationOperator(ast, parameters) { + const method = 'visitArrayCombinationOperator'; + LOG.entry(method, ast, parameters); + + let result = []; + result = result.concat(this.visit(ast.left, parameters)); + result = result.concat(this.visit(ast.right, parameters)); + LOG.exit(method, result); + return result; + } + + /** + * Visitor design pattern; handle a condition operator. + * Condition operators are operators that compare two pieces of data, such + * as '>=' and '!='. + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitConditionOperator(ast, parameters) { + const method = 'visitConditionOperator'; + LOG.entry(method, ast, parameters); + + let result = []; + + // Grab the right hand side of this expression. + const rhs = this.visit(ast.right, parameters); + const lhs = this.visit(ast.left, parameters); + + // if the rhs is a string, it is the name of a property + if (typeof rhs === 'string' && (lhs instanceof Array && lhs.length > 0)) { + lhs[0].type = this.getPropertyType(rhs); + result = result.concat(lhs); + } + + // if the lhs is a string, it is the name of a property + if (typeof lhs === 'string' && (rhs instanceof Array && rhs.length > 0)) { + rhs[0].type = this.getPropertyType(lhs); + result = result.concat(rhs); + } + + LOG.exit(method, result); + return result; + } + + /** + * Visitor design pattern; handle an identifier. + * Identifiers are either references to properties in the data being queried, + * or references to a query parameter (these are of the format _$varname). + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitIdentifier(ast, parameters) { + const method = 'visitIdentifier'; + LOG.entry(method, ast, parameters); + + // Check to see if this is a parameter reference. + const parameterMatch = ast.name.match(/^_\$(.+)/); + if (parameterMatch) { + const parameterName = parameterMatch[1]; + LOG.exit(method, parameterName); + + // We return a parameter object with a null type + // performing the type inference in the parent visit + return [{ + name: parameterName, + type: null + }]; + } + + // Otherwise it's a property name. + const selector = ast.name; + LOG.exit(method, selector); + return selector; + + } + + /** + * Visitor design pattern; handle a literal. + * Literals are just plain old literal values ;-) + * @param {string} parameterName The parameter name or name with nested structure e.g A.B.C + * @return {string} The result of the parameter type or null + * @private + */ + getPropertyType(parameterName) { + const method = 'getParameterType'; + LOG.entry(method, parameterName); + + // The grammar ensures that the resource property is set. + const modelManager = this.query.getQueryFile().getModelManager(); + const resource = this.query.getSelect().getResource(); + + let result = null; + const parameterNames = parameterName.split('.'); + + // checks the resource type exists + let classDeclaration = modelManager.getType(resource); + + for (let n = 0; n < parameterNames.length; n++) { + const property = classDeclaration.getProperty(parameterNames[n]); + + if (property !== null) { + // enums are relationships are represented as strings + if (property.isTypeEnum() || property instanceof RelationshipDeclaration) { + result = 'String'; + break; + } else if (property.isPrimitive()) { + result = property.getType(); + break; + } else { + const resource = property.getFullyQualifiedTypeName(); + classDeclaration = modelManager.getType(resource); + property.validate(classDeclaration); + } + } else { + throw new Error('Property ' + parameterNames[n] + ' does not exist on ' + resource); + } + } + + if (result === null) { + throw new Error('Property ' + parameterName + ' is not a primitive, enum or relationship on ' + resource); + } + + LOG.exit(method, result); + return result; + } + /** + * Visitor design pattern; handle a literal. + * Literals are just plain old literal values ;-) + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitLiteral(ast, parameters) { + const method = 'visitLiteral'; + LOG.entry(method, ast, parameters); + const result = []; + LOG.exit(method, result); + return result; + } + + /** + * Visitor design pattern; handle a member expression. + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitMemberExpression(ast, parameters) { + const method = 'visitMemberExpression'; + LOG.entry(method, ast, parameters); + const property = this.visit(ast.property, parameters); + const object = this.visit(ast.object, parameters); + const selector = `${object}.${property}`; + LOG.exit(method, selector); + return selector; + } +} + +module.exports = QueryAnalyzer; diff --git a/packages/composer-common/lib/querymanager.js b/packages/composer-common/lib/querymanager.js index 49bf377ca5..0d39731c85 100644 --- a/packages/composer-common/lib/querymanager.js +++ b/packages/composer-common/lib/querymanager.js @@ -57,7 +57,6 @@ class QueryManager { * @return {QueryFile} The new Query file. */ createQueryFile(identifier, contents) { - return new QueryFile(identifier, this.modelManager, contents); } @@ -81,7 +80,7 @@ class QueryManager { /** * Get the Queries associated with this QueryManager - * @return {Query[]} The Quries for the QueryManager or an empty array if not set + * @return {Query[]} The Queries for the QueryManager or an empty array if not set */ getQueries() { if(this.queryFile) { @@ -90,6 +89,24 @@ class QueryManager { return []; } + /** + * Get the named Query associated with this QueryManager + * @param {string} name - the name of the query + * @return {Query} The Query or null if it does not exist + */ + getQuery(name) { + if(this.queryFile) { + const queries = this.queryFile.getQueries(); + for(let n=0; n < queries.length; n++) { + const query = queries[n]; + if(query.getName() === name) { + return query; + } + } + } + return null; + } + } module.exports = QueryManager; diff --git a/packages/composer-common/package.json b/packages/composer-common/package.json index 89f81588f5..01b444a234 100644 --- a/packages/composer-common/package.json +++ b/packages/composer-common/package.json @@ -15,7 +15,7 @@ "postlint": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run browserify", "browserify": "browserify ./index.js -t [ babelify --presets [ latest ] ] > ./out/composer-common.js", "test": "node ./scripts/api-changelog.js && nyc mocha --recursive -t 10000" diff --git a/packages/composer-common/test/query/queryanalyzer.js b/packages/composer-common/test/query/queryanalyzer.js new file mode 100644 index 0000000000..08b718ddd6 --- /dev/null +++ b/packages/composer-common/test/query/queryanalyzer.js @@ -0,0 +1,291 @@ +/* +* 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. +*/ + +'use strict'; + +const QueryAnalyzer = require('../../lib/query/queryanalyzer'); +const Query = require('../../lib/query/query'); +const Select = require('../../lib/query/select'); +const parser = require('../../lib/query/parser'); + +const QueryFile = require('../../lib/query/queryfile'); +const ModelManager = require('../../lib/modelmanager'); +require('chai').should(); +const sinon = require('sinon'); + +describe('QueryAnalyzer', () => { + + let queryAnalyzer; + let mockQuery; + let mockQueryFile; + let sandbox; + + beforeEach(() => { + const modelManager = new ModelManager(); + modelManager.addModelFile( + ` + namespace org.acme + + enum ContactType { + o MOBILE + o FAX + o LANDLINE + } + + concept PhoneDetails { + o String phoneNumber + o ContactType contactType + } + + concept Address { + o String city + o PhoneDetails phoneDetails + } + + participant Driver identified by driverId { + o String driverId + o String name + o Address address + o Integer age + } + + asset Vehicle identified by vin { + o String vin + --> Driver driver + } + `, 'test'); + + mockQuery = sinon.createStubInstance(Query); + mockQueryFile = sinon.createStubInstance(QueryFile); + mockQuery.getQueryFile.returns(mockQueryFile); + mockQueryFile.getModelManager.returns(modelManager); + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#constructor', () => { + + it('should throw when null Query provided', () => { + (() => { + new QueryAnalyzer(null); + }).should.throw(/Invalid query/); + }); + }); + + describe('#analyze', () => { + + it('should call the visitor', () => { + queryAnalyzer = new QueryAnalyzer( mockQuery ); + queryAnalyzer.analyze(); + sinon.assert.calledOnce(mockQuery.accept); + }); + + }); + + describe('#visitQuery', () => { + + it('should process select with a single string param', () => { + const ast = parser.parse('SELECT org.acme.Driver WHERE (name == _$param1) LIMIT 10 SKIP 5', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(1); + result[0].name.should.equal('param1'); + result[0].type.should.equal('String'); + }); + + it('should process select with a single integer LIMIT param', () => { + const ast = parser.parse('SELECT org.acme.Driver WHERE (name == \'Dan\') LIMIT _$param1 SKIP 5', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(1); + result[0].name.should.equal('param1'); + result[0].type.should.equal('Integer'); + }); + + it('should process select with a single integer SKIP param', () => { + const ast = parser.parse('SELECT org.acme.Driver WHERE (name == \'Dan\') LIMIT 5 SKIP _$param1', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(1); + result[0].name.should.equal('param1'); + result[0].type.should.equal('Integer'); + }); + + it('should process select with an order by', () => { + const ast = parser.parse('SELECT org.acme.Driver WHERE (name == _$param1) ORDER BY name DESC', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(1); + result[0].name.should.equal('param1'); + result[0].type.should.equal('String'); + }); + + it('should process select with a member expression', () => { + const ast = parser.parse('SELECT org.acme.Driver WHERE (address.city == _$param1)', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(1); + result[0].name.should.equal('param1'); + result[0].type.should.equal('String'); + }); + + it('should process select with a member expression with param on RHS', () => { + const ast = parser.parse('SELECT org.acme.Driver WHERE (_$param1 == address.city)', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(1); + result[0].name.should.equal('param1'); + result[0].type.should.equal('String'); + }); + + it('should process select with an array combination operator', () => { + const ast = parser.parse('SELECT org.acme.Driver WHERE ((address.city == _$param1) AND (age > _$param2))', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(2); + result[0].name.should.equal('param1'); + result[0].type.should.equal('String'); + result[1].name.should.equal('param2'); + result[1].type.should.equal('Integer'); + }); + + it('should process select with a 3 level member expression', () => { + const ast = parser.parse('SELECT org.acme.Driver WHERE (address.phoneDetails.phoneNumber == _$param1)', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(1); + result[0].name.should.equal('param1'); + result[0].type.should.equal('String'); + }); + + it('should process select with a 3 level member expression on enum', () => { + const ast = parser.parse('SELECT org.acme.Driver WHERE (address.phoneDetails.contactType == _$param1)', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(1); + result[0].name.should.equal('param1'); + result[0].type.should.equal('String'); + }); + + it('should process select with relationship', () => { + const ast = parser.parse('SELECT org.acme.Vehicle WHERE (_$driverParam == driver)', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(1); + result[0].name.should.equal('driverParam'); + result[0].type.should.equal('String'); + }); + + it('should process select without a WHERE', () => { + const ast = parser.parse('SELECT org.acme.Vehicle', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(0); + }); + + it('should process select with a hardcoded limit', () => { + const ast = parser.parse('SELECT org.acme.Vehicle LIMIT 5', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(0); + }); + + it('should process select with a hardcoded skip', () => { + const ast = parser.parse('SELECT org.acme.Vehicle SKIP 5', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(0); + }); + + it('should throw when using missing property', () => { + (() => { + const ast = parser.parse('SELECT org.acme.Driver WHERE (address.foo == _$param1)', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(0); + }).should.throw(/Property foo does not exist on org.acme.Driver/); + }); + + it('should throw when parameter is not a primitive, enum or relationship', () => { + (() => { + const ast = parser.parse('SELECT org.acme.Driver WHERE (address == _$param1)', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(0); + }).should.throw(/Property address is not a primitive, enum or relationship on org.acme.Driver/); + }); + + it('should throw when using invalid AST', () => { + (() => { + const ast = parser.parse('SELECT org.acme.Driver WHERE (address == _$param1)', { startRule: 'SelectStatement' }); + ast.where.type = 'DAN'; + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + const result = queryAnalyzer.visit(mockQuery, {}); + result.should.not.be.null; + result.length.should.equal(0); + }).should.throw(/Unrecognised type/); + }); + + }); +}); \ No newline at end of file diff --git a/packages/composer-common/test/querymanager.js b/packages/composer-common/test/querymanager.js index 202da86112..d8ae0d7edc 100644 --- a/packages/composer-common/test/querymanager.js +++ b/packages/composer-common/test/querymanager.js @@ -28,6 +28,7 @@ const sinon = require('sinon'); describe('QueryManager', () => { const testQuery = fs.readFileSync(path.resolve(__dirname, 'query', 'test.qry'), 'utf8'); + const testModel = fs.readFileSync(path.resolve(__dirname, 'query', 'model.cto'), 'utf8'); let modelManager; let queryFile; @@ -36,6 +37,7 @@ describe('QueryManager', () => { beforeEach(() => { modelManager = new ModelManager(); + modelManager.addModelFile(testModel); queryFile = sinon.createStubInstance(QueryFile); queryFile.getQueries.returns(dummyQueries); sandbox = sinon.sandbox.create(); @@ -80,4 +82,21 @@ describe('QueryManager', () => { }); }); + describe('#getQuery', () => { + + it('should return a named query', () => { + let qm = new QueryManager(modelManager); + let queryFile = qm.createQueryFile('test.qrl', testQuery); + qm.setQueryFile(queryFile); + qm.getQuery('Q23').getName().should.equal('Q23'); + }); + + it('should return null for unknown query', () => { + let qm = new QueryManager(modelManager); + let queryFile = qm.createQueryFile('test.qrl', testQuery); + qm.setQueryFile(queryFile); + (qm.getQuery('xxxx') === null).should.be.true; + }); + }); + }); diff --git a/packages/composer-connector-embedded/jsdoc.conf b/packages/composer-connector-embedded/jsdoc.json similarity index 100% rename from packages/composer-connector-embedded/jsdoc.conf rename to packages/composer-connector-embedded/jsdoc.json diff --git a/packages/composer-connector-embedded/package.json b/packages/composer-connector-embedded/package.json index 25748dffe1..2d61ccf986 100644 --- a/packages/composer-connector-embedded/package.json +++ b/packages/composer-connector-embedded/package.json @@ -11,7 +11,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test": "nyc mocha --recursive -t 10000" diff --git a/packages/composer-connector-hlf/jsdoc.conf b/packages/composer-connector-hlf/jsdoc.json similarity index 100% rename from packages/composer-connector-hlf/jsdoc.conf rename to packages/composer-connector-hlf/jsdoc.json diff --git a/packages/composer-connector-hlf/package.json b/packages/composer-connector-hlf/package.json index a03ecdb206..1b5935e0de 100644 --- a/packages/composer-connector-hlf/package.json +++ b/packages/composer-connector-hlf/package.json @@ -11,7 +11,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test": "nyc mocha --recursive -t 10000" diff --git a/packages/composer-connector-hlfv1/jsdoc.conf b/packages/composer-connector-hlfv1/jsdoc.json similarity index 100% rename from packages/composer-connector-hlfv1/jsdoc.conf rename to packages/composer-connector-hlfv1/jsdoc.json diff --git a/packages/composer-connector-hlfv1/package.json b/packages/composer-connector-hlfv1/package.json index 417520efbd..e0bca20ba1 100644 --- a/packages/composer-connector-hlfv1/package.json +++ b/packages/composer-connector-hlfv1/package.json @@ -11,7 +11,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test": "nyc mocha --recursive -t 10000" diff --git a/packages/composer-connector-proxy/jsdoc.conf b/packages/composer-connector-proxy/jsdoc.json similarity index 100% rename from packages/composer-connector-proxy/jsdoc.conf rename to packages/composer-connector-proxy/jsdoc.json diff --git a/packages/composer-connector-proxy/package.json b/packages/composer-connector-proxy/package.json index 02cfed488e..5b48de6cb2 100644 --- a/packages/composer-connector-proxy/package.json +++ b/packages/composer-connector-proxy/package.json @@ -11,7 +11,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test": "nyc mocha --recursive -t 10000" diff --git a/packages/composer-connector-server/jsdoc.conf b/packages/composer-connector-server/jsdoc.json similarity index 100% rename from packages/composer-connector-server/jsdoc.conf rename to packages/composer-connector-server/jsdoc.json diff --git a/packages/composer-connector-server/package.json b/packages/composer-connector-server/package.json index 35961d1681..dbefab1c07 100644 --- a/packages/composer-connector-server/package.json +++ b/packages/composer-connector-server/package.json @@ -14,7 +14,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test": "nyc mocha --recursive -t 10000" diff --git a/packages/composer-connector-web/jsdoc.conf b/packages/composer-connector-web/jsdoc.json similarity index 100% rename from packages/composer-connector-web/jsdoc.conf rename to packages/composer-connector-web/jsdoc.json diff --git a/packages/composer-connector-web/package.json b/packages/composer-connector-web/package.json index 3251cfcc43..abc04f1c97 100644 --- a/packages/composer-connector-web/package.json +++ b/packages/composer-connector-web/package.json @@ -13,7 +13,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test": "karma start --single-run" diff --git a/packages/composer-cucumber-steps/jsdoc.conf b/packages/composer-cucumber-steps/jsdoc.json similarity index 100% rename from packages/composer-cucumber-steps/jsdoc.conf rename to packages/composer-cucumber-steps/jsdoc.json diff --git a/packages/composer-cucumber-steps/package.json b/packages/composer-cucumber-steps/package.json index a76e9efe70..f763e2127d 100644 --- a/packages/composer-cucumber-steps/package.json +++ b/packages/composer-cucumber-steps/package.json @@ -7,7 +7,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test-inner": "mocha --recursive -t 10000 && cucumber-js", diff --git a/packages/composer-playground-api/jsdoc.conf b/packages/composer-playground-api/jsdoc.json similarity index 100% rename from packages/composer-playground-api/jsdoc.conf rename to packages/composer-playground-api/jsdoc.json diff --git a/packages/composer-playground-api/package.json b/packages/composer-playground-api/package.json index 64a3833f03..f06ddc0059 100644 --- a/packages/composer-playground-api/package.json +++ b/packages/composer-playground-api/package.json @@ -14,7 +14,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "start": "node cli.js", diff --git a/packages/composer-rest-server/jsdoc.conf b/packages/composer-rest-server/jsdoc.json similarity index 100% rename from packages/composer-rest-server/jsdoc.conf rename to packages/composer-rest-server/jsdoc.json diff --git a/packages/composer-rest-server/package.json b/packages/composer-rest-server/package.json index d05f6b4967..a8ebb58858 100644 --- a/packages/composer-rest-server/package.json +++ b/packages/composer-rest-server/package.json @@ -15,7 +15,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test": "nyc mocha --recursive -t 10000" diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index e8ca1d04d1..aad7111899 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -16,6 +16,9 @@ const connector = require('loopback-connector-composer'); const LoopBackWallet = require('../../lib/loopbackwallet'); +const QueryAnalyzer = require('composer-common').QueryAnalyzer; +const ModelUtil = require('composer-common').ModelUtil; +const LoopbackVisitor = require('composer-common').LoopbackVisitor; /** * Find or create the system wallet for storing identities in. @@ -110,6 +113,33 @@ function createSystemModel(app, dataSource) { } +/** + * Create all of the Composer system models. + * @param {Object} app The LoopBack application. + * @param {Object} dataSource The LoopBack data source. + */ +function createQueryModel(app, dataSource) { + + // Create the query model schema. + let modelSchema = { + name: 'Query', + description: 'Named queries', + plural: '/queries', + base: 'Model' + }; + modelSchema = updateModelSchema(modelSchema); + + // Create the query model which is an anchor for all query methods. + const Query = app.loopback.createModel(modelSchema); + + // Register the query model. + app.model(Query, { + dataSource: dataSource, + public: true + }); + +} + /** * Register all of the Composer system methods. * @param {Object} app The LoopBack application. @@ -138,6 +168,96 @@ function registerSystemMethods(app, dataSource) { } +/** + * Register all of the Composer query methods. + * @param {Object} app The LoopBack application. + * @param {Object} dataSource The LoopBack data source. + * @param {boolean} namespaces true if types should be fully qualified + * @returns {Promise} a promise when complete + */ +function registerQueryMethods(app, dataSource, namespaces) { + + // Grab the query model. + const Query = app.models.Query; + const connector = dataSource.connector; + + return new Promise((resolve, reject) => { + connector.discoverQueries(null, (error, queries) => { + if (error) { + return reject(error); + } + + queries.forEach((query) => { + registerQueryMethod(app, dataSource, Query, connector, query, namespaces); + }); + + resolve(queries); + }); + }); +} + +/** + * Register a composer named query method at a GET method on the REST API. The + * parameters for the named query are exposed as GET query parameters. + * @param {Object} app The LoopBack application. + * @param {Object} dataSource The LoopBack data source. + * @param {Object} Query The LoopBack Query model + * @param {Object} connector The LoopBack connector. + * @param {Query} query the named Composer query to register + * @param {boolean} namespaces true if types should be fully qualified + */ +function registerQueryMethod(app, dataSource, Query, connector, query, namespaces) { + + console.log('Registering named query: ' + query.getName()); + const qa = new QueryAnalyzer(query); + const parameters = qa.analyze(); + const returnType = namespaces + ? query.getSelect().getResource() + : ModelUtil.getShortName(query.getSelect().getResource()); + + // declare the arguments to the query method + let accepts = []; + + // we need the HTTP request so we can get the named parameters off the query string + accepts.push({'arg': 'req', 'type': 'object', 'http': {source: 'req'}}); + accepts.push({'arg': 'options', 'type': 'object', 'http': 'optionsFromRequest'}); + + // we need to declare the parameters and types so that the LoopBack UI + // will generate the web form to enter them + for(let n=0; n < parameters.length; n++) { + const param = parameters[n]; + accepts.push( {arg: param.name, type: LoopbackVisitor.toLoopbackType(param.type), required: true, http: {verb : 'get', source: 'query'}} ); + } + + // Define and register dynamic query method + /* istanbul ignore next */ + const queryMethod = { + [query.getName()]() { + const args = [].slice.apply(arguments); + const httpRequest = args[0]; + const options = args[1]; + const callback = args[args.length-1]; + connector.executeQuery( query.getName(), httpRequest.query, options, callback); + } + }; + Object.assign(Query, queryMethod); + + Query.remoteMethod( + query.getName(), { + description: query.getDescription(), + accepts: accepts, + returns: { + type : [ returnType ], + root: true + }, + http: { + verb: 'get', + path: '/' + query.getName() + } + } + ); +} + /** * Register the 'ping' Composer system method. * @param {Object} app The LoopBack application. @@ -678,6 +798,9 @@ module.exports = function (app, callback) { // Register the system methods. registerSystemMethods(app, dataSource); + // Create the query model + createQueryModel(app, dataSource); + // Discover the model definitions (types) from the connector. // This will go and find all of the non-abstract types in the business network definition. console.log('Discovering types from business network definition ...'); @@ -686,6 +809,12 @@ module.exports = function (app, callback) { }) .then((modelDefinitions) => { + /* istanbul ignore else */ + if(modelDefinitions.length>0) { + // Register the named query methods, passing in whether we should use namespaces + registerQueryMethods(app, dataSource, modelDefinitions[0].namespaces); + } + // For each model definition (type), we need to generate a Loopback model definition JSON file. console.log('Discovered types from business network definition'); console.log('Generating schemas for all types in business network definition ...'); @@ -722,6 +851,7 @@ module.exports = function (app, callback) { callback(); }) .catch((error) => { + console.log('Exception: ' + error ); callback(error); }); }; diff --git a/packages/composer-rest-server/test/assets.js b/packages/composer-rest-server/test/assets.js index 21b577a2f2..c68ba3dd08 100644 --- a/packages/composer-rest-server/test/assets.js +++ b/packages/composer-rest-server/test/assets.js @@ -18,9 +18,7 @@ const AdminConnection = require('composer-admin').AdminConnection; const BrowserFS = require('browserfs/dist/node/index'); const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; -const fs = require('fs'); require('loopback-component-passport'); -const path = require('path'); const server = require('../server/server'); const chai = require('chai'); @@ -141,8 +139,7 @@ const bfs_fs = BrowserFS.BFSRequire('fs'); return adminConnection.connect('defaultProfile', 'admin', 'Xurw3yU9zI0l'); }) .then(() => { - const banana = fs.readFileSync(path.resolve(__dirname, 'bond-network.bna')); - return BusinessNetworkDefinition.fromArchive(banana); + return BusinessNetworkDefinition.fromDirectory('./test/data/bond-network'); }) .then((businessNetworkDefinition) => { serializer = businessNetworkDefinition.getSerializer(); diff --git a/packages/composer-rest-server/test/bond-network.bna b/packages/composer-rest-server/test/bond-network.bna deleted file mode 100644 index 8014c9e547..0000000000 Binary files a/packages/composer-rest-server/test/bond-network.bna and /dev/null differ diff --git a/packages/composer-rest-server/test/data/bond-network/README.md b/packages/composer-rest-server/test/data/bond-network/README.md new file mode 100644 index 0000000000..d3852a90a5 --- /dev/null +++ b/packages/composer-rest-server/test/data/bond-network/README.md @@ -0,0 +1,8 @@ +# Hyperledger Composer Bond Reference Data Demo + +Example business network that stores information about financial bonds on the blockchain. It allows +the issuer of a bond to update the bond information whilst other members of the business network can +only read the bond data. + +The data model for a bond is based on the FpML schema: +http://www.fpml.org/spec/fpml-5-3-2-wd-2/html/reporting/schemaDocumentation/schemas/fpml-asset-5-3_xsd/elements/bond.html diff --git a/packages/composer-rest-server/test/data/bond-network/lib/logic.js b/packages/composer-rest-server/test/data/bond-network/lib/logic.js new file mode 100644 index 0000000000..fb49403093 --- /dev/null +++ b/packages/composer-rest-server/test/data/bond-network/lib/logic.js @@ -0,0 +1,41 @@ +/* + * 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. + */ + +'use strict'; + +/*eslint-disable no-unused-vars*/ +/*eslint-disable no-undef*/ +/*eslint-disable no-var*/ + +/** + * Publish a new bond + * @param {org.acme.bond.PublishBond} publishBond - the publishBond transaction + * @transaction + * @return {Promise} a promise when completed + */ +function publish(publishBond) { + + return getAssetRegistry('org.acme.bond.BondAsset') + .then(function (registry) { + var factory = getFactory(); + // Create the bond asset. + var bondAsset = factory.newResource('org.acme.bond', 'BondAsset', publishBond.ISINCode); + bondAsset.bond = publishBond.bond; + // Add the bond asset to the registry. + return registry.add(bondAsset); + }); +} + +/*eslint-enable no-unused-vars*/ +/*eslint-enable no-undef*/ \ No newline at end of file diff --git a/packages/composer-rest-server/test/data/bond-network/models/bond.cto b/packages/composer-rest-server/test/data/bond-network/models/bond.cto new file mode 100644 index 0000000000..ed3140ebb8 --- /dev/null +++ b/packages/composer-rest-server/test/data/bond-network/models/bond.cto @@ -0,0 +1,59 @@ +/** + * Definition of a Bond, based on the FpML schema: + * http://www.fpml.org/spec/fpml-5-3-2-wd-2/html/reporting/schemaDocumentation/schemas/fpml-asset-5-3_xsd/elements/bond.html + * + */ +namespace org.acme.bond + +enum CouponType { + o FIXED + o FLOATING +} + +participant Member identified by memberId { + o String memberId + o String name +} + +participant Issuer extends Member { +} + +enum PeriodEnum { + o DAY + o WEEK + o MONTH + o YEAR +} + +concept PaymentFrequency { + o Integer periodMultiplier + o PeriodEnum period +} + +concept Bond { + o String[] instrumentId + o String description optional + o String currency optional + o String[] exchangeId + o String clearanceSystem optional + o String definition optional + o String seniority optional + o CouponType couponType optional + o Double couponRate optional + o DateTime maturity + o Double parValue + o Double faceAmount + o PaymentFrequency paymentFrequency + o String dayCountFraction + --> Issuer issuer +} + +asset BondAsset identified by ISINCode { + o String ISINCode + o Bond bond +} + +transaction PublishBond { + o String ISINCode + o Bond bond +} \ No newline at end of file diff --git a/packages/composer-rest-server/test/data/bond-network/package.json b/packages/composer-rest-server/test/data/bond-network/package.json new file mode 100644 index 0000000000..d5f6c27681 --- /dev/null +++ b/packages/composer-rest-server/test/data/bond-network/package.json @@ -0,0 +1 @@ +{"name":"bond-network","version":"0.1.0","description":"Bond Reference Data Sharing Business Network","scripts":{"prepublish":"mkdirp ./dist && composer archive create --sourceType dir --sourceName . -a ./dist/bond-network.bna","pretest":"npm run lint","lint":"eslint .","postlint":"npm run licchk","licchk":"license-check","postlicchk":"npm run doc","doc":"jsdoc --pedantic --recurse -c jsdoc.conf","test":"mocha -t 0 --recursive","deploy":"./scripts/deploy.sh"},"repository":{"type":"git","url":"https://github.com/hyperledger/composer-sample-networks.git"},"keywords":["bonds","reference data","finance"],"author":"Hyperledger Composer","license":"Apache-2.0","devDependencies":{"browserfs":"^1.2.0","chai":"^3.5.0","composer-admin":"latest","composer-cli":"latest","composer-client":"latest","composer-connector-embedded":"latest","eslint":"^3.6.1","istanbul":"^0.4.5","jsdoc":"^3.4.1","license-check":"^1.1.5","mkdirp":"^0.5.1","mocha":"^3.2.0","moment":"^2.17.1"},"license-check-config":{"src":["**/*.js","!./coverage/**/*","!./node_modules/**/*","!./out/**/*","!./scripts/**/*"],"path":"header.txt","blocking":true,"logInfo":false,"logError":true}} \ No newline at end of file diff --git a/packages/composer-rest-server/test/data/bond-network/permissions.acl b/packages/composer-rest-server/test/data/bond-network/permissions.acl new file mode 100644 index 0000000000..9b9221c36a --- /dev/null +++ b/packages/composer-rest-server/test/data/bond-network/permissions.acl @@ -0,0 +1,19 @@ +/** + * Access Control List for the bond data + */ +rule Issuer { + description: "Allow full access to the issuer of a bond" + participant(i): "org.acme.bond.Issuer" + operation: ALL + resource(a): "org.acme.bond.BondAsset" + condition: (a.bond.issuer.memberId === i.memberId) + action: ALLOW +} + +rule Default { + description: "Allow read access" + participant: "org.acme.bond.*" + operation: ALL + resource: "org.acme.bond.*" + action: ALLOW +} \ No newline at end of file diff --git a/packages/composer-rest-server/test/data/bond-network/queries.qry b/packages/composer-rest-server/test/data/bond-network/queries.qry new file mode 100644 index 0000000000..50c197dfd9 --- /dev/null +++ b/packages/composer-rest-server/test/data/bond-network/queries.qry @@ -0,0 +1,4 @@ +query findBondByFaceAmount { + description: "Find all bonds with a face amount greater than _$faceAmount" + statement: SELECT org.acme.bond.BondAsset WHERE (bond.faceAmount > _$faceAmount) +} diff --git a/packages/composer-rest-server/test/participants.js b/packages/composer-rest-server/test/participants.js index ca23fbaeb9..2394826399 100644 --- a/packages/composer-rest-server/test/participants.js +++ b/packages/composer-rest-server/test/participants.js @@ -18,9 +18,7 @@ const AdminConnection = require('composer-admin').AdminConnection; const BrowserFS = require('browserfs/dist/node/index'); const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; -const fs = require('fs'); require('loopback-component-passport'); -const path = require('path'); const server = require('../server/server'); const chai = require('chai'); @@ -69,8 +67,7 @@ const bfs_fs = BrowserFS.BFSRequire('fs'); return adminConnection.connect('defaultProfile', 'admin', 'Xurw3yU9zI0l'); }) .then(() => { - const banana = fs.readFileSync(path.resolve(__dirname, 'bond-network.bna')); - return BusinessNetworkDefinition.fromArchive(banana); + return BusinessNetworkDefinition.fromDirectory('./test/data/bond-network'); }) .then((businessNetworkDefinition) => { serializer = businessNetworkDefinition.getSerializer(); diff --git a/packages/composer-rest-server/test/queries.js b/packages/composer-rest-server/test/queries.js new file mode 100644 index 0000000000..a8e80bb050 --- /dev/null +++ b/packages/composer-rest-server/test/queries.js @@ -0,0 +1,188 @@ +/* + * 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. + */ + +'use strict'; + +const AdminConnection = require('composer-admin').AdminConnection; +const BrowserFS = require('browserfs/dist/node/index'); +const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; +const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; +require('loopback-component-passport'); +const server = require('../server/server'); + +const chai = require('chai'); +chai.should(); +chai.use(require('chai-http')); + +const bfs_fs = BrowserFS.BFSRequire('fs'); + +['always', 'never'].forEach((namespaces) => { + + describe(`Query REST API unit tests namespaces[${namespaces}]`, () => { + + const assetData = [{ + $class: 'org.acme.bond.BondAsset', + ISINCode: 'ISIN_1', + bond: { + $class: 'org.acme.bond.Bond', + dayCountFraction: 'EOM', + exchangeId: [ + 'NYSE' + ], + faceAmount: 1000, + instrumentId: [ + 'AliceCorp' + ], + issuer: 'resource:org.acme.bond.Issuer#1', + maturity: '2018-02-27T21:03:52.000Z', + parValue: 1000, + paymentFrequency: { + $class: 'org.acme.bond.PaymentFrequency', + period: 'MONTH', + periodMultiplier: 6 + } + } + }, { + $class: 'org.acme.bond.BondAsset', + ISINCode: 'ISIN_2', + bond: { + $class: 'org.acme.bond.Bond', + dayCountFraction: 'EOM', + exchangeId: [ + 'NYSE' + ], + faceAmount: 1000, + instrumentId: [ + 'BobCorp' + ], + issuer: 'resource:org.acme.bond.Issuer#1', + maturity: '2018-02-27T21:03:52.000Z', + parValue: 1000, + paymentFrequency: { + $class: 'org.acme.bond.PaymentFrequency', + period: 'MONTH', + periodMultiplier: 6 + } + } + }, { + $class: 'org.acme.bond.BondAsset', + ISINCode: 'ISIN_3', + bond: { + $class: 'org.acme.bond.Bond', + dayCountFraction: 'EOM', + exchangeId: [ + 'NYSE' + ], + faceAmount: 1000, + instrumentId: [ + 'CharlieCorp' + ], + issuer: 'resource:org.acme.bond.Issuer#1', + maturity: '2018-02-27T21:03:52.000Z', + parValue: 1000, + paymentFrequency: { + $class: 'org.acme.bond.PaymentFrequency', + period: 'MONTH', + periodMultiplier: 6 + } + } + }, { + // $class: 'org.acme.bond.BondAsset', + ISINCode: 'ISIN_4', + bond: { + $class: 'org.acme.bond.Bond', + dayCountFraction: 'EOM', + exchangeId: [ + 'NYSE' + ], + faceAmount: 1000, + instrumentId: [ + 'DogeCorp' + ], + issuer: 'resource:org.acme.bond.Issuer#1', + maturity: '2018-02-27T21:03:52.000Z', + parValue: 1000, + paymentFrequency: { + $class: 'org.acme.bond.PaymentFrequency', + period: 'MONTH', + periodMultiplier: 6 + } + } + }]; + + let app; + let businessNetworkConnection; + let assetRegistry; + let serializer; + + before(() => { + BrowserFS.initialize(new BrowserFS.FileSystem.InMemory()); + const adminConnection = new AdminConnection({ fs: bfs_fs }); + return adminConnection.createProfile('defaultProfile', { + type : 'embedded' + }) + .then(() => { + return adminConnection.connect('defaultProfile', 'admin', 'Xurw3yU9zI0l'); + }) + .then(() => { + return BusinessNetworkDefinition.fromDirectory('./test/data/bond-network'); + }) + .then((businessNetworkDefinition) => { + serializer = businessNetworkDefinition.getSerializer(); + return adminConnection.deploy(businessNetworkDefinition); + }) + .then(() => { + return server({ + connectionProfileName: 'defaultProfile', + businessNetworkIdentifier: 'bond-network', + participantId: 'admin', + participantPwd: 'adminpw', + fs: bfs_fs, + namespaces: namespaces + }); + }) + .then((app_) => { + app = app_; + businessNetworkConnection = new BusinessNetworkConnection({ fs: bfs_fs }); + return businessNetworkConnection.connect('defaultProfile', 'bond-network', 'admin', 'Xurw3yU9zI0l'); + }) + .then(() => { + return businessNetworkConnection.getAssetRegistry('org.acme.bond.BondAsset'); + }) + .then((assetRegistry_) => { + assetRegistry = assetRegistry_; + return assetRegistry.addAll([ + serializer.fromJSON(assetData[0]), + serializer.fromJSON(assetData[1]) + ]); + }); + }); + + describe(`GET / namespaces[${namespaces}]`, () => { + + it('should return all of the assets', () => { + return chai.request(app) + .get('/api/queries/findBondByFaceAmount?faceAmount=500') + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[0], + assetData[1], + ]); + }); + }); + + }); + }); +}); diff --git a/packages/composer-rest-server/test/server/boot/composer-discovery.js b/packages/composer-rest-server/test/server/boot/composer-discovery.js index ed2778458b..3e3f0620b3 100644 --- a/packages/composer-rest-server/test/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/test/server/boot/composer-discovery.js @@ -19,7 +19,6 @@ const boot = require('loopback-boot'); const BrowserFS = require('browserfs/dist/node/index'); const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; const composerDiscovery = require('../../../server/boot/composer-discovery'); -const fs = require('fs'); const loopback = require('loopback'); require('loopback-component-passport'); const LoopBackWallet = require('../../../lib/loopbackwallet'); @@ -47,8 +46,7 @@ describe('composer-discovery boot script', () => { return adminConnection.connect('defaultProfile', 'admin', 'Xurw3yU9zI0l'); }) .then(() => { - const banana = fs.readFileSync(path.resolve(__dirname, '..', '..', 'bond-network.bna')); - return BusinessNetworkDefinition.fromArchive(banana); + return BusinessNetworkDefinition.fromDirectory('./test/data/bond-network'); }) .then((businessNetworkDefinition) => { return adminConnection.deploy(businessNetworkDefinition); @@ -131,6 +129,20 @@ describe('composer-discovery boot script', () => { }); }); + it('should handle an error from discovering the queries', () => { + const originalCreateDataSource = app.loopback.createDataSource; + sandbox.stub(app.loopback, 'createDataSource', (name, settings) => { + let result = originalCreateDataSource.call(app.loopback, name, settings); + sandbox.stub(result.connector, 'discoverQueries').yields(new Error('such error')); + return result; + }); + const cb = sinon.stub(); + return composerDiscovery(app, cb) + .then(() => { + sinon.assert.calledOnce(cb); + }); + }); + it('should handle an error from discovering the schemas', () => { const originalCreateDataSource = app.loopback.createDataSource; sandbox.stub(app.loopback, 'createDataSource', (name, settings) => { diff --git a/packages/composer-rest-server/test/server/server.js b/packages/composer-rest-server/test/server/server.js index 648db8642c..d94fcc2cf5 100644 --- a/packages/composer-rest-server/test/server/server.js +++ b/packages/composer-rest-server/test/server/server.js @@ -17,8 +17,6 @@ const AdminConnection = require('composer-admin').AdminConnection; const BrowserFS = require('browserfs/dist/node/index'); const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; -const fs = require('fs'); -const path = require('path'); const server = require('../../server/server'); const chai = require('chai'); @@ -42,8 +40,7 @@ describe('server', () => { return adminConnection.connect('defaultProfile', 'admin', 'Xurw3yU9zI0l'); }) .then(() => { - const banana = fs.readFileSync(path.resolve(__dirname, '..', 'bond-network.bna')); - return BusinessNetworkDefinition.fromArchive(banana); + return BusinessNetworkDefinition.fromDirectory('./test/data/bond-network'); }) .then((businessNetworkDefinition) => { return adminConnection.deploy(businessNetworkDefinition); diff --git a/packages/composer-rest-server/test/system.js b/packages/composer-rest-server/test/system.js index 550bcd30ec..89dff8ed97 100644 --- a/packages/composer-rest-server/test/system.js +++ b/packages/composer-rest-server/test/system.js @@ -18,9 +18,7 @@ const AdminConnection = require('composer-admin').AdminConnection; const BrowserFS = require('browserfs/dist/node/index'); const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; -const fs = require('fs'); require('loopback-component-passport'); -const path = require('path'); const server = require('../server/server'); const version = require('../package.json').version; @@ -106,8 +104,7 @@ describe('System REST API unit tests', () => { return adminConnection.connect('defaultProfile', 'admin', 'Xurw3yU9zI0l'); }) .then(() => { - const banana = fs.readFileSync(path.resolve(__dirname, 'bond-network.bna')); - return BusinessNetworkDefinition.fromArchive(banana); + return BusinessNetworkDefinition.fromDirectory('./test/data/bond-network'); }) .then((businessNetworkDefinition) => { serializer = businessNetworkDefinition.getSerializer(); diff --git a/packages/composer-rest-server/test/transactions.js b/packages/composer-rest-server/test/transactions.js index 005f30ff52..27589c03b1 100644 --- a/packages/composer-rest-server/test/transactions.js +++ b/packages/composer-rest-server/test/transactions.js @@ -18,9 +18,7 @@ const AdminConnection = require('composer-admin').AdminConnection; const BrowserFS = require('browserfs/dist/node/index'); const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; -const fs = require('fs'); require('loopback-component-passport'); -const path = require('path'); const server = require('../server/server'); const chai = require('chai'); @@ -147,8 +145,7 @@ const bfs_fs = BrowserFS.BFSRequire('fs'); return adminConnection.connect('defaultProfile', 'admin', 'Xurw3yU9zI0l'); }) .then(() => { - const banana = fs.readFileSync(path.resolve(__dirname, 'bond-network.bna')); - return BusinessNetworkDefinition.fromArchive(banana); + return BusinessNetworkDefinition.fromDirectory('./test/data/bond-network'); }) .then((businessNetworkDefinition) => { serializer = businessNetworkDefinition.getSerializer(); diff --git a/packages/composer-runtime-embedded/jsdoc.conf b/packages/composer-runtime-embedded/jsdoc.json similarity index 100% rename from packages/composer-runtime-embedded/jsdoc.conf rename to packages/composer-runtime-embedded/jsdoc.json diff --git a/packages/composer-runtime-embedded/package.json b/packages/composer-runtime-embedded/package.json index fd88e39c65..061762aaaf 100644 --- a/packages/composer-runtime-embedded/package.json +++ b/packages/composer-runtime-embedded/package.json @@ -11,7 +11,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test": "nyc mocha --recursive -t 10000" diff --git a/packages/composer-runtime-pouchdb/jsdoc.conf b/packages/composer-runtime-pouchdb/jsdoc.json similarity index 100% rename from packages/composer-runtime-pouchdb/jsdoc.conf rename to packages/composer-runtime-pouchdb/jsdoc.json diff --git a/packages/composer-runtime-pouchdb/package.json b/packages/composer-runtime-pouchdb/package.json index 87ef520d59..e3b9cbb8be 100644 --- a/packages/composer-runtime-pouchdb/package.json +++ b/packages/composer-runtime-pouchdb/package.json @@ -11,7 +11,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test": "nyc mocha --recursive -t 10000" diff --git a/packages/composer-runtime-web/jsdoc.conf b/packages/composer-runtime-web/jsdoc.json similarity index 100% rename from packages/composer-runtime-web/jsdoc.conf rename to packages/composer-runtime-web/jsdoc.json diff --git a/packages/composer-runtime-web/package.json b/packages/composer-runtime-web/package.json index c5bf17413f..6f29bbe8cc 100644 --- a/packages/composer-runtime-web/package.json +++ b/packages/composer-runtime-web/package.json @@ -11,7 +11,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "test": "karma start --single-run" diff --git a/packages/composer-runtime/jsdoc.conf b/packages/composer-runtime/jsdoc.json similarity index 100% rename from packages/composer-runtime/jsdoc.conf rename to packages/composer-runtime/jsdoc.json diff --git a/packages/composer-runtime/package.json b/packages/composer-runtime/package.json index f32eda1786..4c1fb28678 100644 --- a/packages/composer-runtime/package.json +++ b/packages/composer-runtime/package.json @@ -13,7 +13,7 @@ "pretest": "npm run licchk", "licchk": "license-check", "postlicchk": "npm run doc", - "doc": "jsdoc --pedantic --recurse -c jsdoc.conf", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", "postdoc": "npm run lint", "lint": "eslint .", "postlint": "npm run browserify", diff --git a/packages/composer-systests/jsdoc.conf b/packages/composer-systests/jsdoc.conf deleted file mode 100644 index 8cfff66f9e..0000000000 --- a/packages/composer-systests/jsdoc.conf +++ /dev/null @@ -1,36 +0,0 @@ -{ - "tags": { - "allowUnknownTags": true, - "dictionaries": ["jsdoc","closure"] - }, - "source": { - "include": [ - "./lib", - "./index.js" - ], - "includePattern": ".+\\.js(doc|x)?$" - }, - "plugins": ["plugins/markdown"], - "templates": { - "logoFile": "", - "cleverLinks": false, - "monospaceLinks": false, - "dateFormat": "ddd MMM Do YYYY", - "outputSourceFiles": true, - "outputSourcePath": true, - "systemName": "Hyperledger Composer", - "footer": "", - "copyright": "Released under the Apache License v2.0", - "navType": "vertical", - "theme": "spacelab", - "linenums": true, - "collapseSymbols": false, - "inverseNav": true, - "protocol": "html://", - "methodHeadingReturns": false - }, - "markdown": { - "parser": "gfm", - "hardwrap": true - } -} diff --git a/packages/composer-website/jekylldocs/business-network/query.md b/packages/composer-website/jekylldocs/business-network/query.md index fb741d0193..e204939263 100644 --- a/packages/composer-website/jekylldocs/business-network/query.md +++ b/packages/composer-website/jekylldocs/business-network/query.md @@ -12,13 +12,17 @@ excerpt: Queries are used to return data about the blockchain world-state; for e >**Warning**: The status of this feature is experimental. You **must** use Hyperledger Composer v0.8+ with the the HLFv1 runtime to use queries. We welcome feedback and comments while we continue to iterate upon query functionality. The API may change based on the feedback received. -Queries are used to return data about the blockchain world-state; for example, you could write a query to return all drivers over a defined age parameter, or all drivers with a specific name. +Queries are used to return data about the blockchain world-state; for example, you could write a query to return all drivers over a specified age, or all drivers with a specific name. The composer-rest-server component exposes named queries via the generated REST API. Queries are an optional component of a business network definition, written in a single query file (`queries.qry`). Note: Queries are supported by the {{site.data.conrefs.hlf_full}} v1.0, embedded and web runtimes. The query support for the embedded and web runtimes currently has limitations and is unstable. When using the {{site.data.conrefs.hlf_full}} v1.0 runtime {{site.data.conrefs.hlf_full}} must be configured to use CouchDB persistence. Queries are **not** supported with the {{site.data.conrefs.hlf_full}} v0.6 runtime. -## Writing Queries +## Types of Queries + +{site.data.conrefs.composer}} supports two types of queries: named queries and dynamic queries. Named queries are specified in the business network definition and are exposed as GET methods by the composer-rest-server component. Dynamic queries may be constructed dynamically at runtime within a Transaction Processor function, or from client code. + +## Writing Named Queries Queries must contain a description and a statement. Query descriptions are a string that describe the function of the query. Query statements contain the operators and functions that control the query behavior. @@ -35,8 +39,27 @@ query Q1{ } ``` -For more information on the specifics of the {{site.data.conrefs.composer_full}} query language, see the [query language reference documentation](../reference/query-language.html). +### Query Parameters + +Queries may embed parameters using the `_$` syntax. Note that query parameters must be primitive types (String, Integer, Double, Long, Boolean, DateTime), a Relationship or an Enumeration. +The named query below is defined in terms of 3 parameters: + +``` +query Q18 { + description: "Select all drivers aged older than PARAM" + statement: + SELECT org.acme.Driver + WHERE (_$ageParam < age) + ORDER BY [lastName ASC, firstName DESC] + LIMIT _$limitParam + SKIP _$skipParam +} +``` + +Query parameters are automatically exposed via the GET method created for named queries by the composer-rest-server. + +For more information on the specifics of the {{site.data.conrefs.composer_full}} query language, see the [query language reference documentation](../reference/query-language.html). ## Using Queries @@ -49,62 +72,3 @@ For more information on the query APIs, see the [API documentation](../jsdoc/ind When returning the results of a query, your access control rules are applied to the results. Any content which the current user does not have authority to view is stripped from the results. For example, if the current user sends a query that would return all assets, if they only have authority to view a limited selection of assets, the query would return only that limited set of assets. - - diff --git a/packages/loopback-connector-composer/lib/businessnetworkconnector.js b/packages/loopback-connector-composer/lib/businessnetworkconnector.js index 906b5182de..98cf0b9cee 100644 --- a/packages/loopback-connector-composer/lib/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/lib/businessnetworkconnector.js @@ -24,6 +24,8 @@ const LoopbackVisitor = require('composer-common').LoopbackVisitor; const NodeCache = require('node-cache'); const ParticipantDeclaration = require('composer-common').ParticipantDeclaration; const TransactionDeclaration = require('composer-common').TransactionDeclaration; +const QueryAnalyzer = require('composer-common').QueryAnalyzer; +const util = require('util'); /** * A Loopback connector for exposing the Blockchain Solution Framework to Loopback enabled applications. @@ -908,6 +910,68 @@ class BusinessNetworkConnector extends Connector { }); } + /** + * Execute a named query and returns the results + * @param {string} queryName The name of the query to execute + * @param {object} queryParameters The query parameters + * @param {Object} options The LoopBack options. + * @param {function} callback The callback to call when complete. + * @returns {Promise} A promise that is resolved when complete. + */ + executeQuery( queryName, queryParameters, options, callback) { + debug('executeQuery', options); + debug('queryName', queryName); + debug('queryParameters', util.inspect(queryParameters)); + + return this.ensureConnected(options) + .then((businessNetworkConnection) => { + // all query parameters come in as string + // so we need to coerse them to their correct types + // before executing a query + // TODO (DCS) not sure this should be done here, as it will also + // need to be done on the runtime side + const query = businessNetworkConnection.getBusinessNetwork().getQueryManager().getQuery(queryName); + + if(!query) { + throw new Error('Named query ' + queryName + ' does not exist in the business network.'); + } + + const qa = new QueryAnalyzer(query); + const parameters = qa.analyze(); + + for(let n=0; n < parameters.length; n++) { + const param = parameters[n]; + const paramValue = queryParameters[param.name]; + switch(param.type) { + case 'Integer': + case 'Long': + queryParameters[param.name] = parseInt(paramValue,10); + break; + case 'Double': + queryParameters[param.name] = parseFloat(paramValue); + break; + case 'DateTime': + queryParameters[param.name] = Date.parse(paramValue); + break; + case 'Boolean': + queryParameters[param.name] = (paramValue === 'true'); + break; + } + } + + return businessNetworkConnection.query(queryName, queryParameters); + }) + .then((queryResult) => { + const result = queryResult.map((item) => { + return this.serializer.toJSON(item); + }); + callback(null, result); + }) + .catch((error) => { + callback(error); + }); + } + /** * Get the transaction with the specified ID from the transaction registry. * @param {string} id The ID for the transaction. @@ -953,7 +1017,7 @@ class BusinessNetworkConnector extends Connector { let modelNames = new Set(); let namesAreUnique = true; - // Find all the types in the buiness network. + // Find all the types in the business network. const classDeclarations = this.introspector.getClassDeclarations() .filter((classDeclaration) => { @@ -1008,7 +1072,8 @@ class BusinessNetworkConnector extends Connector { classDeclarations.forEach((classDeclaration) => { models.push({ type : 'table', - name : namespaces ? classDeclaration.getFullyQualifiedName() : classDeclaration.getName() + name : namespaces ? classDeclaration.getFullyQualifiedName() : classDeclaration.getName(), + namespaces : namespaces }); }); @@ -1021,6 +1086,25 @@ class BusinessNetworkConnector extends Connector { }); } + /** + * Retrieve the list of all named queries in the business network + * @param {Object} options the options provided by Loopback. + * @param {function} callback the callback to call when complete. + * @returns {Promise} A promise that is resolved when complete. + */ + discoverQueries(options, callback) { + debug('discoverQueries', options); + return this.ensureConnected(options) + .then(() => { + const queries = this.businessNetworkDefinition.getQueryManager().getQueries(); + callback(null, queries); + }) + .catch((error) => { + debug('discoverQueries', 'error thrown discovering list of query declarations', error); + callback(error); + }); + } + /** * Retrieve the model definition for the specified model name. * @param {string} object The name of the model. @@ -1062,8 +1146,8 @@ class BusinessNetworkConnector extends Connector { first : true, modelFile : classDeclaration.getModelFile() }); - callback(null, schema); + callback(null, schema); }) .catch((error) => { debug('discoverSchemas', 'error thrown generating schema', error); diff --git a/packages/loopback-connector-composer/test/assets.js b/packages/loopback-connector-composer/test/assets.js index bd0a52ce98..65d2e9a1df 100644 --- a/packages/loopback-connector-composer/test/assets.js +++ b/packages/loopback-connector-composer/test/assets.js @@ -19,9 +19,7 @@ const BrowserFS = require('browserfs/dist/node/index'); const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; const connector = require('..'); -const fs = require('fs'); const loopback = require('loopback'); -const path = require('path'); const Util = require('composer-common').Util; const chai = require('chai'); @@ -142,8 +140,7 @@ const bfs_fs = BrowserFS.BFSRequire('fs'); return adminConnection.connect('defaultProfile', 'admin', 'Xurw3yU9zI0l'); }) .then(() => { - const banana = fs.readFileSync(path.resolve(__dirname, 'bond-network.bna')); - return BusinessNetworkDefinition.fromArchive(banana); + return BusinessNetworkDefinition.fromDirectory('./test/data/bond-network'); }) .then((businessNetworkDefinition) => { serializer = businessNetworkDefinition.getSerializer(); diff --git a/packages/loopback-connector-composer/test/bond-network.bna b/packages/loopback-connector-composer/test/bond-network.bna deleted file mode 100644 index ea3eea4700..0000000000 Binary files a/packages/loopback-connector-composer/test/bond-network.bna and /dev/null differ diff --git a/packages/loopback-connector-composer/test/businessnetworkconnector.js b/packages/loopback-connector-composer/test/businessnetworkconnector.js index 52d634417f..1567d656b0 100644 --- a/packages/loopback-connector-composer/test/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/test/businessnetworkconnector.js @@ -24,6 +24,9 @@ const IdentityRegistry = require('composer-client/lib/identityregistry'); const Introspector = require('composer-common').Introspector; const LoopbackVisitor = require('composer-common').LoopbackVisitor; const ModelManager = require('composer-common').ModelManager; +const Query = require('composer-common').Query; +const QueryManager = require('composer-common').QueryManager; +const QueryFile = require('composer-common').QueryFile; const NodeCache = require('node-cache'); const ParticipantRegistry = require('composer-client/lib/participantregistry'); const Serializer = require('composer-common').Serializer; @@ -45,6 +48,11 @@ describe('BusinessNetworkConnector', () => { } asset BaseAsset identified by theValue { o String theValue + o Integer theInteger optional + o Boolean theBoolean optional + o DateTime theDateTime optional + o Double theDouble optional + o Long theLong optional } participant BaseParticipant identified by theValue { o String theValue @@ -57,11 +65,13 @@ describe('BusinessNetworkConnector', () => { let mockBusinessNetworkConnection; let mockBusinessNetworkDefinition; let mockSerializer; + let mockQueryManager; let sandbox; let testConnector; let modelManager; let factory; let introspector; + let mockQueryFile; beforeEach(() => { @@ -72,6 +82,12 @@ describe('BusinessNetworkConnector', () => { participantPwd : 'MockEnrollmentPwd' }; + // create real instances + modelManager = new ModelManager(); + modelManager.addModelFile(MODEL_FILE); + introspector = new Introspector(modelManager); + factory = new Factory(modelManager); + // // create mocks mockBusinessNetworkConnection = sinon.createStubInstance(BusinessNetworkConnection); mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); @@ -84,12 +100,6 @@ describe('BusinessNetworkConnector', () => { mockBusinessNetworkConnection.submitTransaction.resolves(); mockBusinessNetworkDefinition.getIntrospector.returns(introspector); - // // create real instances - modelManager = new ModelManager(); - modelManager.addModelFile(MODEL_FILE); - introspector = new Introspector(modelManager); - factory = new Factory(modelManager); - sandbox = sinon.sandbox.create(); // setup test instance @@ -2131,6 +2141,203 @@ describe('BusinessNetworkConnector', () => { }); + describe('#executeQuery', () => { + + beforeEach(() => { + // create mock query + mockQueryFile = sinon.createStubInstance(QueryFile); + mockQueryFile.getModelManager.returns(modelManager); + let stringQuery = Query.buildQuery(mockQueryFile, 'stringQuery', 'test query', 'SELECT org.acme.base.BaseAsset WHERE (theValue==_$param1)'); + let integerQuery = Query.buildQuery(mockQueryFile, 'integerQuery', 'test query', 'SELECT org.acme.base.BaseAsset WHERE (theInteger==_$param1)'); + let doubleQuery = Query.buildQuery(mockQueryFile, 'doubleQuery', 'test query', 'SELECT org.acme.base.BaseAsset WHERE (theDouble==_$param1)'); + let longQuery = Query.buildQuery(mockQueryFile, 'longQuery', 'test query', 'SELECT org.acme.base.BaseAsset WHERE (theLong==_$param1)'); + let dateTimeQuery = Query.buildQuery(mockQueryFile, 'dateTimeQuery', 'test query', 'SELECT org.acme.base.BaseAsset WHERE (theDateTime==_$param1)'); + let booleanQuery = Query.buildQuery(mockQueryFile, 'booleanQuery', 'test query', 'SELECT org.acme.base.BaseAsset WHERE (theBoolean==_$param1)'); + + // // create mocks + mockQueryManager = sinon.createStubInstance(QueryManager); + mockQueryManager.getQuery.withArgs('stringQuery').returns(stringQuery); + mockQueryManager.getQuery.withArgs('integerQuery').returns(integerQuery); + mockQueryManager.getQuery.withArgs('doubleQuery').returns(doubleQuery); + mockQueryManager.getQuery.withArgs('longQuery').returns(longQuery); + mockQueryManager.getQuery.withArgs('dateTimeQuery').returns(dateTimeQuery); + mockQueryManager.getQuery.withArgs('booleanQuery').returns(booleanQuery); + + // // setup mocks + mockBusinessNetworkDefinition.getQueryManager.returns(mockQueryManager); + + sinon.stub(testConnector, 'ensureConnected').resolves(mockBusinessNetworkConnection); + testConnector.connected = true; + testConnector.serializer = mockSerializer; + testConnector.businessNetworkDefinition = mockBusinessNetworkDefinition; + mockBusinessNetworkConnection.getBusinessNetwork.returns(mockBusinessNetworkDefinition); + mockBusinessNetworkConnection.query.resolves([{$class: 'org.acme.base.BaseAsset', theValue: 'my value'}]); + mockSerializer.toJSON.returns({$class: 'org.acme.base.BaseAsset', theValue: 'my value'}); + }); + + it('should call the executeQuery with an expected string result', () => { + + const cb = sinon.stub(); + return testConnector.executeQuery( 'stringQuery', { param1: 'blue' }, {test: 'options' }, cb) + .then(( queryResult) => { + sinon.assert.calledOnce(testConnector.ensureConnected); + sinon.assert.calledWith(testConnector.ensureConnected, { test: 'options' }); + + const result = cb.args[0][1]; // First call, second argument (error, queryResult) + result.should.deep.equal([{ + $class: 'org.acme.base.BaseAsset', + theValue: 'my value' + }]); + }); + }); + + it('should call the executeQuery with an expected double result', () => { + + const cb = sinon.stub(); + return testConnector.executeQuery( 'doubleQuery', { param1: '10.2' }, {test: 'options' }, cb) + .then(( queryResult) => { + sinon.assert.calledOnce(testConnector.ensureConnected); + sinon.assert.calledWith(testConnector.ensureConnected, { test: 'options' }); + + const result = cb.args[0][1]; // First call, second argument (error, queryResult) + result.should.deep.equal([{ + $class: 'org.acme.base.BaseAsset', + theValue: 'my value' + }]); + }); + }); + + it('should call the executeQuery with an expected long result', () => { + + const cb = sinon.stub(); + return testConnector.executeQuery( 'longQuery', { param1: '100' }, {test: 'options' }, cb) + .then(( queryResult) => { + sinon.assert.calledOnce(testConnector.ensureConnected); + sinon.assert.calledWith(testConnector.ensureConnected, { test: 'options' }); + + const result = cb.args[0][1]; // First call, second argument (error, queryResult) + result.should.deep.equal([{ + $class: 'org.acme.base.BaseAsset', + theValue: 'my value' + }]); + }); + }); + + it('should call the executeQuery with an expected integer result', () => { + + const cb = sinon.stub(); + return testConnector.executeQuery( 'integerQuery', { param1: '100' }, {test: 'options' }, cb) + .then(( queryResult) => { + sinon.assert.calledOnce(testConnector.ensureConnected); + sinon.assert.calledWith(testConnector.ensureConnected, { test: 'options' }); + + const result = cb.args[0][1]; // First call, second argument (error, queryResult) + result.should.deep.equal([{ + $class: 'org.acme.base.BaseAsset', + theValue: 'my value' + }]); + }); + }); + + it('should call the executeQuery with an expected dateTime result', () => { + + const cb = sinon.stub(); + return testConnector.executeQuery( 'dateTimeQuery', { param1: '2007-04-05T14:30' }, {test: 'options' }, cb) + .then(( queryResult) => { + sinon.assert.calledOnce(testConnector.ensureConnected); + sinon.assert.calledWith(testConnector.ensureConnected, { test: 'options' }); + + const result = cb.args[0][1]; // First call, second argument (error, queryResult) + result.should.deep.equal([{ + $class: 'org.acme.base.BaseAsset', + theValue: 'my value' + }]); + }); + }); + + it('should call the executeQuery with an expected boolean result', () => { + + const cb = sinon.stub(); + return testConnector.executeQuery( 'booleanQuery', { param1: 'false' }, {test: 'options' }, cb) + .then(( queryResult) => { + sinon.assert.calledOnce(testConnector.ensureConnected); + sinon.assert.calledWith(testConnector.ensureConnected, { test: 'options' }); + + const result = cb.args[0][1]; // First call, second argument (error, queryResult) + result.should.deep.equal([{ + $class: 'org.acme.base.BaseAsset', + theValue: 'my value' + }]); + }); + }); + + it('should throw when executing a query that does not exist', () => { + return new Promise((resolve, reject) => { + testConnector.executeQuery( 'missing', { param1: 'false' }, {test: 'options' }, (error, result) => { + if (error) { + return reject(error); + } + resolve(result); + }); + }).should.be.rejectedWith(/Named query missing does not exist in the business network./); + }); + + }); + + describe('#discoverQueries', () => { + + beforeEach(() => { + // create mock query file + mockQueryFile = sinon.createStubInstance(QueryFile); + mockQueryFile.getModelManager.returns(modelManager); + + // create real query + const stringQuery = Query.buildQuery(mockQueryFile, 'stringQuery', 'test query', + 'SELECT org.acme.base.BaseAsset WHERE (theValue==_$param1)'); + + // // create mocks + mockQueryManager = sinon.createStubInstance(QueryManager); + mockQueryManager.getQueries.returns([stringQuery]); + + // // setup mocks + mockBusinessNetworkDefinition.getQueryManager.returns(mockQueryManager); + + sinon.stub(testConnector, 'ensureConnected').resolves(mockBusinessNetworkConnection); + testConnector.connected = true; + testConnector.serializer = mockSerializer; + testConnector.businessNetworkDefinition = mockBusinessNetworkDefinition; + }); + + it('should return the queries in the business network definition', () => { + + const cb = sinon.stub(); + return testConnector.discoverQueries( {test: 'options' }, cb) + .then(( queries ) => { + sinon.assert.calledOnce(testConnector.ensureConnected); + sinon.assert.calledWith(testConnector.ensureConnected, { test: 'options' }); + + const result = cb.args[0][1]; // First call, second argument (error, queries) + result.length.should.equal(1); + result[0].getName().should.equal('stringQuery'); + }); + }); + + it('should throw when getQueries fails', () => { + + mockQueryManager.getQueries.throws(); + + return new Promise((resolve, reject) => { + testConnector.discoverQueries( {test: 'options' }, (error, result) => { + if (error) { + return reject(error); + } + resolve(result); + }); + }).should.be.rejectedWith(); + }); + + }); + describe('#getTransactionByID', () => { let mockTransactionRegistry; @@ -2225,15 +2432,19 @@ describe('BusinessNetworkConnector', () => { sinon.assert.calledOnce(introspector.getClassDeclarations); result.should.deep.equal([{ type: 'table', + namespaces: true, name: 'org.acme.base.BaseConcept' }, { type: 'table', + namespaces: true, name: 'org.acme.base.BaseAsset' }, { type: 'table', + namespaces: true, name: 'org.acme.base.BaseParticipant' }, { type: 'table', + namespaces: true, name: 'org.acme.base.BaseTransaction' }]); }); @@ -2255,15 +2466,19 @@ describe('BusinessNetworkConnector', () => { sinon.assert.calledOnce(introspector.getClassDeclarations); result.should.deep.equal([{ type: 'table', + namespaces: false, name: 'BaseConcept' }, { type: 'table', + namespaces: false, name: 'BaseAsset' }, { type: 'table', + namespaces: false, name: 'BaseParticipant' }, { type: 'table', + namespaces: false, name: 'BaseTransaction' }]); }); @@ -2289,18 +2504,23 @@ describe('BusinessNetworkConnector', () => { sinon.assert.calledOnce(introspector.getClassDeclarations); result.should.deep.equal([{ type: 'table', + namespaces: true, name: 'org.acme.base.BaseConcept' }, { type: 'table', + namespaces: true, name: 'org.acme.base.BaseAsset' }, { type: 'table', + namespaces: true, name: 'org.acme.base.BaseParticipant' }, { type: 'table', + namespaces: true, name: 'org.acme.base.BaseTransaction' }, { type: 'table', + namespaces: true, name: 'org.acme.extra.BaseAsset' }]); }); @@ -2322,15 +2542,19 @@ describe('BusinessNetworkConnector', () => { sinon.assert.calledOnce(introspector.getClassDeclarations); result.should.deep.equal([{ type: 'table', + namespaces: false, name: 'BaseConcept' }, { type: 'table', + namespaces: false, name: 'BaseAsset' }, { type: 'table', + namespaces: false, name: 'BaseParticipant' }, { type: 'table', + namespaces: false, name: 'BaseTransaction' }]); }); @@ -2410,10 +2634,30 @@ describe('BusinessNetworkConnector', () => { 'type': 'string' }, 'theValue' : { - 'description' : 'The instance identifier for this type', - 'id' : true, + 'description': 'The instance identifier for this type', + 'id': true, 'required' : true, 'type' : 'string' + }, + 'theInteger' : { + 'required' : false, + 'type' : 'number' + }, + 'theDouble' : { + 'required' : false, + 'type' : 'number' + }, + 'theLong' : { + 'required' : false, + 'type' : 'number' + }, + 'theDateTime' : { + 'required' : false, + 'type' : 'date' + }, + 'theBoolean' : { + 'required' : false, + 'type' : 'boolean' } }, 'relations' : {}, diff --git a/packages/loopback-connector-composer/test/data/bond-network/README.md b/packages/loopback-connector-composer/test/data/bond-network/README.md new file mode 100644 index 0000000000..d3852a90a5 --- /dev/null +++ b/packages/loopback-connector-composer/test/data/bond-network/README.md @@ -0,0 +1,8 @@ +# Hyperledger Composer Bond Reference Data Demo + +Example business network that stores information about financial bonds on the blockchain. It allows +the issuer of a bond to update the bond information whilst other members of the business network can +only read the bond data. + +The data model for a bond is based on the FpML schema: +http://www.fpml.org/spec/fpml-5-3-2-wd-2/html/reporting/schemaDocumentation/schemas/fpml-asset-5-3_xsd/elements/bond.html diff --git a/packages/loopback-connector-composer/test/data/bond-network/lib/logic.js b/packages/loopback-connector-composer/test/data/bond-network/lib/logic.js new file mode 100644 index 0000000000..fb49403093 --- /dev/null +++ b/packages/loopback-connector-composer/test/data/bond-network/lib/logic.js @@ -0,0 +1,41 @@ +/* + * 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. + */ + +'use strict'; + +/*eslint-disable no-unused-vars*/ +/*eslint-disable no-undef*/ +/*eslint-disable no-var*/ + +/** + * Publish a new bond + * @param {org.acme.bond.PublishBond} publishBond - the publishBond transaction + * @transaction + * @return {Promise} a promise when completed + */ +function publish(publishBond) { + + return getAssetRegistry('org.acme.bond.BondAsset') + .then(function (registry) { + var factory = getFactory(); + // Create the bond asset. + var bondAsset = factory.newResource('org.acme.bond', 'BondAsset', publishBond.ISINCode); + bondAsset.bond = publishBond.bond; + // Add the bond asset to the registry. + return registry.add(bondAsset); + }); +} + +/*eslint-enable no-unused-vars*/ +/*eslint-enable no-undef*/ \ No newline at end of file diff --git a/packages/loopback-connector-composer/test/data/bond-network/models/bond.cto b/packages/loopback-connector-composer/test/data/bond-network/models/bond.cto new file mode 100644 index 0000000000..ed3140ebb8 --- /dev/null +++ b/packages/loopback-connector-composer/test/data/bond-network/models/bond.cto @@ -0,0 +1,59 @@ +/** + * Definition of a Bond, based on the FpML schema: + * http://www.fpml.org/spec/fpml-5-3-2-wd-2/html/reporting/schemaDocumentation/schemas/fpml-asset-5-3_xsd/elements/bond.html + * + */ +namespace org.acme.bond + +enum CouponType { + o FIXED + o FLOATING +} + +participant Member identified by memberId { + o String memberId + o String name +} + +participant Issuer extends Member { +} + +enum PeriodEnum { + o DAY + o WEEK + o MONTH + o YEAR +} + +concept PaymentFrequency { + o Integer periodMultiplier + o PeriodEnum period +} + +concept Bond { + o String[] instrumentId + o String description optional + o String currency optional + o String[] exchangeId + o String clearanceSystem optional + o String definition optional + o String seniority optional + o CouponType couponType optional + o Double couponRate optional + o DateTime maturity + o Double parValue + o Double faceAmount + o PaymentFrequency paymentFrequency + o String dayCountFraction + --> Issuer issuer +} + +asset BondAsset identified by ISINCode { + o String ISINCode + o Bond bond +} + +transaction PublishBond { + o String ISINCode + o Bond bond +} \ No newline at end of file diff --git a/packages/loopback-connector-composer/test/data/bond-network/package.json b/packages/loopback-connector-composer/test/data/bond-network/package.json new file mode 100644 index 0000000000..d5f6c27681 --- /dev/null +++ b/packages/loopback-connector-composer/test/data/bond-network/package.json @@ -0,0 +1 @@ +{"name":"bond-network","version":"0.1.0","description":"Bond Reference Data Sharing Business Network","scripts":{"prepublish":"mkdirp ./dist && composer archive create --sourceType dir --sourceName . -a ./dist/bond-network.bna","pretest":"npm run lint","lint":"eslint .","postlint":"npm run licchk","licchk":"license-check","postlicchk":"npm run doc","doc":"jsdoc --pedantic --recurse -c jsdoc.conf","test":"mocha -t 0 --recursive","deploy":"./scripts/deploy.sh"},"repository":{"type":"git","url":"https://github.com/hyperledger/composer-sample-networks.git"},"keywords":["bonds","reference data","finance"],"author":"Hyperledger Composer","license":"Apache-2.0","devDependencies":{"browserfs":"^1.2.0","chai":"^3.5.0","composer-admin":"latest","composer-cli":"latest","composer-client":"latest","composer-connector-embedded":"latest","eslint":"^3.6.1","istanbul":"^0.4.5","jsdoc":"^3.4.1","license-check":"^1.1.5","mkdirp":"^0.5.1","mocha":"^3.2.0","moment":"^2.17.1"},"license-check-config":{"src":["**/*.js","!./coverage/**/*","!./node_modules/**/*","!./out/**/*","!./scripts/**/*"],"path":"header.txt","blocking":true,"logInfo":false,"logError":true}} \ No newline at end of file diff --git a/packages/loopback-connector-composer/test/data/bond-network/permissions.acl b/packages/loopback-connector-composer/test/data/bond-network/permissions.acl new file mode 100644 index 0000000000..9b9221c36a --- /dev/null +++ b/packages/loopback-connector-composer/test/data/bond-network/permissions.acl @@ -0,0 +1,19 @@ +/** + * Access Control List for the bond data + */ +rule Issuer { + description: "Allow full access to the issuer of a bond" + participant(i): "org.acme.bond.Issuer" + operation: ALL + resource(a): "org.acme.bond.BondAsset" + condition: (a.bond.issuer.memberId === i.memberId) + action: ALLOW +} + +rule Default { + description: "Allow read access" + participant: "org.acme.bond.*" + operation: ALL + resource: "org.acme.bond.*" + action: ALLOW +} \ No newline at end of file diff --git a/packages/loopback-connector-composer/test/data/bond-network/queries.qry b/packages/loopback-connector-composer/test/data/bond-network/queries.qry new file mode 100644 index 0000000000..50c197dfd9 --- /dev/null +++ b/packages/loopback-connector-composer/test/data/bond-network/queries.qry @@ -0,0 +1,4 @@ +query findBondByFaceAmount { + description: "Find all bonds with a face amount greater than _$faceAmount" + statement: SELECT org.acme.bond.BondAsset WHERE (bond.faceAmount > _$faceAmount) +} diff --git a/packages/loopback-connector-composer/test/participants.js b/packages/loopback-connector-composer/test/participants.js index 72d46bb54b..3355cae6b9 100644 --- a/packages/loopback-connector-composer/test/participants.js +++ b/packages/loopback-connector-composer/test/participants.js @@ -19,9 +19,7 @@ const BrowserFS = require('browserfs/dist/node/index'); const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; const connector = require('..'); -const fs = require('fs'); const loopback = require('loopback'); -const path = require('path'); const Util = require('composer-common').Util; const chai = require('chai'); @@ -70,8 +68,7 @@ const bfs_fs = BrowserFS.BFSRequire('fs'); return adminConnection.connect('defaultProfile', 'admin', 'Xurw3yU9zI0l'); }) .then(() => { - const banana = fs.readFileSync(path.resolve(__dirname, 'bond-network.bna')); - return BusinessNetworkDefinition.fromArchive(banana); + return BusinessNetworkDefinition.fromDirectory('./test/data/bond-network'); }) .then((businessNetworkDefinition) => { serializer = businessNetworkDefinition.getSerializer();