From 5905159bf7cbc9bb13e0e3478f976c6661c0caae Mon Sep 17 00:00:00 2001 From: xufengli Date: Mon, 10 Jul 2017 15:05:09 +0100 Subject: [PATCH 01/17] feature-921 expose REST queries --- .../server/boot/composer-discovery.js | 122 ++++++++++++++++++ .../lib/businessnetworkconnector.js | 66 ++++++++++ 2 files changed, 188 insertions(+) diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index a9207307f4..7d4ae40395 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -110,6 +110,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: 'Rich Query Methods', + plural: '/query', + 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. @@ -135,6 +162,95 @@ function registerSystemMethods(app, dataSource) { } +/** + * Register all of the Composer query methods. + * @param {Object} app The LoopBack application. + * @param {Object} dataSource The LoopBack data source. + */ +function registerQueryMethods(app, dataSource) { + + // Grab the query model. + const Query = app.models.Query; + const connector = dataSource.connector; + + // Register all query methods + const registerMethods = [ + registerGetAllRedVehiclesMethod, + registerGetAllActiveVehiclesMethod + + ]; + registerMethods.forEach((registerMethod) => { + registerMethod(app, dataSource, Query, connector); + }); + +} + +/** + * Register the 'getAllRedVehicles' Composer query method. + * @param {Object} app The LoopBack application. + * @param {Object} dataSource The LoopBack data source. + * @param {Object} Query The Query model class. + * @param {Object} connector The LoopBack connector. + */ +function registerGetAllRedVehiclesMethod(app, dataSource, Query, connector) { + + // Define and register the method. + Query.getAllRedVehicles = (options, callback) => { + connector.getAllRedVehicles(options, callback); + }; + Query.remoteMethod( + 'getAllRedVehicles', { + description: 'Get all red vehicles from the asset registry', + accepts: [{ + arg: 'options', + type: 'object', + http: 'optionsFromRequest' + }], + returns: { + type: [ 'object' ], + root: true + }, + http: { + verb: 'get', + path: '/getAllRedVehicles' + } + } + ); +} + +/** + * Register the 'getAllRedVehicles' Composer query method. + * @param {Object} app The LoopBack application. + * @param {Object} dataSource The LoopBack data source. + * @param {Object} Query The Query model class. + * @param {Object} connector The LoopBack connector. + */ +function registerGetAllActiveVehiclesMethod(app, dataSource, Query, connector) { + + // Define and register the method. + Query.getAllActiveVehicles = (options, callback) => { + connector.getAllActiveVehicles(options, callback); + }; + Query.remoteMethod( + 'getAllActiveVehicles', { + description: 'Get all active vehicles from the asset registry', + accepts: [{ + arg: 'options', + type: 'object', + http: 'optionsFromRequest' + }], + returns: { + type: [ 'object' ], + root: true + }, + http: { + verb: 'get', + path: '/getAllActiveVehicles' + } + } + ); +} + /** * Register the 'ping' Composer system method. * @param {Object} app The LoopBack application. @@ -557,6 +673,12 @@ module.exports = function (app, callback) { // Register the system methods. registerSystemMethods(app, dataSource); + // Create the query model. + createQueryModel(app, dataSource); + + // Register the query methods. + registerQueryMethods(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 ...'); diff --git a/packages/loopback-connector-composer/lib/businessnetworkconnector.js b/packages/loopback-connector-composer/lib/businessnetworkconnector.js index b4b6c7e22f..f2f2ab06e3 100644 --- a/packages/loopback-connector-composer/lib/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/lib/businessnetworkconnector.js @@ -820,6 +820,72 @@ class BusinessNetworkConnector extends Connector { }); } + /** + * Get all of the assets from the asset registry. + * @param {Object} options The LoopBack options. + * @param {function} callback The callback to call when complete. + * @returns {Promise} A promise that is resolved when complete. + */ + getAllRedVehicles(options, callback) { + debug('getAllRedVehicles', options); + let actualOptions = null, actualCallback = null; + if (arguments.length === 1) { + // LoopBack API, called with (callback). + actualCallback = options; + } else { + // Composer API, called with (options, callback). + actualOptions = options; + actualCallback = callback; + } + debug('getAllRedVehicles', actualOptions); + return this.ensureConnected(actualOptions) + .then((businessNetworkConnection) => { + return businessNetworkConnection.query('selectAllRedVehicles'); + }) + .then((result) => { + actualCallback(null, result); + }) + .catch((error) => { + debug('getAllRedVehicles', 'error thrown doing query', error); + actualCallback(error); + }); + } + + /** + * Get all of the assets from the asset registry. + * @param {Object} options The LoopBack options. + * @param {function} callback The callback to call when complete. + * @returns {Promise} A promise that is resolved when complete. + */ + getAllActiveVehicles(options, callback) { + debug('getAllActiveVehicles', options); + let actualOptions = null, actualCallback = null; + if (arguments.length === 1) { + // LoopBack API, called with (callback). + actualCallback = options; + } else { + // Composer API, called with (options, callback). + actualOptions = options; + actualCallback = callback; + } + debug('getAllRedVehicles', actualOptions); + return this.ensureConnected(actualOptions) + .then((businessNetworkConnection) => { + return businessNetworkConnection.query('selectAllActiveVehicles'); + }) + .then((queryResult) => { + const result = queryResult.map((item) => { + return this.serializer.toJSON(item); + }); + actualCallback(null, result); + }) + .catch((error) => { + console.log(error); + debug('getAllActiveVehicles', 'error thrown doing query', error); + actualCallback(error); + }); + } + /** * Get the transaction with the specified ID from the transaction registry. * @param {string} id The ID for the transaction. From 4d0dd2480714c6a0b825163a622666caaa75cb21 Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Mon, 10 Jul 2017 16:43:39 +0100 Subject: [PATCH 02/17] work in progress --- .../server/boot/composer-discovery.js | 70 ++++--------- .../lib/businessnetworkconnector.js | 99 +++++++++++-------- 2 files changed, 77 insertions(+), 92 deletions(-) diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index 7d4ae40395..5991d0e968 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -120,8 +120,8 @@ function createQueryModel(app, dataSource) { // Create the query model schema. let modelSchema = { name: 'Query', - description: 'Rich Query Methods', - plural: '/query', + description: 'Content-based Query Methods', + plural: '/queries', base: 'Model' }; modelSchema = updateModelSchema(modelSchema); @@ -166,6 +166,7 @@ function registerSystemMethods(app, dataSource) { * Register all of the Composer query methods. * @param {Object} app The LoopBack application. * @param {Object} dataSource The LoopBack data source. + * @returns {Promise} a promise when complete */ function registerQueryMethods(app, dataSource) { @@ -173,49 +174,19 @@ function registerQueryMethods(app, dataSource) { const Query = app.models.Query; const connector = dataSource.connector; - // Register all query methods - const registerMethods = [ - registerGetAllRedVehiclesMethod, - registerGetAllActiveVehiclesMethod - - ]; - registerMethods.forEach((registerMethod) => { - registerMethod(app, dataSource, Query, connector); - }); - -} + return new Promise((resolve, reject) => { + connector.discoverQueries(null, (error, queries) => { + if (error) { + return reject(error); + } -/** - * Register the 'getAllRedVehicles' Composer query method. - * @param {Object} app The LoopBack application. - * @param {Object} dataSource The LoopBack data source. - * @param {Object} Query The Query model class. - * @param {Object} connector The LoopBack connector. - */ -function registerGetAllRedVehiclesMethod(app, dataSource, Query, connector) { + queries.forEach((query) => { + registerQueryMethod(app, dataSource, Query, connector, query); + }); - // Define and register the method. - Query.getAllRedVehicles = (options, callback) => { - connector.getAllRedVehicles(options, callback); - }; - Query.remoteMethod( - 'getAllRedVehicles', { - description: 'Get all red vehicles from the asset registry', - accepts: [{ - arg: 'options', - type: 'object', - http: 'optionsFromRequest' - }], - returns: { - type: [ 'object' ], - root: true - }, - http: { - verb: 'get', - path: '/getAllRedVehicles' - } - } - ); + resolve(queries); + }); + }); } /** @@ -224,16 +195,17 @@ function registerGetAllRedVehiclesMethod(app, dataSource, Query, connector) { * @param {Object} dataSource The LoopBack data source. * @param {Object} Query The Query model class. * @param {Object} connector The LoopBack connector. + * @param {Query} query the query instance */ -function registerGetAllActiveVehiclesMethod(app, dataSource, Query, connector) { +function registerQueryMethod(app, dataSource, Query, connector, query) { // Define and register the method. - Query.getAllActiveVehicles = (options, callback) => { - connector.getAllActiveVehicles(options, callback); + Query.executeQuery = (options, callback) => { + connector.executeQuery(options, callback); }; Query.remoteMethod( - 'getAllActiveVehicles', { - description: 'Get all active vehicles from the asset registry', + query.getName(), { + description: query.getDescription(), accepts: [{ arg: 'options', type: 'object', @@ -245,7 +217,7 @@ function registerGetAllActiveVehiclesMethod(app, dataSource, Query, connector) { }, http: { verb: 'get', - path: '/getAllActiveVehicles' + path: query.getName() } } ); diff --git a/packages/loopback-connector-composer/lib/businessnetworkconnector.js b/packages/loopback-connector-composer/lib/businessnetworkconnector.js index f2f2ab06e3..2ebeadc5a8 100644 --- a/packages/loopback-connector-composer/lib/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/lib/businessnetworkconnector.js @@ -821,13 +821,13 @@ class BusinessNetworkConnector extends Connector { } /** - * Get all of the assets from the asset registry. + * Execute a named query and returns the results * @param {Object} options The LoopBack options. * @param {function} callback The callback to call when complete. * @returns {Promise} A promise that is resolved when complete. */ - getAllRedVehicles(options, callback) { - debug('getAllRedVehicles', options); + executeQuery(options, callback) { + debug('executeQuery', options); let actualOptions = null, actualCallback = null; if (arguments.length === 1) { // LoopBack API, called with (callback). @@ -837,41 +837,11 @@ class BusinessNetworkConnector extends Connector { actualOptions = options; actualCallback = callback; } - debug('getAllRedVehicles', actualOptions); + debug('executeQuery', actualOptions); return this.ensureConnected(actualOptions) .then((businessNetworkConnection) => { - return businessNetworkConnection.query('selectAllRedVehicles'); - }) - .then((result) => { - actualCallback(null, result); - }) - .catch((error) => { - debug('getAllRedVehicles', 'error thrown doing query', error); - actualCallback(error); - }); - } - - /** - * Get all of the assets from the asset registry. - * @param {Object} options The LoopBack options. - * @param {function} callback The callback to call when complete. - * @returns {Promise} A promise that is resolved when complete. - */ - getAllActiveVehicles(options, callback) { - debug('getAllActiveVehicles', options); - let actualOptions = null, actualCallback = null; - if (arguments.length === 1) { - // LoopBack API, called with (callback). - actualCallback = options; - } else { - // Composer API, called with (options, callback). - actualOptions = options; - actualCallback = callback; - } - debug('getAllRedVehicles', actualOptions); - return this.ensureConnected(actualOptions) - .then((businessNetworkConnection) => { - return businessNetworkConnection.query('selectAllActiveVehicles'); + console.log( '***** executeQuery with options: ' + JSON.stringify(options) ); + return businessNetworkConnection.query(options.query); }) .then((queryResult) => { const result = queryResult.map((item) => { @@ -881,7 +851,7 @@ class BusinessNetworkConnector extends Connector { }) .catch((error) => { console.log(error); - debug('getAllActiveVehicles', 'error thrown doing query', error); + debug('executeQuery', 'error thrown doing query', error); actualCallback(error); }); } @@ -993,6 +963,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. @@ -1024,18 +1013,42 @@ class BusinessNetworkConnector extends Connector { } } - // If we didn't find it, throw! + // Then look for the item as a query + let query = null; if (!classDeclaration) { + let matchingQuery = this.businessNetworkDefinition.getQueryManager().getQueries() + .filter((query) => { + return query.getName() === object; + }); + if (matchingQuery.length > 1) { + throw new Error(`Found multiple queries for ${object}`); + } else if (matchingQuery.length === 1) { + query = matchingQuery[0]; + } + } + + // If we didn't find it, throw! + if (!classDeclaration && !query) { throw new Error(`Failed to find type definition for ${object}`); } // Generate a LoopBack schema for the type. - let schema = classDeclaration.accept(this.visitor, { - first : true, - modelFile : classDeclaration.getModelFile() - }); - callback(null, schema); + let schema = null; + if(classDeclaration) { + schema = classDeclaration.accept(this.visitor, { + first : true, + modelFile : classDeclaration.getModelFile() + }); + } + else { + schema = query.accept(this.visitor, { + first : true, + modelFile : classDeclaration.getModelFile() + }); + } + + callback(null, schema); }) .catch((error) => { debug('discoverSchemas', 'error thrown generating schema', error); From 3b7626db126523eb09f8dbf581eb911a7f05be28 Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Mon, 10 Jul 2017 18:49:00 +0100 Subject: [PATCH 03/17] work in progress --- packages/composer-common/index.js | 1 + .../lib/query/queryanalyzer.js | 333 ++++++++++++++++++ .../server/boot/composer-discovery.js | 28 +- .../lib/businessnetworkconnector.js | 34 +- 4 files changed, 359 insertions(+), 37 deletions(-) create mode 100644 packages/composer-common/lib/query/queryanalyzer.js diff --git a/packages/composer-common/index.js b/packages/composer-common/index.js index 6c0103a92b..bb0242168c 100644 --- a/packages/composer-common/index.js +++ b/packages/composer-common/index.js @@ -82,6 +82,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/lib/query/queryanalyzer.js b/packages/composer-common/lib/query/queryanalyzer.js new file mode 100644 index 0000000000..e3afcdb32e --- /dev/null +++ b/packages/composer-common/lib/query/queryanalyzer.js @@ -0,0 +1,333 @@ +/* + * 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 LOG = Logger.getLog('QueryAnalyzer'); + +/** + * The query analyzer visits a query and extracts the names and types of all parameters + * @protected + */ +class QueryAnalyzer { + + /** + * Extract the names and types of query parameters + * @param {QueryManager} query The query to process. + * @return {object[]} The names and types of the query parameters + */ + analyze(query) { + const method = 'analyze'; + LOG.entry(method, query); + const result = 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 { + 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 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); + 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); + 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' ]; + const conditionOperators = [ '<', '<=', '>', '>=', '==', '!=' ]; + let result; + if (arrayCombinationOperators.indexOf(ast.operator) !== -1) { + result = this.visitArrayCombinationOperator(ast, parameters); + } else if (conditionOperators.indexOf(ast.operator) !== -1) { + result = this.visitConditionOperator(ast, parameters); + } else { + throw new Error('The query compiler does not support this binary expression'); + } + + 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 = []; + const lhs = this.visit(ast.left, parameters); + console.log('Result of lhs: ' + lhs ); + result.concat(lhs); + + const rhs = this.visit(ast.right, parameters); + console.log('Result of rhs: ' + rhs ); + result = result.concat(rhs); + + console.log('***** result of conditional operator ' + result); + + 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); + const result = []; + + console.log('**** visiting ' + JSON.stringify(ast)); + + // Check to see if this is a parameter reference. + const parameterMatch = ast.name.match(/^_\$(.+)/); + if (parameterMatch) { + const parameterName = parameterMatch[1]; + // TODO - figure out the type of the parameter by looking + // at the type of the LHS or RHS if there is one + result.push({name: parameterName, type : 'String'}); + } + + console.log('**** visitIdentifier result ' + result); + + 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; + } +} + +module.exports = QueryAnalyzer; diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index 5991d0e968..11cd23fb1c 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -16,6 +16,7 @@ const connector = require('loopback-connector-composer'); const LoopBackWallet = require('../../lib/loopbackwallet'); +const QueryAnalyzer = require('composer-common').QueryAnalyzer; /** * Find or create the system wallet for storing identities in. @@ -181,6 +182,7 @@ function registerQueryMethods(app, dataSource) { } queries.forEach((query) => { + console.log('Registering query: ' + query.getName() ); registerQueryMethod(app, dataSource, Query, connector, query); }); @@ -199,25 +201,35 @@ function registerQueryMethods(app, dataSource) { */ function registerQueryMethod(app, dataSource, Query, connector, query) { + const analyzer = new QueryAnalyzer(); + const parameters = analyzer.analyze(query); + + let accepts = []; + + for(let n=0; n < parameters.length; n++) { + const param = parameters[n]; + accepts.push( {arg: param.name, type: param.type, required: true, http: 'optionsFromRequest' } ); + } + + console.log( '**** PARAM FOR QUERY ' + query.getName() + '=' + JSON.stringify(accepts) ); + // Define and register the method. - Query.executeQuery = (options, callback) => { + Query[query.getName()] = (options, callback) => { + console.log('**** options: ' + JSON.stringify(options)); + options.query = query.getName(); connector.executeQuery(options, callback); }; Query.remoteMethod( query.getName(), { description: query.getDescription(), - accepts: [{ - arg: 'options', - type: 'object', - http: 'optionsFromRequest' - }], + accepts: accepts, returns: { - type: [ 'object' ], + type: [ query.getSelect().getResource() ], root: true }, http: { verb: 'get', - path: query.getName() + path: '/' + query.getName() } } ); diff --git a/packages/loopback-connector-composer/lib/businessnetworkconnector.js b/packages/loopback-connector-composer/lib/businessnetworkconnector.js index 2ebeadc5a8..a2bd2eab19 100644 --- a/packages/loopback-connector-composer/lib/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/lib/businessnetworkconnector.js @@ -1013,40 +1013,16 @@ class BusinessNetworkConnector extends Connector { } } - // Then look for the item as a query - let query = null; - if (!classDeclaration) { - let matchingQuery = this.businessNetworkDefinition.getQueryManager().getQueries() - .filter((query) => { - return query.getName() === object; - }); - if (matchingQuery.length > 1) { - throw new Error(`Found multiple queries for ${object}`); - } else if (matchingQuery.length === 1) { - query = matchingQuery[0]; - } - } - // If we didn't find it, throw! - if (!classDeclaration && !query) { + if (!classDeclaration) { throw new Error(`Failed to find type definition for ${object}`); } // Generate a LoopBack schema for the type. - let schema = null; - - if(classDeclaration) { - schema = classDeclaration.accept(this.visitor, { - first : true, - modelFile : classDeclaration.getModelFile() - }); - } - else { - schema = query.accept(this.visitor, { - first : true, - modelFile : classDeclaration.getModelFile() - }); - } + let schema = classDeclaration.accept(this.visitor, { + first : true, + modelFile : classDeclaration.getModelFile() + }); callback(null, schema); }) From c6703af60ff50e0d3eb1950a1c2537fc52382b8c Mon Sep 17 00:00:00 2001 From: xufengli Date: Mon, 10 Jul 2017 23:21:56 +0100 Subject: [PATCH 04/17] working progress to add parameters in REST APIs --- .../server/boot/composer-discovery.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index 11cd23fb1c..36db55e009 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -205,11 +205,16 @@ function registerQueryMethod(app, dataSource, Query, connector, query) { const parameters = analyzer.analyze(query); let accepts = []; + let pathWithPrameters = '/' + query.getName(); for(let n=0; n < parameters.length; n++) { const param = parameters[n]; - accepts.push( {arg: param.name, type: param.type, required: true, http: 'optionsFromRequest' } ); + + // accepts.push( {arg: param.name, type: param.type, required: true, http: 'optionsFromRequest' } ); + accepts.push( {arg: param.name, type: param.type, required: true, http: {source :'path'} } ); + pathWithPrameters = pathWithPrameters + '/:' + param.name; } + accepts.push({arg: 'options', type: 'object', http: 'optionsFromRequest' }); console.log( '**** PARAM FOR QUERY ' + query.getName() + '=' + JSON.stringify(accepts) ); @@ -229,7 +234,8 @@ function registerQueryMethod(app, dataSource, Query, connector, query) { }, http: { verb: 'get', - path: '/' + query.getName() + // path: '/' + query.getName(); + path: pathWithPrameters } } ); From 162bf1777ff5188374c00fa66a589e6adefab6b4 Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Mon, 10 Jul 2017 23:24:25 +0100 Subject: [PATCH 05/17] Queries exposed as REST methods using query params --- .../server/boot/composer-discovery.js | 41 +++++++++++++------ .../lib/businessnetworkconnector.js | 27 +++++------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index 11cd23fb1c..5db056cb6d 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -17,6 +17,8 @@ const connector = require('loopback-connector-composer'); const LoopBackWallet = require('../../lib/loopbackwallet'); const QueryAnalyzer = require('composer-common').QueryAnalyzer; +const ModelUtil = require('composer-common').ModelUtil; +const util = require('util'); /** * Find or create the system wallet for storing identities in. @@ -182,7 +184,6 @@ function registerQueryMethods(app, dataSource) { } queries.forEach((query) => { - console.log('Registering query: ' + query.getName() ); registerQueryMethod(app, dataSource, Query, connector, query); }); @@ -192,39 +193,53 @@ function registerQueryMethods(app, dataSource) { } /** - * Register the 'getAllRedVehicles' Composer query method. + * Register a composer named query method. * @param {Object} app The LoopBack application. * @param {Object} dataSource The LoopBack data source. - * @param {Object} Query The Query model class. + * @param {Object} Query The LoopBack Query model * @param {Object} connector The LoopBack connector. - * @param {Query} query the query instance + * @param {Query} query the named Composer query to register */ function registerQueryMethod(app, dataSource, Query, connector, query) { const analyzer = new QueryAnalyzer(); const parameters = analyzer.analyze(query); + const returnType = dataSource.settings.namespace + ? 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: param.type, required: true, http: 'optionsFromRequest' } ); + accepts.push( {arg: param.name, type: param.type, required: true, http: {verb : 'get', source: 'query'}} ); } - console.log( '**** PARAM FOR QUERY ' + query.getName() + '=' + JSON.stringify(accepts) ); - - // Define and register the method. - Query[query.getName()] = (options, callback) => { - console.log('**** options: ' + JSON.stringify(options)); - options.query = query.getName(); - connector.executeQuery(options, callback); + // Define and register dynamic query method + 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: [ query.getSelect().getResource() ], + type : [ returnType ], root: true }, http: { diff --git a/packages/loopback-connector-composer/lib/businessnetworkconnector.js b/packages/loopback-connector-composer/lib/businessnetworkconnector.js index a2bd2eab19..c471838668 100644 --- a/packages/loopback-connector-composer/lib/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/lib/businessnetworkconnector.js @@ -24,6 +24,7 @@ const LoopbackVisitor = require('composer-common').LoopbackVisitor; const NodeCache = require('node-cache'); const ParticipantDeclaration = require('composer-common').ParticipantDeclaration; const TransactionDeclaration = require('composer-common').TransactionDeclaration; +const util = require('util'); /** * A Loopback connector for exposing the Blockchain Solution Framework to Loopback enabled applications. @@ -822,37 +823,31 @@ 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(options, callback) { + executeQuery( queryName, queryParameters, options, callback) { debug('executeQuery', options); - let actualOptions = null, actualCallback = null; - if (arguments.length === 1) { - // LoopBack API, called with (callback). - actualCallback = options; - } else { - // Composer API, called with (options, callback). - actualOptions = options; - actualCallback = callback; - } - debug('executeQuery', actualOptions); - return this.ensureConnected(actualOptions) + debug('queryName', queryName); + debug('queryParameters', util.inspect(queryParameters)); + + return this.ensureConnected(options) .then((businessNetworkConnection) => { - console.log( '***** executeQuery with options: ' + JSON.stringify(options) ); - return businessNetworkConnection.query(options.query); + return businessNetworkConnection.query(queryName, queryParameters); }) .then((queryResult) => { const result = queryResult.map((item) => { return this.serializer.toJSON(item); }); - actualCallback(null, result); + callback(null, result); }) .catch((error) => { console.log(error); debug('executeQuery', 'error thrown doing query', error); - actualCallback(error); + callback(error); }); } From 503c3cfd06dc5388caef2b7aafdc3531b3a8b0db Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Mon, 10 Jul 2017 23:29:09 +0100 Subject: [PATCH 06/17] wip --- .../server/boot/composer-discovery.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index d1c58b5f2f..5db056cb6d 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -219,14 +219,8 @@ function registerQueryMethod(app, dataSource, Query, connector, query) { // 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: param.type, required: true, http: {verb : 'get', source: 'query'}} ); - accepts.push( {arg: param.name, type: param.type, required: true, http: {source :'path'} } ); - pathWithPrameters = pathWithPrameters + '/:' + param.name; } - accepts.push({arg: 'options', type: 'object', http: 'optionsFromRequest' }); - - console.log( '**** PARAM FOR QUERY ' + query.getName() + '=' + JSON.stringify(accepts) ); // Define and register dynamic query method const queryMethod = { @@ -250,8 +244,7 @@ function registerQueryMethod(app, dataSource, Query, connector, query) { }, http: { verb: 'get', - // path: '/' + query.getName(); - path: pathWithPrameters + path: '/' + query.getName() } } ); From aa40ddd5bdc85a6de507a43fcd9a670b27d67d18 Mon Sep 17 00:00:00 2001 From: xufengli Date: Tue, 11 Jul 2017 16:45:03 +0100 Subject: [PATCH 07/17] add MemberExpression --- packages/composer-common/api.txt | 3 +++ .../lib/query/queryanalyzer.js | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/packages/composer-common/api.txt b/packages/composer-common/api.txt index 11e9c4b445..efad8b3341 100644 --- a/packages/composer-common/api.txt +++ b/packages/composer-common/api.txt @@ -102,6 +102,9 @@ class ValidatedResource extends Resource { + void addArrayValue(string,string) throws Error + void validate() throws Error } +class QueryAnalyzer { + + object[] analyze(QueryManager) +} class SecurityContext { + void constructor(Connection,string) + Connection getConnection() diff --git a/packages/composer-common/lib/query/queryanalyzer.js b/packages/composer-common/lib/query/queryanalyzer.js index e3afcdb32e..ea92a5ba26 100644 --- a/packages/composer-common/lib/query/queryanalyzer.js +++ b/packages/composer-common/lib/query/queryanalyzer.js @@ -71,6 +71,8 @@ class QueryAnalyzer { 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)); } @@ -328,6 +330,29 @@ class QueryAnalyzer { 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 result = []; + + const property = this.visit(ast.property, parameters); + result.concat(property); + + const object = this.visit(ast.object, parameters); + + result.concat(object); + // const selector = `${object}.${property}`; + LOG.exit(method, result); + return result; + } } module.exports = QueryAnalyzer; From 5ac67804058470efe1fb05fc33dee98d75d34b71 Mon Sep 17 00:00:00 2001 From: xufengli Date: Tue, 18 Jul 2017 11:47:06 +0100 Subject: [PATCH 08/17] feature-921 --- .../lib/query/queryanalyzer.js | 142 ++++++++++++++---- .../server/boot/composer-discovery.js | 8 +- .../lib/businessnetworkconnector.js | 4 + 3 files changed, 120 insertions(+), 34 deletions(-) diff --git a/packages/composer-common/lib/query/queryanalyzer.js b/packages/composer-common/lib/query/queryanalyzer.js index ea92a5ba26..c479391269 100644 --- a/packages/composer-common/lib/query/queryanalyzer.js +++ b/packages/composer-common/lib/query/queryanalyzer.js @@ -29,16 +29,27 @@ const LOG = Logger.getLog('QueryAnalyzer'); */ 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) { + this.query = query; + } + /** * Extract the names and types of query parameters - * @param {QueryManager} query The query to process. * @return {object[]} The names and types of the query parameters */ - analyze(query) { + analyze() { const method = 'analyze'; - LOG.entry(method, query); - const result = query.accept(this, {}); + LOG.entry(method); + const result = this.query.accept(this, {}); LOG.exit(method, result); + return result; } @@ -52,6 +63,7 @@ class QueryAnalyzer { visit(thing, parameters) { const method = 'visit'; LOG.entry(method, thing, parameters); + console.log('xxxxx visit parameters = ', parameters); let result = null; if (thing instanceof Query) { result = this.visitQuery(thing, parameters); @@ -93,6 +105,10 @@ class QueryAnalyzer { // 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); @@ -110,6 +126,7 @@ class QueryAnalyzer { const method = 'visitSelect'; LOG.entry(method, select, parameters); + console.log('visiting ' + method + JSON.stringify(select.ast) + 'parameters = ' + parameters); let results = []; // Handle the where clause, if it exists. @@ -152,6 +169,7 @@ class QueryAnalyzer { visitWhere(where, parameters) { const method = 'visitWhere'; LOG.entry(method, where, parameters); + console.log('visiting ' + method + JSON.stringify(where.ast)); // Simply visit the AST, which will generate a selector. // The root of the AST is probably a binary expression. @@ -185,9 +203,13 @@ class QueryAnalyzer { 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); + const result = []; + const value = this.visit(limit.getAST(), parameters); + const rparams = parameters.requiredParameters; + if(rparams !== null && rparams.length === 1){ + result.push({name: rparams[0], type: 'Integer'}); + } LOG.exit(method, result); return result; } @@ -203,7 +225,12 @@ class QueryAnalyzer { const method = 'visitSkip'; LOG.entry(method, skip, parameters); // Get the skip value from the AST. - const result = this.visit(skip.getAST(), parameters); + const result = []; + let value = this.visit(skip.getAST(), parameters); + const rparams = parameters.requiredParameters; + if(rparams !== null && rparams.length === 1){ + result.push({name: rparams[0], type: 'Integer'}); + } LOG.exit(method, result); return result; } @@ -270,16 +297,21 @@ class QueryAnalyzer { LOG.entry(method, ast, parameters); let result = []; - const lhs = this.visit(ast.left, parameters); - console.log('Result of lhs: ' + lhs ); - result.concat(lhs); + // Grab the right hand side of this expression. const rhs = this.visit(ast.right, parameters); - console.log('Result of rhs: ' + rhs ); - result = result.concat(rhs); + const rparams = parameters.requiredParameters; + const lhs = this.visit(ast.left, parameters); + - console.log('***** result of conditional operator ' + result); + if( rparams !== null && rparams.length === 1){ + //find the variable's type from the rhs + const paramType = this.getParameterType(lhs); + result.push({name: rparams[0], type: paramType}); + result.concat(rhs); + } + // result.concat(lhs); LOG.exit(method, result); return result; } @@ -296,25 +328,80 @@ class QueryAnalyzer { visitIdentifier(ast, parameters) { const method = 'visitIdentifier'; LOG.entry(method, ast, parameters); - const result = []; - - console.log('**** visiting ' + JSON.stringify(ast)); + // clear the parameters for each indentifier + if( parameters !== null ){ + parameters.requiredParameters= []; + } // Check to see if this is a parameter reference. const parameterMatch = ast.name.match(/^_\$(.+)/); if (parameterMatch) { const parameterName = parameterMatch[1]; - // TODO - figure out the type of the parameter by looking - // at the type of the LHS or RHS if there is one - result.push({name: parameterName, type : 'String'}); + parameters.requiredParameters.push(parameterName); + const parametersToUse = parameters.parametersToUse; + const selector = () => { + return parametersToUse[parameterName]; + }; + LOG.exit(method, selector); + return selector; + } + + // Otherwise it's a property name. + // TODO: We should validate that it is 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} parameterNames The parameter name or name with nested structure e.g A.B.C + * @return {string} The result of the parameter type or null + * @private + */ + getParameterType(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; + if(parameterName === null || parameterName === undefined ) {return result;} + + const parameterNames = parameterName.split('.'); + + if(parameterNames === null || parameterNames.length === 0){ + throw new Error('Can only find a valid property type.'); } - console.log('**** visitIdentifier result ' + result); + // checks the resource type exists + let classDeclaration = modelManager.getType(resource); + // check that it is not an enum or concept + if(/*classDeclaration.isConcept() ||*/classDeclaration.isEnum()) { + throw new Error('Can only select assets, participants and transactions.'); + } + + for(let n=0; n0) { + result[0].type = 'Integer'; } LOG.exit(method, result); return result; @@ -225,11 +225,10 @@ class QueryAnalyzer { const method = 'visitSkip'; LOG.entry(method, skip, parameters); // Get the skip value from the AST. - const result = []; - let value = this.visit(skip.getAST(), parameters); - const rparams = parameters.requiredParameters; - if(rparams !== null && rparams.length === 1){ - result.push({name: rparams[0], type: 'Integer'}); + const result = this.visit(skip.getAST(), parameters); + + if(result.length>0) { + result[0].type = 'Integer'; } LOG.exit(method, result); return result; @@ -249,14 +248,11 @@ class QueryAnalyzer { // Binary expressions are handled differently in Mango based on the type, // so figure out the type and handle it appropriately. const arrayCombinationOperators = [ 'AND', 'OR' ]; - const conditionOperators = [ '<', '<=', '>', '>=', '==', '!=' ]; let result; if (arrayCombinationOperators.indexOf(ast.operator) !== -1) { result = this.visitArrayCombinationOperator(ast, parameters); - } else if (conditionOperators.indexOf(ast.operator) !== -1) { - result = this.visitConditionOperator(ast, parameters); } else { - throw new Error('The query compiler does not support this binary expression'); + result = this.visitConditionOperator(ast, parameters); } LOG.exit(method, result); @@ -298,20 +294,22 @@ class QueryAnalyzer { let result = []; - // Grab the right hand side of this expression. + // Grab the right hand side of this expression. const rhs = this.visit(ast.right, parameters); - const rparams = parameters.requiredParameters; 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( rparams !== null && rparams.length === 1){ - //find the variable's type from the rhs - const paramType = this.getParameterType(lhs); - result.push({name: rparams[0], type: paramType}); - result.concat(rhs); + // 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); } - // result.concat(lhs); LOG.exit(method, result); return result; } @@ -329,25 +327,18 @@ class QueryAnalyzer { const method = 'visitIdentifier'; LOG.entry(method, ast, parameters); - // clear the parameters for each indentifier - if( parameters !== null ){ - parameters.requiredParameters= []; - } // Check to see if this is a parameter reference. const parameterMatch = ast.name.match(/^_\$(.+)/); if (parameterMatch) { const parameterName = parameterMatch[1]; - parameters.requiredParameters.push(parameterName); - const parametersToUse = parameters.parametersToUse; - const selector = () => { - return parametersToUse[parameterName]; - }; - LOG.exit(method, selector); - return selector; + 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. - // TODO: We should validate that it is a property name! const selector = ast.name; LOG.exit(method, selector); return selector; @@ -357,48 +348,52 @@ class QueryAnalyzer { /** * Visitor design pattern; handle a literal. * Literals are just plain old literal values ;-) - * @param {string} parameterNames The parameter name or name with nested structure e.g A.B.C + * @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 */ - getParameterType(parameterName) { + getPropertyType(parameterName) { const method = 'getParameterType'; LOG.entry(method, parameterName); - // The grammar ensures that the resource property is set. + // The grammar ensures that the resource property is set. const modelManager = this.query.getQueryFile().getModelManager(); const resource = this.query.getSelect().getResource(); let result = null; - if(parameterName === null || parameterName === undefined ) {return result;} - const parameterNames = parameterName.split('.'); - if(parameterNames === null || parameterNames.length === 0){ - throw new Error('Can only find a valid property type.'); - } - // checks the resource type exists let classDeclaration = modelManager.getType(resource); - // check that it is not an enum or concept - if(/*classDeclaration.isConcept() ||*/classDeclaration.isEnum()) { - throw new Error('Can only select assets, participants and transactions.'); - } - for(let n=0; n ./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-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 653ccf31d0..2b6f3d8282 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -18,6 +18,8 @@ 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. * @param {Object} app The LoopBack application. @@ -222,7 +224,7 @@ function registerQueryMethod(app, dataSource, Query, connector, query) { // 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: param.type, required: true, http: {verb : 'get', source: 'query'}} ); + accepts.push( {arg: param.name, type: LoopbackVisitor.toLoopackType(param.type), required: true, http: {verb : 'get', source: 'query'}} ); } // Define and register dynamic query method 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 - } -} From 15380889f4e1474a3fa57c243a70e074ecf9292d Mon Sep 17 00:00:00 2001 From: xufengli Date: Wed, 19 Jul 2017 15:12:18 +0100 Subject: [PATCH 10/17] Adding Unit Test for loop connector --- .../server/boot/composer-discovery.js | 2 +- .../lib/businessnetworkconnector.js | 1 - .../test/businessnetworkconnector.js | 89 +++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index 2b6f3d8282..1706a5f305 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -224,7 +224,7 @@ function registerQueryMethod(app, dataSource, Query, connector, query) { // 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.toLoopackType(param.type), required: true, http: {verb : 'get', source: 'query'}} ); + accepts.push( {arg: param.name, type: LoopbackVisitor.toLoopbackType(param.type), required: true, http: {verb : 'get', source: 'query'}} ); } // Define and register dynamic query method diff --git a/packages/loopback-connector-composer/lib/businessnetworkconnector.js b/packages/loopback-connector-composer/lib/businessnetworkconnector.js index 64a408463a..892e621e8d 100644 --- a/packages/loopback-connector-composer/lib/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/lib/businessnetworkconnector.js @@ -937,7 +937,6 @@ class BusinessNetworkConnector extends Connector { callback(null, result); }) .catch((error) => { - console.log(error); debug('executeQuery', 'error thrown doing query', error); callback(error); }); diff --git a/packages/loopback-connector-composer/test/businessnetworkconnector.js b/packages/loopback-connector-composer/test/businessnetworkconnector.js index 52d634417f..6dd3716849 100644 --- a/packages/loopback-connector-composer/test/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/test/businessnetworkconnector.js @@ -24,6 +24,8 @@ 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 QueryFile = require('composer-common').QueryFile; const NodeCache = require('node-cache'); const ParticipantRegistry = require('composer-client/lib/participantregistry'); const Serializer = require('composer-common').Serializer; @@ -52,6 +54,37 @@ describe('BusinessNetworkConnector', () => { transaction BaseTransaction { }`; + // const QUERY_MODEL_FILE =` + // 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 + // }`; + let settings; let mockBusinessNetworkConnectionWrapper; let mockBusinessNetworkConnection; @@ -62,6 +95,8 @@ describe('BusinessNetworkConnector', () => { let modelManager; let factory; let introspector; + let mockQuery; + let mockQueryFile; beforeEach(() => { @@ -90,6 +125,12 @@ describe('BusinessNetworkConnector', () => { introspector = new Introspector(modelManager); factory = new Factory(modelManager); + // create mock query + mockQuery = sinon.createStubInstance(Query); + mockQueryFile = sinon.createStubInstance(QueryFile); + mockQuery.getQueryFile.returns(mockQueryFile); + mockQueryFile.getModelManager.returns(modelManager); + sandbox = sinon.sandbox.create(); // setup test instance @@ -2131,6 +2172,54 @@ describe('BusinessNetworkConnector', () => { }); + describe('#executeQuery', () => { + + let mockAssetRegistry; + + beforeEach(() => { + sinon.stub(testConnector, 'ensureConnected').resolves(mockBusinessNetworkConnection); + testConnector.connected = true; + mockAssetRegistry = sinon.createStubInstance(AssetRegistry); + mockBusinessNetworkConnection.getAssetRegistry.resolves(mockAssetRegistry); + testConnector.serializer = mockSerializer; + }); + + it('should call the executeQuery with a result', () => { + let query = Query.buildQuery('SELECT org.acme.base.BaseAsset WHERE (theValue == _$inputValue)'); + const cb = sinon.stub(); + + return testConnector.executeQuery(query.name, { inputValue: '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, transactions) + result.should.deep.equal([{ + transactionId: 'tx1', + $class: 'sometx' + }, { + transactionId: 'tx2', + $class: 'sometx' + }]); + + }); + }); + + it('should handle an error when executing query', () => { + mockBusinessNetworkConnection.executeQuery.resolves() ; + // mockTransactionRegistry.getAll.rejects(new Error('such error')); + const cb = sinon.stub(); + return testConnector.executeQuery({ test: 'options' }, cb) + .then(() => { + sinon.assert.calledOnce(testConnector.ensureConnected); + sinon.assert.calledWith(testConnector.ensureConnected, { test: 'options' }); + const error = cb.args[0][0]; // First call, first argument (error) + error.should.match(/such error/); + }); + }); + + }); + describe('#getTransactionByID', () => { let mockTransactionRegistry; From acb5d43546f91b9b6c5f6f62e83b881ad72edaea Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Wed, 19 Jul 2017 15:12:56 +0100 Subject: [PATCH 11/17] wip --- .../server/boot/composer-discovery.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index 2b6f3d8282..e5935f5a21 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -123,7 +123,7 @@ function createQueryModel(app, dataSource) { // Create the query model schema. let modelSchema = { name: 'Query', - description: 'Content-based Query Methods', + description: 'Named queries', plural: '/queries', base: 'Model' }; @@ -205,14 +205,13 @@ function registerQueryMethods(app, dataSource) { */ function registerQueryMethod(app, dataSource, Query, connector, query) { - const analyzer = new QueryAnalyzer(query); - const parameters = analyzer.analyze(); + console.log('*** register query method: ' + query.getName() ); + const parameters = query.getParameters(); + console.log('*** parameters: ' + JSON.stringify(parameters) ); const returnType = dataSource.settings.namespace ? query.getSelect().getResource() : ModelUtil.getShortName(query.getSelect().getResource()); - console.log('=====return type = ' + returnType); - // declare the arguments to the query method let accepts = []; @@ -224,7 +223,7 @@ function registerQueryMethod(app, dataSource, Query, connector, query) { // 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.toLoopackType(param.type), required: true, http: {verb : 'get', source: 'query'}} ); + accepts.push( {arg: param.name, type: LoopbackVisitor.toLoopbackType(param.type), required: true, http: {verb : 'get', source: 'query'}} ); } // Define and register dynamic query method @@ -795,10 +794,10 @@ module.exports = function (app, callback) { // Register the system methods. registerSystemMethods(app, dataSource); - // Create the query model. + // Create the query model createQueryModel(app, dataSource); - // Register the query methods. + // Register the named query methods registerQueryMethods(app, dataSource); // Discover the model definitions (types) from the connector. @@ -845,6 +844,7 @@ module.exports = function (app, callback) { callback(); }) .catch((error) => { + console.log('Exception: ' + error ); callback(error); }); }; From 8e4e3cd999d50d10c43c021a381c2373eb1680b0 Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Wed, 19 Jul 2017 15:13:26 +0100 Subject: [PATCH 12/17] wip --- packages/composer-common/lib/query/query.js | 9 +++++ .../lib/query/queryanalyzer.js | 40 +++++++++---------- packages/composer-common/lib/querymanager.js | 21 +++++++++- .../lib/businessnetworkconnector.js | 40 ++++++++++++++++--- 4 files changed, 82 insertions(+), 28 deletions(-) diff --git a/packages/composer-common/lib/query/query.js b/packages/composer-common/lib/query/query.js index 717e38bd7e..da95500e30 100644 --- a/packages/composer-common/lib/query/query.js +++ b/packages/composer-common/lib/query/query.js @@ -18,6 +18,7 @@ const IllegalModelException = require('../introspect/illegalmodelexception'); const ParseException = require('../introspect/parseexception'); const parser = require('./parser'); const Select = require('./select'); +const QueryAnalyzer = require('./queryanalyzer'); /** * Query defines a SELECT query over a resource (asset, transaction or participant) @@ -145,6 +146,14 @@ class Query { return this.select; } + /** + * Returns the parameters (names and types) for this query + * @return {object[]} The array of paramters + */ + getParameters() { + const qa = new QueryAnalyzer(this); + return qa.analyze(); + } } module.exports = Query; diff --git a/packages/composer-common/lib/query/queryanalyzer.js b/packages/composer-common/lib/query/queryanalyzer.js index eecacc5796..c9e0276112 100644 --- a/packages/composer-common/lib/query/queryanalyzer.js +++ b/packages/composer-common/lib/query/queryanalyzer.js @@ -38,7 +38,7 @@ class QueryAnalyzer { * @throws {IllegalModelException} */ constructor(query) { - if(!query) { + if (!query) { throw new Error('Invalid query'); } @@ -207,7 +207,7 @@ class QueryAnalyzer { LOG.entry(method, limit, parameters); // Get the limit value from the AST. const result = this.visit(limit.getAST(), parameters); - if(result.length>0) { + if (result.length > 0) { result[0].type = 'Integer'; } LOG.exit(method, result); @@ -227,7 +227,7 @@ class QueryAnalyzer { // Get the skip value from the AST. const result = this.visit(skip.getAST(), parameters); - if(result.length>0) { + if (result.length > 0) { result[0].type = 'Integer'; } LOG.exit(method, result); @@ -247,7 +247,7 @@ class QueryAnalyzer { // Binary expressions are handled differently in Mango based on the type, // so figure out the type and handle it appropriately. - const arrayCombinationOperators = [ 'AND', 'OR' ]; + const arrayCombinationOperators = ['AND', 'OR']; let result; if (arrayCombinationOperators.indexOf(ast.operator) !== -1) { result = this.visitArrayCombinationOperator(ast, parameters); @@ -299,13 +299,13 @@ class QueryAnalyzer { 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)) { + 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)) { + if (typeof lhs === 'string' && (rhs instanceof Array && rhs.length > 0)) { rhs[0].type = this.getPropertyType(lhs); result = result.concat(rhs); } @@ -335,7 +335,10 @@ class QueryAnalyzer { // We return a parameter object with a null type // performing the type inference in the parent visit - return [{name: parameterName, type: null}]; + return [{ + name: parameterName, + type: null + }]; } // Otherwise it's a property name. @@ -357,7 +360,7 @@ class QueryAnalyzer { LOG.entry(method, parameterName); // The grammar ensures that the resource property is set. - const modelManager = this.query.getQueryFile().getModelManager(); + const modelManager = this.query.getQueryFile().getModelManager(); const resource = this.query.getSelect().getResource(); let result = null; @@ -366,32 +369,29 @@ class QueryAnalyzer { // checks the resource type exists let classDeclaration = modelManager.getType(resource); - for(let n=0; n { @@ -937,8 +966,7 @@ class BusinessNetworkConnector extends Connector { callback(null, result); }) .catch((error) => { - console.log(error); - debug('executeQuery', 'error thrown doing query', error); + debug('executeQuery', 'error thrown executing query', error); callback(error); }); } @@ -988,7 +1016,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) => { From 9058fa4fda501d81a4b71e6ca3a91725d951fa29 Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Wed, 19 Jul 2017 15:31:30 +0100 Subject: [PATCH 13/17] Fixed type conversion for query --- packages/composer-common/lib/query/query.js | 11 +---------- .../server/boot/composer-discovery.js | 3 ++- .../lib/businessnetworkconnector.js | 4 +++- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/composer-common/lib/query/query.js b/packages/composer-common/lib/query/query.js index da95500e30..c6a2e6b9be 100644 --- a/packages/composer-common/lib/query/query.js +++ b/packages/composer-common/lib/query/query.js @@ -18,7 +18,7 @@ const IllegalModelException = require('../introspect/illegalmodelexception'); const ParseException = require('../introspect/parseexception'); const parser = require('./parser'); const Select = require('./select'); -const QueryAnalyzer = require('./queryanalyzer'); +const util = require('util'); /** * Query defines a SELECT query over a resource (asset, transaction or participant) @@ -145,15 +145,6 @@ class Query { getSelect() { return this.select; } - - /** - * Returns the parameters (names and types) for this query - * @return {object[]} The array of paramters - */ - getParameters() { - const qa = new QueryAnalyzer(this); - return qa.analyze(); - } } module.exports = Query; diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index e5935f5a21..13c53f30e7 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -206,7 +206,8 @@ function registerQueryMethods(app, dataSource) { function registerQueryMethod(app, dataSource, Query, connector, query) { console.log('*** register query method: ' + query.getName() ); - const parameters = query.getParameters(); + const qa = new QueryAnalyzer(query); + const parameters = qa.analyze(); console.log('*** parameters: ' + JSON.stringify(parameters) ); const returnType = dataSource.settings.namespace ? query.getSelect().getResource() diff --git a/packages/loopback-connector-composer/lib/businessnetworkconnector.js b/packages/loopback-connector-composer/lib/businessnetworkconnector.js index 677deee0c6..062942e4ab 100644 --- a/packages/loopback-connector-composer/lib/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/lib/businessnetworkconnector.js @@ -24,6 +24,7 @@ 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'); /** @@ -933,7 +934,8 @@ class BusinessNetworkConnector extends Connector { throw new Error('Named query ' + queryName + ' does not exist in the business network.'); } - const parameters = query.getParameters(); + const qa = new QueryAnalyzer(query); + const parameters = qa.analyze(); for(let n=0; n < parameters.length; n++) { const param = parameters[n]; From 975f1c00d37a55987bd419a95cb99bb9d76c41cd Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Wed, 19 Jul 2017 18:02:08 +0100 Subject: [PATCH 14/17] wip testing --- packages/composer-common/lib/query/query.js | 1 - packages/composer-common/test/querymanager.js | 19 ++ .../lib/businessnetworkconnector.js | 75 ++++--- .../test/businessnetworkconnector.js | 194 ++++++++++++------ 4 files changed, 190 insertions(+), 99 deletions(-) diff --git a/packages/composer-common/lib/query/query.js b/packages/composer-common/lib/query/query.js index c6a2e6b9be..3a3d91efd1 100644 --- a/packages/composer-common/lib/query/query.js +++ b/packages/composer-common/lib/query/query.js @@ -18,7 +18,6 @@ const IllegalModelException = require('../introspect/illegalmodelexception'); const ParseException = require('../introspect/parseexception'); const parser = require('./parser'); const Select = require('./select'); -const util = require('util'); /** * Query defines a SELECT query over a resource (asset, transaction or participant) 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/loopback-connector-composer/lib/businessnetworkconnector.js b/packages/loopback-connector-composer/lib/businessnetworkconnector.js index 062942e4ab..ebb50a2ad2 100644 --- a/packages/loopback-connector-composer/lib/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/lib/businessnetworkconnector.js @@ -922,43 +922,52 @@ class BusinessNetworkConnector extends Connector { debug('executeQuery', options); debug('queryName', queryName); debug('queryParameters', util.inspect(queryParameters)); + console.log('executeQuery: ' + queryName); - // 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 = this.businessNetworkDefinition.getQueryManager().getQuery(queryName); + return this.ensureConnected(options) + .then((businessNetworkConnection) => { - if(!query) { - throw new Error('Named query ' + queryName + ' does not exist in the business network.'); - } + console.log('businessNetworkConnection: ' + util.inspect(businessNetworkConnection)); - 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] = Boolean.parse(paramValue); - break; - } - } + // console.log('****', businessNetworkConnection.getBusinessNetwork()); - 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); + console.log('query: ' + util.inspect(query)); + + 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; + } + } + + console.log('about to query with: ' + queryParameters); return businessNetworkConnection.query(queryName, queryParameters); }) .then((queryResult) => { diff --git a/packages/loopback-connector-composer/test/businessnetworkconnector.js b/packages/loopback-connector-composer/test/businessnetworkconnector.js index 6dd3716849..dd11b14748 100644 --- a/packages/loopback-connector-composer/test/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/test/businessnetworkconnector.js @@ -25,6 +25,7 @@ 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'); @@ -47,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 @@ -54,48 +60,17 @@ describe('BusinessNetworkConnector', () => { transaction BaseTransaction { }`; - // const QUERY_MODEL_FILE =` - // 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 - // }`; - let settings; let mockBusinessNetworkConnectionWrapper; let mockBusinessNetworkConnection; let mockBusinessNetworkDefinition; let mockSerializer; + let mockQueryManager; let sandbox; let testConnector; let modelManager; let factory; let introspector; - let mockQuery; let mockQueryFile; beforeEach(() => { @@ -107,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); @@ -119,18 +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); - - // create mock query - mockQuery = sinon.createStubInstance(Query); - mockQueryFile = sinon.createStubInstance(QueryFile); - mockQuery.getQueryFile.returns(mockQueryFile); - mockQueryFile.getModelManager.returns(modelManager); - sandbox = sinon.sandbox.create(); // setup test instance @@ -2172,50 +2141,145 @@ describe('BusinessNetworkConnector', () => { }); - describe('#executeQuery', () => { - - let mockAssetRegistry; + describe.only('#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; - mockAssetRegistry = sinon.createStubInstance(AssetRegistry); - mockBusinessNetworkConnection.getAssetRegistry.resolves(mockAssetRegistry); 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 a result', () => { - let query = Query.buildQuery('SELECT org.acme.base.BaseAsset WHERE (theValue == _$inputValue)'); + 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' + }]); + }); + }); - return testConnector.executeQuery(query.name, { inputValue: 'blue' }, {test: 'options'}, cb) + 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, transactions) + const result = cb.args[0][1]; // First call, second argument (error, queryResult) result.should.deep.equal([{ - transactionId: 'tx1', - $class: 'sometx' - }, { - transactionId: 'tx2', - $class: 'sometx' + $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 handle an error when executing query', () => { - mockBusinessNetworkConnection.executeQuery.resolves() ; - // mockTransactionRegistry.getAll.rejects(new Error('such error')); + it('should call the executeQuery with an expected integer result', () => { + const cb = sinon.stub(); - return testConnector.executeQuery({ test: 'options' }, cb) - .then(() => { + return testConnector.executeQuery( 'integerQuery', { param1: '100' }, {test: 'options' }, cb) + .then(( queryResult) => { sinon.assert.calledOnce(testConnector.ensureConnected); sinon.assert.calledWith(testConnector.ensureConnected, { test: 'options' }); - const error = cb.args[0][0]; // First call, first argument (error) - error.should.match(/such error/); + + 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./); }); }); From bdb328a823f940e315a3e2b6186b29cd20664103 Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Wed, 19 Jul 2017 22:03:24 +0100 Subject: [PATCH 15/17] unit tests for businessnetworkconnector --- .../lib/businessnetworkconnector.js | 10 --- .../test/businessnetworkconnector.js | 80 ++++++++++++++++++- 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/packages/loopback-connector-composer/lib/businessnetworkconnector.js b/packages/loopback-connector-composer/lib/businessnetworkconnector.js index ebb50a2ad2..eb1102cc5f 100644 --- a/packages/loopback-connector-composer/lib/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/lib/businessnetworkconnector.js @@ -922,23 +922,15 @@ class BusinessNetworkConnector extends Connector { debug('executeQuery', options); debug('queryName', queryName); debug('queryParameters', util.inspect(queryParameters)); - console.log('executeQuery: ' + queryName); return this.ensureConnected(options) .then((businessNetworkConnection) => { - - console.log('businessNetworkConnection: ' + util.inspect(businessNetworkConnection)); - - // console.log('****', businessNetworkConnection.getBusinessNetwork()); - - // 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); - console.log('query: ' + util.inspect(query)); if(!query) { throw new Error('Named query ' + queryName + ' does not exist in the business network.'); @@ -967,7 +959,6 @@ class BusinessNetworkConnector extends Connector { } } - console.log('about to query with: ' + queryParameters); return businessNetworkConnection.query(queryName, queryParameters); }) .then((queryResult) => { @@ -977,7 +968,6 @@ class BusinessNetworkConnector extends Connector { callback(null, result); }) .catch((error) => { - debug('executeQuery', 'error thrown executing query', error); callback(error); }); } diff --git a/packages/loopback-connector-composer/test/businessnetworkconnector.js b/packages/loopback-connector-composer/test/businessnetworkconnector.js index dd11b14748..644fc18bbf 100644 --- a/packages/loopback-connector-composer/test/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/test/businessnetworkconnector.js @@ -2141,7 +2141,7 @@ describe('BusinessNetworkConnector', () => { }); - describe.only('#executeQuery', () => { + describe('#executeQuery', () => { beforeEach(() => { // create mock query @@ -2284,6 +2284,60 @@ describe('BusinessNetworkConnector', () => { }); + 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; @@ -2563,10 +2617,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' : {}, From 0aee48e3d7e4b662a32bedcf72f3fadc3837c91e Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Wed, 19 Jul 2017 22:17:38 +0100 Subject: [PATCH 16/17] query docs --- packages/composer-cli/bond-network@0.1.1.bna | Bin 0 -> 5116 bytes packages/composer-cli/trade-network@0.1.1.bna | Bin 0 -> 6998 bytes packages/composer-cli/trade-network@0.1.2.bna | Bin 0 -> 6999 bytes .../jekylldocs/business-network/query.md | 88 ++++++------------ 4 files changed, 26 insertions(+), 62 deletions(-) create mode 100644 packages/composer-cli/bond-network@0.1.1.bna create mode 100644 packages/composer-cli/trade-network@0.1.1.bna create mode 100644 packages/composer-cli/trade-network@0.1.2.bna 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 0000000000000000000000000000000000000000..fe26bcae91a0566215e3432a88797225da581f80 GIT binary patch literal 5116 zcmc&&Pmdf&6`zEVgaJf^_yCVair1^1Zrh0yG02GV&KM6~@2++?F~&-wn(msJio3ho zUDci)t@g?V?uaX2ASVvM4JkJyE*$s*+&Cd2#Esvp>YnMDUBi)z+55X=VB(q&yCJ60-4WLE=|zC2x6TT+Q>5K z2cO;wzaQQTF86|5WD*k-ohQ9q+J!Ek2YbOwSsJBtEZhojhqp14NE4T;uzvPF$mv8* zWhwJm(ok4Ir*l!Nd`9;x!#f%}T#56nPKlzFMKwv4nL~K?JW*vqVU#Fi>D_m!QISL$ z&(%vwaVdqBM7^HTRT;~(Wg!VntBFJCPa*Y0EhTE7B%I^|x?@i(jm3s}kx^OYl&aif z!Tt0DX?Q>(lWJq@*)?q(&)F!ca)Uk(WNu_Hp39gQJX5{cuq8Ui694T7pBwx_c!+%!&v82Fw}}>I-U#aS-Q2Ng*jNDEKXruei~nPNP*wlMuD!rZ)?VB0L3gQ`qrGLvQ0c%WxQR;04Qgb7&&*rb$pFBJ&|OJ(Ot*ETF+jb zIu0KnoAVNwxt2xexr|}*p3EjPNo3L{^WgGYeBeR}SQq)EVrO9g_rtq5k;8x|dfyRk zVhofBLr3Xj#`E5Fqz)^d*B?96S*`1x@b-tGf!{Xd^%!hw#t1Pb0QxN0-HmpUF`&tJ zLtp^$j}#K|te?ttB0tM?Ql-+grgddoUkxA~8@yWx%TYd;B9Uci&l!Z1RL9SeCW5{# zD+#^yY@APZ(4UIb`0r6!GA4Na^742i*Zu3~fB5>BuXQ^7ehsl%x^ ztMhb8$dbug9!Fu=?Ve$K9uoW}Q^HXi9h9MoFfwV>e^5L=AcOeIM8C@k0{LOFScKCe zOG91GB2&mX;;-J_-g~_}y+zWy6U}XwMx1X-3x^^dG5=7<6{F7?t)4TURTv{J3;68Z zB#}%d(;1hrLzdOu^!4BD2N!>apZWb3*ELyYuqJ$ALJ_CHU6*!g5XW3=_H}Mcozejp z5ookQ!rqs(X>ia5?&}=b)5?X$6N>;J5~h}`i{B@ ze%J|mWin)BgnlDWs-e2GN1p}<2aX%)a`kLSTxDg@4%k+n8?l-whG3s|gct5X6?%A# z6S{Zr9;wymO>$d^*fU|vqbJ?Vt~bC?PDPbkdbI@{mb?Y7ob<{ucmL-JU&XSe-lp&2 z?$z@aMh?Ly+9dkLmtXw;1)3gy86Y*0Ge6RC4G%7FE%(7Ee`*o>jDI>B`q9DB;B0*OV|I57p!C=!5>hCbR!tQm zf3mEz3*h6WoLbkuHK20x>N(bDTVx$c^4y{hn`Va#CUaUI%TnoN#0q=)!@*N0^T}xR z$PFGJ9iBaOqo*bp~Nq1v}zjXR-#KG1Qdin29Q;eDp^7b z8=|$Rr)Udh$FBtWgFX8Mx!0i3@NVOzmQo+Ol^saC##f}96zJf%|6N|c4B_$|H6fqL zZP9TmMadn~>C#x4UC)p7v_|f=`x}`nT`GG;ffhpIHCVO|G1Tb8(P2A+6D~;G1eiNh zXs=NLE8eC)ea#4beJs*SZq81D+CiqP+}86~d|7NcchjbcSVDXDy-yHswb%P$levBB zW52p`;wrpLmRsN+pNotIDT(l%;-(b}I zo7riVzc4_8>ZEG2mB0S^o7aE6z!JZFabpU!Ln=li7P;bf;4c~pRcLj6P4j&)>YTfR z?9s>mrbg)eXoss+P%i~Hf57!|sVn$zNx8OE;l5$#DE?Cg#{--kYbtlQu2eA|X@OgT zD|)q*kYzlrW$1}Thl3^})&(%s5ns(Pgq+VeY9gO!!ho>X%dN_5PfE&6THUAWV2Lcef*Us-8z=SD`un@r7 vW?mEfB){JASKn+>{-#B@lm1%5?`n>w1ROv5Dvr!2d8_kF+<|`fZT|WXU)wW+ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..29de3419954f6c71300372263f91ce73cde418f6 GIT binary patch literal 6998 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 J&ws#gUjULuY61WN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7e67dd47680b7bba4df530ac03052054ebff4896 GIT binary patch literal 6999 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 EFGpNnMgRZ+ literal 0 HcmV?d00001 diff --git a/packages/composer-website/jekylldocs/business-network/query.md b/packages/composer-website/jekylldocs/business-network/query.md index 973cc3bd80..6fe29d0a2e 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-RC 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. - - From 72404a04763b685559579dff88949027000e6b94 Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Thu, 20 Jul 2017 00:17:14 +0100 Subject: [PATCH 17/17] query tests --- .../server/boot/composer-discovery.js | 26 ++- packages/composer-rest-server/test/assets.js | 5 +- .../test/bond-network.bna | Bin 3257 -> 0 bytes .../test/data/bond-network/README.md | 8 + .../test/data/bond-network/lib/logic.js | 41 ++++ .../test/data/bond-network/models/bond.cto | 59 ++++++ .../test/data/bond-network/package.json | 1 + .../test/data/bond-network/permissions.acl | 19 ++ .../test/data/bond-network/queries.qry | 4 + .../composer-rest-server/test/participants.js | 5 +- packages/composer-rest-server/test/queries.js | 188 ++++++++++++++++++ .../test/server/boot/composer-discovery.js | 18 +- .../test/server/server.js | 5 +- packages/composer-rest-server/test/system.js | 5 +- .../composer-rest-server/test/transactions.js | 5 +- .../lib/businessnetworkconnector.js | 3 +- .../test/assets.js | 5 +- .../test/bond-network.bna | Bin 6033 -> 0 bytes .../test/businessnetworkconnector.js | 17 ++ .../test/data/bond-network/README.md | 8 + .../test/data/bond-network/lib/logic.js | 41 ++++ .../test/data/bond-network/models/bond.cto | 59 ++++++ .../test/data/bond-network/package.json | 1 + .../test/data/bond-network/permissions.acl | 19 ++ .../test/data/bond-network/queries.qry | 4 + .../test/participants.js | 5 +- 26 files changed, 509 insertions(+), 42 deletions(-) delete mode 100644 packages/composer-rest-server/test/bond-network.bna create mode 100644 packages/composer-rest-server/test/data/bond-network/README.md create mode 100644 packages/composer-rest-server/test/data/bond-network/lib/logic.js create mode 100644 packages/composer-rest-server/test/data/bond-network/models/bond.cto create mode 100644 packages/composer-rest-server/test/data/bond-network/package.json create mode 100644 packages/composer-rest-server/test/data/bond-network/permissions.acl create mode 100644 packages/composer-rest-server/test/data/bond-network/queries.qry create mode 100644 packages/composer-rest-server/test/queries.js delete mode 100644 packages/loopback-connector-composer/test/bond-network.bna create mode 100644 packages/loopback-connector-composer/test/data/bond-network/README.md create mode 100644 packages/loopback-connector-composer/test/data/bond-network/lib/logic.js create mode 100644 packages/loopback-connector-composer/test/data/bond-network/models/bond.cto create mode 100644 packages/loopback-connector-composer/test/data/bond-network/package.json create mode 100644 packages/loopback-connector-composer/test/data/bond-network/permissions.acl create mode 100644 packages/loopback-connector-composer/test/data/bond-network/queries.qry diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index 13c53f30e7..aad7111899 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -172,9 +172,10 @@ 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) { +function registerQueryMethods(app, dataSource, namespaces) { // Grab the query model. const Query = app.models.Query; @@ -187,7 +188,7 @@ function registerQueryMethods(app, dataSource) { } queries.forEach((query) => { - registerQueryMethod(app, dataSource, Query, connector, query); + registerQueryMethod(app, dataSource, Query, connector, query, namespaces); }); resolve(queries); @@ -196,20 +197,21 @@ function registerQueryMethods(app, dataSource) { } /** - * Register a composer named query method. + * 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) { +function registerQueryMethod(app, dataSource, Query, connector, query, namespaces) { - console.log('*** register query method: ' + query.getName() ); + console.log('Registering named query: ' + query.getName()); const qa = new QueryAnalyzer(query); const parameters = qa.analyze(); - console.log('*** parameters: ' + JSON.stringify(parameters) ); - const returnType = dataSource.settings.namespace + const returnType = namespaces ? query.getSelect().getResource() : ModelUtil.getShortName(query.getSelect().getResource()); @@ -228,6 +230,7 @@ function registerQueryMethod(app, dataSource, Query, connector, query) { } // Define and register dynamic query method + /* istanbul ignore next */ const queryMethod = { [query.getName()]() { const args = [].slice.apply(arguments); @@ -798,9 +801,6 @@ module.exports = function (app, callback) { // Create the query model createQueryModel(app, dataSource); - // Register the named query methods - registerQueryMethods(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 ...'); @@ -809,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 ...'); 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 8014c9e5472d49784ba3df31e61e29d0a141fa10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3257 zcma)^0)p#V{IM42^Xd<)E=PBE>K%TXr2wqC(+V zL$dFqjIBhWC?(4~s?RCs?exCS^Lg&i{l{~EKi6~Jzx%p=kLf8UAOx^;=@I)4zf8Uj zK!6zF9M(z2$`TA|#qn9^7C(cfqNvif~CW9kt`0Du4l z0NB5+KIiR*byoK9|C_Qy`Uk}|$;^99i~aG$Cq4lcpk5L-;U-Ke&+@W$6sgB5T=_*1WJD4)WdELB1;ve+3oXECJwDC+Q6RS65Zj%5#P~!k-9JPUfW!M@QY=^_+(VW z>206QQxM_td3cz;hWVqWNMM1gyGT+*yK!y35zwvJ~5 zt=>Sy*~N&y!S-LYxm?1iCPTr_or>Km72L$5RS zRTgs84M!i9G-^+3ek5ua9z-;q+n-o+AStS|@UYd9Y(3fcq9Xd@+VN*)l)MLcFkJ_e z$B{Djgn)iQL5M*PX2|%RNs@J+7giTyck;a%%}pt8qybdYAUMhq68JIXwdUsDtSvaL zw14k&_5Eai;r;jvlt5K^kwM`CY45AYq7r&p=Y;U_W~SL}CHfCq6Nk&!U=*9QerBV5 zw8-Afi)2FSRO)Y(dyYQOBZ=Wvm7`6ZrK>^kW1)};Uf>;7*>PV@djTM2)q9|ich9T$ z9lyl~##^hwniM{^i@`ECRwmguWl2*}U2h??_F24H@=aoOHBy`_8<3lOnJ3a({YDLr zuT@ygR1-=yvJgGnlgD?o_jBcf=-R`1ySSe7|1RD$zY=c~df0GeE7gaUE?ytHcsaiV zoV>kUl%4V3U-XNkLw*7Zu%O0LRk=dePIvkjI*;^GLS%EREAwxj8;SIym<#CNXl7h6 zN1yEM3oDbBDSGoP>SF5&x31LoxY%CV2<}dwZYaSXX&NlKtdIP_5ZJsg;$k(#@fL-E z!A6CTw~{*$A$EL^r+ps{XGTUCpg|U9dxe?GSP6Js;fmLvrj_a7oa;lGC_}06 zvH`yWY;=l9DfKB5RP>pBJ{~z+dtSWxt)9X@PIYqrVIEx_+ncQ#d`oQ=T`%g8cVw#% zj@Qa(RYMQHRw0beZbF$zD~m7 z9K~xU?;Eadg8IznX1AGkS3|pVD@1#`pd!CjLmx+HPe(UbdM)(!`c*Xyur&7XSLNDN z(20>aSe`}}IQvHqS-5HyFfP=0Eia(BH3hDKfcCeCxj_+F42{QgqHeQ?YUNOsyni|R zLDzhT&L4qQ)2He{?5(zb#m%T<)?O|?k-cmePT)WAYiQkpjMT;@hAcou9=Z}NR0VV} zoG8 z=zJ#>@lI@m#?j36zSsjTsU0gpNt+31(2I%54V13F^ziVj`@9_ZMd#I9MV^i8MR|9f zpYgJRdhGsMhZE)7dO3Z}4_+ciyj!Aa-Osn(YCFXnbo)FCXna{q%dJ>CY1`fp(ehks zaB~9{8IpFH&nziL?;{NgMxnbG?$?7jtu0VY=lxOmZod9&4QifdLAi;@4|cwGuPr{C zC%|y?k`>v45{IvEPhJIWb5be<_(-URk?^4`6|8HBq2N;U4X!a=ELE<()|Mpdd&(xm zPwJUVU#1;-sj{o{oxI^|T>cB3Z29b1j_9efo1W#NCTY`WKAAsBAB8<*ybUjI$#%F@ z054Sw&o@%B_qz4FJF&QbSzSYO50^4Vt%Yr&(ogu-s|&*YM;Em{w7A2dh_Qwu@lEb! z*c5PpLJciXpEj0>F@0@v2`iJh9kj7$w?b#}M}n#J?%mDswdDD@`r)wt{#b7>e`QDK zbH5U4)YSu?&8qBCYa0+>3uRp|6_=CLS;?NaXVXh6S!5u0jbd`PYfG)X+}#%%B;VGp zNvGG85;+WB%0h#ttfKZCNy&98W_bfoJ7@-3DmGf|NARRsbV&PnW*swWM~=Xk*8wpH z3_@cNk6GRGI|gAwO@}b|j#&oj_R<=o|GYnG6O=M6@l0ovlK((5>77A%%E8|)aA0WH zRAGzex=q~ysCf=rE}~cIMk8!q5LGJe#@VE*RYf^bC{`0Z^Ca(+`)fcq-1*A-W9G=7 zurrtkue^p9C-#KthQ_Udcjss=`XQSLJwGk<2TX4Z=K9)t#`?-QmtSQEouF;fM9r0WzVCuL&;i;TFerNvni-! zK7PyEy!?hKn1t9n1#RdxeB*VdhN?>@(u^ehtRFDlOqA_|IO%iuv3%4Z z9pWo|L&B1xTj%Q-OiwW|N-+JrZ36*9->fGW;Endq^sD`3{%Sw}VH&@Z{>^-TCDHc` zkW2jS8|k|h{T1=&v4M8QRzolAe;bTn!M=v~-}d6i@IHTseK#9)=#JU=AK$#Y#rz00 zVx~KpT@B}dhq?0uu)8GuNL&QbiT^AR{~vVs8Tt{*0|xwfqW%rX%(9~f5dDVJljZ$o Gi~a) 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/loopback-connector-composer/lib/businessnetworkconnector.js b/packages/loopback-connector-composer/lib/businessnetworkconnector.js index eb1102cc5f..98cf0b9cee 100644 --- a/packages/loopback-connector-composer/lib/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/lib/businessnetworkconnector.js @@ -1072,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 }); }); 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 ea3eea47004efb0abae5b40a37288ab8c773baf4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6033 zcma)=byQSsyN8Fap}Rpzk&;HFK{^B!7(kHjhCu`zy1Sd95k{mzLYe_29O*9U7C|Hq zzP{&Ezct4To-`9RLlu=Mg0000cpc*uxBxtI)FNzHSv;hGC0ssx* znWKXlr-Qkhr=yE4*E0tb9c?TCO8l;hUe>;fo;w%^fP}h%0s#DRNlM1zPqe4*#pvW2 z-vKRm2j0~xqKNZh{U(^a1MF!X%s7Y^P?`#*sCq2K@5zE+5mcU%KnlUf> z^u>ND!ZYXCZoTpKf~&;DW!vj-jwQ-Ss3@(10)x4DiyA1%U?d}jhwOY>RDhr*|DvRK znb|r7>12}VnZ*n(SI_S;$yLXnKe{Gv(UZ(}kEQ`&A|V6Z7V=wO%*Lw4(;>A%8w#kt zMskd{q@EA6 z-1V5+!yjH$Fc594Y0*NdW=X2$&fSB7VQ4XM-h@}2l^-G$P;dxR989!^3Jr0CWz(s`$57z6dnHcoynwM)K~o=gm=)@o8w4TOjn42&5^jl;m-K3bEHR# z@iC24>JNq~Q?1N&B|)&OGq3*beooBsd)~2+UUwyblbwHR%}C0pM_ws_*Xju6jdLF} zwT~pFwn}{=EF6w@MsVleQn7I-UYEle5VvxK+s@;u0#J=V1yYkWy|)~+x?7=+*p28K z?rJ{A%&vx8!pUt{>}?2)4Hi%q#HB=%E@%BzWSn31o7IUINORH8d-7CRooTK4D@@CL z6@CjwyP#igiObd>lq7N_RBVov07Q$=55=oN#M_Y3(iVviG>R%YBeDURhz)?CNRgKWN@DjeyLluqEg4k{8YB{B?kz%5v*4=F12DjuGrU|9XohU7Sy2y7p zP(w$32VGG|BA;unqS zBeV;E?!s?V8ip!x62pRzqj$l>Iw$Renoz00hE=;DkymjXgfEKw95F8(raMG}n$D9= zYFd!H5*fB{@|=odW8qmwhh}?H&|fcvJ-khulk>%!$t3Pcp_L9aO8H=dJDf;Ocu19C z;i}}FZfpO0+1uh4Ye$cVeOMS9K2%K{Xr5pyfatQvO?z!w*-Re49_{h?*4?=z=JOUZ zeB?!V&S~o_@Qod9a=04mAt%}+1H3JFi1DBTH7A38y#_7Sojg><2r`)B(Q|2sV!V&? zRJ#$itc_VrLrR1_IKlK`l`($s#r`c*z6Op1KidE$&rLGo+I5x_N#CPOtSk~>cVE{R zrp~x{rQqmLJ&t++y@hiYb`t^pK{NS@I7UG7XM;Wqpr(C?{+eC|uEfb;(pHeN8+TZk z%!{|j)2FgiPGV0V=?T4wBDOwN zrZwlQpCZADgQ9?){LR$8?RRB@6`yV|bEIs|G-SB!bWuq}zTO|DOG>Jp5bX(OdbP1H zFgno=N3_2W(kAq2_A6!Ax_l|chPYx+5^m*7nzG)3J;6paYzYF0Gg08(Qs%>a#>I;r z0fvMQ9Zm%gAXBbo)@}HX7Ee;R6e4{tY~Fy@l$_>3b1VdNt{m4@97L^`Y7+^FamY3(K_A z>XlSriERUT$lrs@kb9{gWTfT@B~?A62JH|Azdqa}q?k?<91yennnZk7IaMviY2)lH z*DihXgnF3kV-*i7OxS*w{lspLozr)fA1;lvRqb1c(i!Uhh3}jyr!k$ZwoXZ-jPuR; zhtdK6)>BsG5$$^=}+TIl$-wdE>o0C`B0=!w~+VJ~{3Tv}q#+bc; zne3z`^eIPfA3@(VKJ+xEHVlq!w#v89$saQk>p@w~SA5o?_lxXe**JFEl0WI5{qzyUr>+5#=-8rDo6& zS~gC1LiTfbE2;QXazB!L41moQT87QV)f0}m9;U1>b|D|S?RFuTDoi>pg_*Snt`|P^ zhOYguO4a=7H@ivc#431>Jv^VQ+%LPw(4@ z4T0Yal~%=YdJX0@6%nR6e}ZC#%b$$_VKOGYRhu=H&UqD9+HGAGkNa+6K&AVl-r$yL zhm3U{3I-0f-o4ss1#Se1%uRYpQO=P&!>&=A+e_?taqv_H_av&RKVbRga=2ZKmxRIuM9%q^=!W(>oa($Z2Ic-||`GT;y-#tn)`1oS>LsNa=MDL? zh%IMyDTnP>rtxt0c`fe;)zMqM=p}V)%Y}N*^viLFZ-&V;`hf26zOQV)7WYNk7I=}w z+5B33`SKDMjxJt5xZv2dB>PMq#&7xZ)?mt^(J+tI zivW;}3)*ncEyJ%+G;SR9MmldkvJiyQA2)wqP;VU6&iWbmAKt04`B1O{)h6N^k4RBo}lSCEeY&h_)0>?jVha99h1Sr zS*RWHI4EpH$ETS|O_+Vw+%u)9Wclqd9t6E1rOWc$evZ8%4|O{*+bp>M8D3QX=*yW~ zfwg@b2=7k^v+=K!1VIED3JM4-K)qdt28gNp}9jgxJ=r%eX&iR%0_T5pahGJnBC+t6wO{T5goqHk5Vr$TG}t7YuNCZ!Ys`| z-_mTlA;K%`pk&(wpsK<~5rQQPr`YRcQu=iFh{)czB3tNy2sUBT9F~k#K3=KYjX%nw z-);6lnxd^^pUpQFU#sQJNZ1oC>-6 zp7@D3rz|PDnFutMB108v2=2r!D(1>E{>Ogmuz9nK|_bwYX-T*xpz8gTALX0 zk1wz@=+PE3dt9dEB~GigRvSHGqPZd?*Gm%roLr2X?Abe-ncKPk3~%ngg?G$gLg^`E z+%5E*@H*av7yl>V`&$^7shi^;`F(Q(`5TbC)=c;e4}s6=lR?)K)5#AhK5PYHh;)Z4 z6XGUACH8RsFtqW)rr6ZNYa>OqN4**TQY-mAPtzPK;I|Q%8z{>|~TzN=U;)iu;%dtpxHg^FF6) zKvw$T_Y#fXX?su>HK~@*Bot@5_nCwYykDQ5>2O2io;U1(%DQxblnSOx*{S9y=%u*ss^_cP zzU8tNB5YUG^C>?Vb!}Ru_4zvw;xJ`^}~&)ux~2 zR+hP?=ZqzMkyFOgYF_iZI9dfKQ^D@97XTFj>qPt2kpr<3%hDhKQ4n{%R(aa|m@U1;gMq_F&0#s-5+Ei#$a9ASyvIjwUn(kXvGo~00dv93rYE7G$#_qyD zXT&W0vm=@uxyQ4&Rh8HXC(u(BSLMY!m)yViLXh1t#NZ}Qfq(WwClga!6HD`(PUz_H zZ@ti6Th$f8LvYC<9YS~SO=cpQ#f$tz%7)K?S&9#Fh3@4&$^0B5%m}!jC9{aN@gkA! zLhI$ojE+%5((Qxb`k}3U=@%XiE6S}Hz#cu9@-F>y%uxaoDtesf58b|yib_^uPqswH z`)o5)y*2mL;vthZ#$R`1=0Erp)RoNno_QB;MM011^oO_jKoRnGpF-3^5B1t>jMjkT`TVLW{=3VN@sMWrlU>OAr z7NW5r;r!PIh3AGA>oN&=#wgll7`68?-VbS|dfb7_x8eF>e=baZU}c@J_z+dV1n`(W zxkXxJM)Te5#)dYx9_iH9AhXC3&BY>K7r{xnjUj_nI9*ESs(ZU^NJxPPQ)B2`fz3K% zJhwg5&v~V`9p|NmRi<;qI2a?Kcjx?&JAI@0PLJ+|CI%esFG|)G>0R|H6NAgV^ifqK zBqc4&nG&uITtvF(8OJU!^aFh|BS7#hL>bnh-AmZAE zY&F3J@rqwS&Y&HwZ;xVF?6(-Iq2zP|sn<)f7}xkI)np`L`t6hc3` zY$OT&m_?@BOKC3&4_KB_Kc>w1zR6lqWew5TQ4h6djlTw8;QpS_H8(-;nVVTZjP#@7 zIhniITf4eiJ36>>nV8!BTS$Xqd2PFRfI+7h*v?v9G7j8k3NvdABZn4Q6*cIr*i|Ya z`PVHl9S19`?RJL4)>EdeCKv=yew~dOysQ&+TZNH*@NTXn>Jy^}VC}mdTDL`rGPU}d zoNRN&)ZvPg{0HZN5MFuT5RqO?tD^of7K%w#fBiOqTeaJdzD54@+p8yjKSb zzgq18hWSmS&wJ4VNBo`|*KazE?=9f^$@oT|V*OsD)8Mx_R5$hMzWHI@424H>QnIRY zT=r)FRvp6_DfP~q>g->VCJWccNg8+rLL~ihmZN984D*?tN#1UK*;f>0ArP7w-akIZ zZO-INlH8L-qZ6@6))#YfIglbIRZ2FF39l^fGdy+cNJVLH2vui74s%;`LCw;XO2olB z<97}#$tGUxi|AM-X4&R_r#p(QGt&47c_u$=v>3^Vln2$Q&^DZ0&xRlyAcVu#8Bcr1 zFw*|>>7eL025f_FtmdE>3 zTygOZ3m1c{&*TU{pr46`@sX75hFOON_iVKyX(%Hh)1my+ktfA}?6Uy!{r0a1plCl1 zK>tf0e^CEPV*i&)0N9lZy!q1)rM)5jjfnobzrPCY57JE%00oeaKS@9J_TPwqzc&oP zuX_{61^kqje}nxf-(QvG*X{ZJg#8ql8|Zg|`P=IKl=$BV*RRkad;sA0G4_9N^LrJ4 wZ#2IWorrFT|J`=}f6(8b%wM62#DHI)(|^LDp?}X9=FKk diff --git a/packages/loopback-connector-composer/test/businessnetworkconnector.js b/packages/loopback-connector-composer/test/businessnetworkconnector.js index 644fc18bbf..1567d656b0 100644 --- a/packages/loopback-connector-composer/test/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/test/businessnetworkconnector.js @@ -2432,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' }]); }); @@ -2462,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' }]); }); @@ -2496,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' }]); }); @@ -2529,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' }]); }); 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();