diff --git a/.travis/before-install.sh b/.travis/before-install.sh index 8f9f56ca19..d67271b28e 100755 --- a/.travis/before-install.sh +++ b/.travis/before-install.sh @@ -10,7 +10,7 @@ sudo rm /usr/local/bin/docker-compose curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose chmod +x docker-compose sudo mv docker-compose /usr/local/bin -echo "Docker-compose version: " +echo "Docker-compose version: " docker-compose --version # Update docker @@ -22,7 +22,7 @@ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" sudo apt-get update sudo apt-get install docker-ce -echo "Docker version: " +echo "Docker version: " docker --version # Grab the parent (root) directory. @@ -51,6 +51,49 @@ echo "->- Build cfg being used" cat ${DIR}/build.cfg echo "-<-" + +###### +# checking the changes that are in this file +echo "Travis commit range $TRAVIS_COMMIT_RANGE" +echo "Travis commit $TRAVIS_COMMIT" +echo "Travis event type $TRAVIS_EVENT_TYPE" + + +if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then + echo -e "Build Pull Request #$TRAVIS_PULL_REQUEST => Branch [$TRAVIS_BRANCH]" +elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then + echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' +elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then + echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' +else + echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG'] Pull Request ['$TRAVIS_PULL_REQUEST']' +fi + + +cd $TRAVIS_BUILD_DIR +git diff --name-only $(echo $TRAVIS_COMMIT_RANGE | sed 's/\.//') + +if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then + git show --pretty=format: --name-only "$TRAVIS_COMMIT_RANGE"|sort|uniq > changedfiles.log +elif [ -n "$TRAVIS_PULL_REQUEST" ]; then + git diff --name-only "$TRAVIS_COMMIT" "$TRAVIS_BRANCH" > changedfiles.log +fi + +RESULT=$(cat changedfiles | sed '/^\s*$/d' | awk '!/composer-website/ { print "MORE" }') +if [ "${RESULT}" == "" ]; +then + echo "Only docs changes" +else + echo "More than docs changes" +fi +rm changedfiles.log + +cd - > /dev/null +###### + + + + # Check of the task current executing if [ "${FC_TASK}" = "docs" ]; then echo Doing Docs - no requirement for installations of other software diff --git a/packages/composer-common/.gitignore b/packages/composer-common/.gitignore index 9fbb2780af..0eaf316b67 100644 --- a/packages/composer-common/.gitignore +++ b/packages/composer-common/.gitignore @@ -47,4 +47,5 @@ out # Build generated files should be ignored by git, but not by npm. lib/acl/parser.js lib/introspect/parser.js +lib/query/parser.js index.d.ts diff --git a/packages/composer-common/.npmignore b/packages/composer-common/.npmignore index f56be875dc..dc39138d9d 100644 --- a/packages/composer-common/.npmignore +++ b/packages/composer-common/.npmignore @@ -47,4 +47,5 @@ out # Build generated files should be ignored by git, but not by npm. # lib/acl/parser.js # lib/introspect/parser.js +# lib/query/parser.js # index.d.ts diff --git a/packages/composer-common/api.txt b/packages/composer-common/api.txt index e5c1659dd2..02a03505ae 100644 --- a/packages/composer-common/api.txt +++ b/packages/composer-common/api.txt @@ -27,9 +27,9 @@ class BusinessNetworkMetadata { class Factory { + void constructor(ModelManager) + Resource newInstance(string,string,string,Object,boolean,string) throws ModelException - + Resource newResource(string,string,string,Object,boolean,string) throws ModelException - + Resource newConcept(string,string,Object,boolean,string) throws ModelException - + Relationship newRelationship(string,string,string) throws ModelException + + Resource newResource(string,string,string,Object,boolean,string) throws TypeNotFoundException + + Resource newConcept(string,string,Object,boolean,string) throws TypeNotFoundException + + Relationship newRelationship(string,string,string) throws TypeNotFoundException + Resource newTransaction(string,string,string,Object,string) + Resource newEvent(string,string,string,Object,string) + Object toJSON() @@ -45,7 +45,7 @@ class FileWallet extends Wallet { + Promise remove(string) } class IllegalModelException extends BaseException { - + void constructor(string,string,string) + + void constructor(String,ModelFile,Object,String,String,String,String) + string getModelFile() + string getFileLocation() } @@ -117,6 +117,10 @@ class Serializer { class ValidationException extends BaseException { + void constructor(string) } +class TypeNotFoundException extends BaseException { + + void constructor(String,String) + + string getTypeName() +} class Wallet { + Wallet getWallet() + void setWallet(Wallet) diff --git a/packages/composer-common/changelog.txt b/packages/composer-common/changelog.txt index d065b34084..705d983e68 100644 --- a/packages/composer-common/changelog.txt +++ b/packages/composer-common/changelog.txt @@ -12,6 +12,11 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # +Version 0.7.6 {1dbfded54f54b7861b8eadb64d95f82a} 2017-06-14 +- Added TypeNotFoundException +- Corrected JSDoc for IllegalModelException.constructor +- Corrected JSDoc for Factory.newRelationship + Version 0.7.5 {b2c1c30d370b04c153426500a03c3061} 2017-06-07 - Added optional JSZip options parameter to toArchive diff --git a/packages/composer-common/index.js b/packages/composer-common/index.js index d8b67f9e64..79081dc5c5 100644 --- a/packages/composer-common/index.js +++ b/packages/composer-common/index.js @@ -54,6 +54,7 @@ module.exports.AssetDeclaration = require('./lib/introspect/assetdeclaration'); module.exports.BaseException = require('./lib/baseexception'); module.exports.BusinessNetworkDefinition = require('./lib/businessnetworkdefinition'); module.exports.ClassDeclaration = require('./lib/introspect/classdeclaration'); +module.exports.CodeGen = require('./lib/codegen/codegen.js'); module.exports.ComboConnectionProfileStore = require('./lib/comboconnectionprofilestore'); module.exports.Concept = require('./lib/model/concept'); module.exports.ConceptDeclaration = require('./lib/introspect/conceptdeclaration'); @@ -74,8 +75,11 @@ module.exports.Logger = require('./lib/log/logger'); module.exports.LoopbackVisitor = require('./lib/codegen/fromcto/loopback/loopbackvisitor'); module.exports.ModelFile = require('./lib/introspect/modelfile'); module.exports.ModelManager = require('./lib/modelmanager'); +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.QueryFile = require('./lib/query/queryfile'); module.exports.Relationship = require('./lib/model/relationship'); module.exports.RelationshipDeclaration = require('./lib/introspect/relationshipdeclaration'); module.exports.Resource = require('./lib/model/resource'); @@ -84,11 +88,13 @@ module.exports.Script = require('./lib/introspect/script'); module.exports.ScriptManager = require('./lib/scriptmanager'); module.exports.SecurityContext = require('./lib/securitycontext'); module.exports.SecurityException = require('./lib/securityexception'); +module.exports.Select = require('./lib/query/select'); module.exports.Serializer = require('./lib/serializer'); +module.exports.Sort = require('./lib/query/sort'); module.exports.TransactionDeclaration = require('./lib/introspect/transactiondeclaration'); module.exports.TypescriptVisitor = require('./lib/codegen/fromcto/typescript/typescriptvisitor'); module.exports.Util = require('./lib/util'); module.exports.Wallet = require('./lib/wallet'); -module.exports.CodeGen = require('./lib/codegen/codegen.js'); +module.exports.Where = require('./lib/query/where'); module.exports.Writer = require('./lib/codegen/writer.js'); module.exports.version = require('./package.json'); diff --git a/packages/composer-common/lib/factory.js b/packages/composer-common/lib/factory.js index fa78dddc08..da5a11b536 100644 --- a/packages/composer-common/lib/factory.js +++ b/packages/composer-common/lib/factory.js @@ -17,6 +17,8 @@ const debug = require('debug')('ibm-concerto'); const Globalize = require('./globalize'); +const ModelUtil = require('./modelutil'); + const InstanceGenerator = require('./serializer/instancegenerator'); const ValueGeneratorFactory = require('./serializer/valuegenerator'); const ResourceValidator = require('./serializer/resourcevalidator'); @@ -86,7 +88,7 @@ class Factory { *
sample
return a resource instance with generated sample data.
*
empty
return a resource instance with empty property values.
* @return {Resource} the new instance - * @throws {ModelException} if the type is not registered with the ModelManager + * @throws {TypeNotFoundException} if the type is not registered with the ModelManager */ newResource(ns, type, id, options) { if(!id || typeof(id) !== 'string') { @@ -105,24 +107,9 @@ class Factory { })); } - let modelFile = this.modelManager.getModelFile(ns); - if(!modelFile) { - let formatter = Globalize.messageFormatter('factory-newinstance-notregisteredwithmm'); - throw new Error(formatter({ - namespace: ns - })); - } - - if(!modelFile.isDefined(type)) { - let formatter = Globalize.messageFormatter('factory-newinstance-typenotdeclaredinns'); - - throw new Error(formatter({ - namespace: ns, - type: type - })); - } + const qualifiedName = ModelUtil.getFullyQualifiedName(ns, type); + const classDecl = this.modelManager.getType(qualifiedName); - let classDecl = modelFile.getType(type); if(classDecl.isAbstract()) { let formatter = Globalize.messageFormatter('factory-newinstance-abstracttype'); throw new Error(formatter({ @@ -176,27 +163,11 @@ class Factory { *
sample
return a resource instance with generated sample data.
*
empty
return a resource instance with empty property values.
* @return {Resource} the new instance - * @throws {ModelException} if the type is not registered with the ModelManager + * @throws {TypeNotFoundException} if the type is not registered with the ModelManager */ newConcept(ns, type, options) { - let modelFile = this.modelManager.getModelFile(ns); - if(!modelFile) { - let formatter = Globalize.messageFormatter('factory-newinstance-notregisteredwithmm'); - throw new Error(formatter({ - namespace: ns - })); - } - - if(!modelFile.isDefined(type)) { - let formatter = Globalize.messageFormatter('factory-newinstance-typenotdeclaredinns'); - - throw new Error(formatter({ - namespace: ns, - type: type - })); - } - - let classDecl = modelFile.getType(type); + const qualifiedName = ModelUtil.getFullyQualifiedName(ns, type); + const classDecl = this.modelManager.getType(qualifiedName); if(classDecl.isAbstract()) { let formatter = Globalize.messageFormatter('factory-newinstance-abstracttype'); @@ -247,29 +218,14 @@ class Factory { * @param {string} type - the type of the Resource * @param {string} id - the identifier * @return {Relationship} - the new relationship instance - * @throws {ModelException} if the type is not registered with the ModelManager + * @throws {TypeNotFoundException} if the type is not registered with the ModelManager */ newRelationship(ns, type, id) { - let modelFile = this.modelManager.getModelFile(ns); - if(!modelFile) { - let formatter = Globalize.messageFormatter('factory-newrelationship-notregisteredwithmm'); - - throw new Error(formatter({ - namespace: ns - })); - } - - if(!modelFile.isDefined(type)) { - let formatter = Globalize.messageFormatter('factory-newinstance-typenotdeclaredinns'); - - throw new Error(formatter({ - namespace: ns, - type: type - })); - } + // Load the type declaration to force an error if it doesn't exist + const fqn = ModelUtil.getFullyQualifiedName(ns, type); + this.modelManager.getType(fqn); - let relationship = new Relationship(this.modelManager,ns,type,id); - return relationship; + return new Relationship(this.modelManager, ns, type, id); } /** diff --git a/packages/composer-common/lib/globalize.js b/packages/composer-common/lib/globalize.js index a2121c47a3..f289dd43ac 100644 --- a/packages/composer-common/lib/globalize.js +++ b/packages/composer-common/lib/globalize.js @@ -25,14 +25,8 @@ const messages = require('../messages/en.json'); function messageFormatter(message) { return function (inserts) { let result = messages.en[message]; - if (Array.isArray(inserts)) { - for (let i = 0; i < inserts.length; i++) { - result = result.replace(new RegExp(`{${i}}`), inserts[i]); - } - } else { - for (let key in inserts) { - result = result.replace(new RegExp(`{${key}}`, 'g'), inserts[key]); - } + for (let key in inserts) { + result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), inserts[key]); } return result; }; diff --git a/packages/composer-common/lib/introspect/functiondeclaration.js b/packages/composer-common/lib/introspect/functiondeclaration.js index 11b2e91a78..3cebba6716 100644 --- a/packages/composer-common/lib/introspect/functiondeclaration.js +++ b/packages/composer-common/lib/introspect/functiondeclaration.js @@ -16,6 +16,7 @@ const TransactionDeclaration = require('./transactiondeclaration'); const IllegalModelException = require('./illegalmodelexception'); +const ModelUtil = require('../modelutil'); //const Globalize = require('../globalize'); /** @@ -145,9 +146,9 @@ class FunctionDeclaration { throw new IllegalModelException('Transaction processing function ' + this.name + ' must have 1 function argument of type transaction.' ); } const transactionClassName = this.parameterTypes[0]; - const classDecl = this.modelManager.getType(transactionClassName); - if(!(classDecl instanceof TransactionDeclaration)) { + if (ModelUtil.isPrimitiveType(transactionClassName) || + !(this.modelManager.getType(transactionClassName) instanceof TransactionDeclaration)) { throw new IllegalModelException('Function ' + this.getName() + ' processes ' + transactionClassName + ' which is not a transaction.'); } } diff --git a/packages/composer-common/lib/introspect/illegalmodelexception.js b/packages/composer-common/lib/introspect/illegalmodelexception.js index 0fb9464e1a..ab8b7bf740 100644 --- a/packages/composer-common/lib/introspect/illegalmodelexception.js +++ b/packages/composer-common/lib/introspect/illegalmodelexception.js @@ -26,10 +26,14 @@ const BaseException = require('../baseexception'); class IllegalModelException extends BaseException { /** - * Create an IllegalModelException - * @param {string} message - the message for the exception - * @param {string} modelFile - the optional modelfile associated with the exception - * @param {string} fileLocation - the optional file location associated with the exception + * Create an IllegalModelException. + * @param {String} message - the message for the exception + * @param {ModelFile} [modelFile] - the optional modelfile associated with the exception + * @param {Object} [fileLocation] - location details of the error within the model file. + * @param {String} fileLocation.start.line - start line of the error location. + * @param {String} fileLocation.start.column - start column of the error location. + * @param {String} fileLocation.end.line - end line of the error location. + * @param {String} fileLocation.end.column - end column of the error location. */ constructor(message, modelFile, fileLocation) { diff --git a/packages/composer-common/lib/introspect/modelfile.js b/packages/composer-common/lib/introspect/modelfile.js index c42c4b9822..77a23c966b 100644 --- a/packages/composer-common/lib/introspect/modelfile.js +++ b/packages/composer-common/lib/introspect/modelfile.js @@ -83,12 +83,13 @@ class ModelFile { let decorate = false; let systemNamespace = ModelUtil.getSystemNamespace(); if(this.namespace !== systemNamespace) { - this.imports.unshift(systemNamespace + '._cst_Asset'); - this.imports.unshift(systemNamespace + '._cst_Participant'); - this.imports.unshift(systemNamespace + '._cst_Event'); - this.imports.unshift(systemNamespace + '._cst_Transaction'); + this.imports.unshift(systemNamespace + '.$Asset'); + this.imports.unshift(systemNamespace + '.$Participant'); + this.imports.unshift(systemNamespace + '.$Event'); + this.imports.unshift(systemNamespace + '.$Transaction'); decorate = true; } + // console.log('\n========\n Parsed AST for the model file '); // console.log(util.inspect(this.ast,{ depth: 7, colors: true, })); // console.log('\n========\n'); @@ -97,19 +98,19 @@ class ModelFile { let doDecorate = (thing.classExtension === null && decorate); if(thing.type === 'AssetDeclaration') { - if (doDecorate){thing.classExtension = { type: 'ClassExtension', class: { type: 'Identifier', name: '_cst_Asset' } }; } + if (doDecorate){thing.classExtension = { type: 'ClassExtension', class: { type: 'Identifier', name: '$Asset' } }; } this.declarations.push( new AssetDeclaration(this, thing) ); } else if(thing.type === 'TransactionDeclaration') { - if (doDecorate){thing.classExtension = { type: 'ClassExtension', class: { type: 'Identifier', name: '_cst_Transaction' } }; } + if (doDecorate){thing.classExtension = { type: 'ClassExtension', class: { type: 'Identifier', name: '$Transaction' } }; } this.declarations.push( new TransactionDeclaration(this, thing) ); } else if(thing.type === 'EventDeclaration') { - if (doDecorate){thing.classExtension = { type: 'ClassExtension', class: { type: 'Identifier', name: '_cst_Event' } }; } + if (doDecorate){thing.classExtension = { type: 'ClassExtension', class: { type: 'Identifier', name: '$Event' } }; } this.declarations.push( new EventDeclaration(this, thing) ); } else if(thing.type === 'ParticipantDeclaration') { - if (doDecorate){thing.classExtension = { type: 'ClassExtension', class: { type: 'Identifier', name: '_cst_Participant' } }; } + if (doDecorate){thing.classExtension = { type: 'ClassExtension', class: { type: 'Identifier', name: '$Participant' } }; } this.declarations.push( new ParticipantDeclaration(this, thing) ); } else if(thing.type === 'EnumDeclaration') { @@ -128,6 +129,7 @@ class ModelFile { } + } /** @@ -184,9 +186,9 @@ class ModelFile { const modelFile = this.getModelManager().getModelFile(importNamespace); if (!modelFile) { let formatter = Globalize.messageFormatter('modelmanager-gettype-noregisteredns'); - throw new Error(formatter({ + throw new IllegalModelException(formatter({ type: importName - })); + }), this); } if (ModelUtil.isWildcardName(importName)) { // This is a wildcard import, org.acme.* @@ -195,7 +197,11 @@ class ModelFile { } const importShortName = ModelUtil.getShortName(importName); if (!modelFile.isLocalType(importShortName)) { - throw new Error('No type ' + importShortName + ' in namespace ' + importNamespace); + let formatter = Globalize.messageFormatter('modelmanager-gettype-notypeinns'); + throw new IllegalModelException(formatter({ + type: importShortName, + namespace: importNamespace + }), this); } }); diff --git a/packages/composer-common/lib/introspect/relationshipdeclaration.js b/packages/composer-common/lib/introspect/relationshipdeclaration.js index 82a894fb10..8ed1b6923b 100644 --- a/packages/composer-common/lib/introspect/relationshipdeclaration.js +++ b/packages/composer-common/lib/introspect/relationshipdeclaration.js @@ -65,8 +65,12 @@ class RelationshipDeclaration extends Property { classDeclaration = this.getParent().getModelFile().getType(this.getType()); } else { - // otherwise we have to use the modelmanager to try to load - classDeclaration = this.getParent().getModelFile().getModelManager().getType(this.getFullyQualifiedTypeName()); + // otherwise we have to use the modelmanager to try to load + try { + classDeclaration = this.getParent().getModelFile().getModelManager().getType(this.getFullyQualifiedTypeName()); + } catch (err) { + // Let classDeclaration remain null and get handled below + } } if(classDeclaration === null) { diff --git a/packages/composer-common/lib/modelmanager.js b/packages/composer-common/lib/modelmanager.js index 9fde05ee58..ae62ef8d5a 100644 --- a/packages/composer-common/lib/modelmanager.js +++ b/packages/composer-common/lib/modelmanager.js @@ -14,29 +14,26 @@ 'use strict'; -const fs = require('fs'); -const path = require('path'); - const Globalize = require('./globalize'); const IllegalModelException = require('./introspect/illegalmodelexception'); const ModelUtil = require('./modelutil'); const ModelFile = require('./introspect/modelfile'); +const TypeNotFoundException = require('./typenotfoundexception'); // const ENCODING = 'utf8'; const LOG = require('./log/logger').getLog('ModelManager'); const SYSTEM_MODEL_CONTENTS = [ 'namespace org.hyperledger.composer.system', - 'abstract asset _cst_Asset { o String superNotes optional }', - 'abstract participant _cst_Participant { o String superNotes optional }', - 'abstract transaction _cst_Transaction identified by transactionId{', + 'abstract asset $Asset { }', + 'abstract participant $Participant { }', + 'abstract transaction $Transaction identified by transactionId{', ' o String transactionId', ' o DateTime timestamp', - ' o String superNotes optional', '}', - 'abstract event _cst_Event identified by eventId{', + 'abstract event $Event identified by eventId{', ' o String eventId', - ' --> _cst_Transaction transaction', + /* ' --> _cst_Transaction transaction',*/ ' }' ]; // const util = require('util'); @@ -71,7 +68,6 @@ class ModelManager { constructor() { LOG.entry('constructor'); this.modelFiles = {}; - LOG.info('info',fs); let systemModelContents = SYSTEM_MODEL_CONTENTS.join('\n'); LOG.info('info',systemModelContents); this.addModelFile(systemModelContents); @@ -220,9 +216,6 @@ class ModelManager { } } - // console.log(util.inspect(this.modelFiles,{ depth: 7 , colors: true, })); - - // re-validate all the model files for (let ns in this.modelFiles) { this.modelFiles[ns].validate(); @@ -271,41 +264,37 @@ class ModelManager { /** * Check that the type is valid and returns the FQN of the type. * @param {string} context - error reporting context - * @param {string} type - a short type name + * @param {string} type - fully qualified type name * @return {string} - the resolved type name (fully qualified) * @throws {IllegalModelException} - if the type is not defined * @private */ resolveType(context, type) { // is the type a primitive? - if (!ModelUtil.isPrimitiveType(type)) { - - let ns = ModelUtil.getNamespace(type); - let modelFile = this.getModelFile(ns); - - if (!modelFile) { - let formatter = Globalize.messageFormatter('modelmanager-resolvetype-nonsfortype'); - throw new IllegalModelException(formatter({ - type: type, - context: context - })); - } + if (ModelUtil.isPrimitiveType(type)) { + return type; + } - if (!modelFile.isLocalType(type)) { - let formatter = Globalize.messageFormatter('modelmanager-resolvetype-notypeinnsforcontext'); - throw new IllegalModelException(formatter({ - context: context, - type: type, - namespace: modelFile.getNamespace() - })); - } - else { - return type; - } + let ns = ModelUtil.getNamespace(type); + let modelFile = this.getModelFile(ns); + if (!modelFile) { + let formatter = Globalize.messageFormatter('modelmanager-resolvetype-nonsfortype'); + throw new IllegalModelException(formatter({ + type: type, + context: context + })); } - else { + + if (modelFile.isLocalType(type)) { return type; } + + let formatter = Globalize.messageFormatter('modelmanager-resolvetype-notypeinnsforcontext'); + throw new IllegalModelException(formatter({ + context: context, + type: type, + namespace: modelFile.getNamespace() + })); } /** @@ -338,35 +327,33 @@ class ModelManager { /** * Look up a type in all registered namespaces. * - * @param {string} type - the fully qualified name of a type - * @return {ClassDeclaration} - the class declaration or null for primitive types - * @throws {Error} - if the type cannot be found + * @param {string} qualifiedName - fully qualified type name. + * @return {ClassDeclaration} - the class declaration for the specified type. + * @throws {TypeNotFoundException} - if the type cannot be found or is a primitive type. * @private */ - getType(type) { - // is the type a primitive? - if (!ModelUtil.isPrimitiveType(type)) { - let ns = ModelUtil.getNamespace(type); - let modelFile = this.getModelFile(ns); - - if (!modelFile) { - let formatter = Globalize.messageFormatter('modelmanager-gettype-noregisteredns'); - throw new Error(formatter({ - type: type - })); - } + getType(qualifiedName) { - let classDecl = modelFile.getType(type); + const namespace = ModelUtil.getNamespace(qualifiedName); - if (!classDecl) { - throw new Error('No type ' + type + ' in namespace ' + modelFile.getNamespace()); - } - - return classDecl; + const modelFile = this.getModelFile(namespace); + if (!modelFile) { + const formatter = Globalize.messageFormatter('modelmanager-gettype-noregisteredns'); + throw new TypeNotFoundException(qualifiedName, formatter({ + type: qualifiedName + })); } - else { - return null; + + const classDecl = modelFile.getType(qualifiedName); + if (!classDecl) { + const formatter = Globalize.messageFormatter('modelmanager-gettype-notypeinns'); + throw new TypeNotFoundException(qualifiedName, formatter({ + type: ModelUtil.getShortName(qualifiedName), + namespace: namespace + })); } + + return classDecl; } /** diff --git a/packages/composer-common/lib/modelutil.js b/packages/composer-common/lib/modelutil.js index 5380b974e3..fe60972d20 100644 --- a/packages/composer-common/lib/modelutil.js +++ b/packages/composer-common/lib/modelutil.js @@ -145,6 +145,20 @@ class ModelUtil { return (typeDeclaration !== null && typeDeclaration.isEnum()); } + /** + * Get the fully qualified name of a type. + * @param {string} namespace - namespace of the type. + * @param {string} type - short name of the type. + * @returns {string} the fully qualified type name. + */ + static getFullyQualifiedName(namespace, type) { + if (namespace) { + return namespace + '.' + type; + } else { + return type; + } + } + } module.exports = ModelUtil; diff --git a/packages/composer-common/lib/query/orderby.js b/packages/composer-common/lib/query/orderby.js index d8334b7613..2675d694da 100644 --- a/packages/composer-common/lib/query/orderby.js +++ b/packages/composer-common/lib/query/orderby.js @@ -99,7 +99,11 @@ class OrderBy { */ toJSON() { let result = { - sortCriteria: this.sortCriteria.toJSON() + + sortCriteria: this.sortCriteria.map((sortCriteria) => { + return sortCriteria.toJSON(); + }) + }; return result; } diff --git a/packages/composer-common/lib/query/query.js b/packages/composer-common/lib/query/query.js index 61feff4348..e1956a0ce0 100644 --- a/packages/composer-common/lib/query/query.js +++ b/packages/composer-common/lib/query/query.js @@ -96,7 +96,7 @@ class Query { } /** - * Returns the description associated with this ACL Rule. + * Returns the description associated with this Query. * * @return {string} the description */ @@ -104,6 +104,14 @@ class Query { return this.description; } + /** + * Returns the select statement associated with this Query. + * @return {Select} The select statement. + */ + getSelect() { + return this.select; + } + /** * Returns a new object representing this Query that is * suitable for serializing as JSON. diff --git a/packages/composer-common/lib/serializer/jsonpopulator.js b/packages/composer-common/lib/serializer/jsonpopulator.js index 7bd6d6a87a..e19896aaa1 100644 --- a/packages/composer-common/lib/serializer/jsonpopulator.js +++ b/packages/composer-common/lib/serializer/jsonpopulator.js @@ -230,9 +230,6 @@ class JSONPopulator { } const classDeclaration = parameters.modelManager.getType(jsonItem.$class); - if(!classDeclaration) { - throw new Error( 'Failed to find type ' + jsonItem.$class + ' in ModelManager.' ); - } // create a new instance, using the identifier field name as the ID. let subResource = parameters.factory.newResource(classDeclaration.getModelFile().getNamespace(), @@ -258,9 +255,6 @@ class JSONPopulator { } const classDeclaration = parameters.modelManager.getType(jsonObj.$class); - if(!classDeclaration) { - throw new Error( 'Failed to find type ' + jsonObj.$class + ' in ModelManager.' ); - } // create a new instance, using the identifier field name as the ID. let subResource = parameters.factory.newResource(classDeclaration.getModelFile().getNamespace(), diff --git a/packages/composer-common/lib/serializer/resourcevalidator.js b/packages/composer-common/lib/serializer/resourcevalidator.js index 0bbac9a0aa..a49350e669 100644 --- a/packages/composer-common/lib/serializer/resourcevalidator.js +++ b/packages/composer-common/lib/serializer/resourcevalidator.js @@ -318,13 +318,13 @@ class ResourceValidator { // a field that points to a transaction, asset, participant... let classDeclaration = parameters.modelManager.getType(field.getFullyQualifiedTypeName()); if(obj instanceof Identifiable) { - classDeclaration = parameters.modelManager.getType(obj.getFullyQualifiedType()); - - if(!classDeclaration) { + try { + classDeclaration = parameters.modelManager.getType(obj.getFullyQualifiedType()); + } catch (err) { ResourceValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, propName, obj, field); } - // is it compatible? + // is it compatible? if(!ModelUtil.isAssignableTo(classDeclaration.getModelFile(), classDeclaration.getFullyQualifiedName(), field)) { ResourceValidator.reportInvalidFieldAssignment(parameters.rootResourceIdentifier, propName, obj, field); } diff --git a/packages/composer-common/lib/typenotfoundexception.js b/packages/composer-common/lib/typenotfoundexception.js new file mode 100644 index 0000000000..be6fcdbbac --- /dev/null +++ b/packages/composer-common/lib/typenotfoundexception.js @@ -0,0 +1,56 @@ +/* + * 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 BaseException = require('./baseexception'); +const Globalize = require('./globalize'); + +/** + * Error thrown when a Composer type does not exist. + * @extends BaseException + * @see See [BaseException]{@link module:composer-common.BaseException} + * @class + * @memberof module:composer-common + */ +class TypeNotFoundException extends BaseException { + /** + * Constructor. If the optional 'message' argument is not supplied, it will be set to a default value that + * includes the type name. + * @param {String} typeName - fully qualified type name. + * @param {String} [message] - error message. + */ + constructor(typeName, message) { + if (!message) { + const formatter = Globalize.messageFormatter('typenotfounderror-defaultmessage'); + message = formatter({ + typeName: typeName + }); + } + + super(message); + this.typeName = typeName; + } + + /** + * Get the name of the type that was not found. + * @returns {string} fully qualified type name. + */ + getTypeName() { + return this.typeName; + } + +} + +module.exports = TypeNotFoundException; \ No newline at end of file diff --git a/packages/composer-common/messages/en.json b/packages/composer-common/messages/en.json index 6b642f073c..d0065c9aab 100644 --- a/packages/composer-common/messages/en.json +++ b/packages/composer-common/messages/en.json @@ -8,8 +8,10 @@ " other {You and # others liked this}", "}" ], - "hello": "Hello, {0} {1} {2}", - "hey": "Hey, {first} {middle} {last}", + "test-hello-array": "Hello, {0} {1} {2}", + "test-hello-object": "Hello, {first} {middle} {last}", + "test-repeat-array": "{0} {0} {0}", + "test-repeat-object": "{value} {value} {value}", "classdeclaration-constructor-modelastreq": "ModelFile and AST are required to create a ClassDecl.", "classdeclaration-process-unrecmodelelem": "Unrecognised model element {type}", @@ -34,22 +36,17 @@ "concerto-login-noenrollmentsecret": "enrollmentSecret not specified", "concerto-deploy-nosecuritycontext": "securityContext not specified", - "factory-newinstance-notregisteredwithmm": "ModelFile for namespace {namespace} has not been registered with the ModelManager", - "factory-newinstance-typenotdeclaredinns": "Type {type} is not declared in namespace {namespace}", "factory-newinstance-missingidentifier": "Missing identifier for Type {type} in namespace {namespace}", "factory-newinstance-invalididentifier": "Invalid or missing identifier for Type {type} in namespace {namespace}", "factory-newinstance-abstracttype": "Cannot instantiate Abstract Type {type} in namespace {namespace}", - "factory-newrelationship-notregisteredwithmm": "ModelFile for namespace {namespace} has not been registered with the ModelManager", - "factory-newrelationship-typenotdeclaredinns": "Type {type} is not declared in namespace {namespace}", - "instancegenerator-newinstance-noconcreteclass": "No concrete extending type for {type}", "modelmanager-resolvetype-nonsfortype": "No registered namespace for type {type} in {context}", "modelmanager-resolvetype-notypeinnsforcontext": "No type {type} in namespace {namespace} for {context}", - "modelmanager-gettype-noregisteredns": "No registered namespace for type {type}", - "modelmanager-gettype-notypeinns": "No type {type} in namespace {namespace}", + "modelmanager-gettype-noregisteredns": "Namespace is not defined for type {type}", + "modelmanager-gettype-notypeinns": "Type {type} is not defined in namespace {namespace}", "serializer-constructor-factorynull": "Factory cannot be null", "serializer-constructor-modelmanagernull": "ModelManager cannot be null", @@ -68,6 +65,8 @@ "resourcevalidator-abstractclass": "The class {className} is abstract. Should not have an instance!", "resourcevalidator-undeclaredfield": "Instance {resourceId} has a property named {propertyName} which is not declared in {fullyQualifiedTypeName}", "resourcevalidator-invalidfieldassignment": "Instance {resourceId} has property {propertyName} with type {objectType} that is not derived from {fieldType}", - "resourcevalidator-emptyidentifier": "Instance {resourceId} has an empty identifier." + "resourcevalidator-emptyidentifier": "Instance {resourceId} has an empty identifier.", + + "typenotfounderror-defaultmessage": "Type not found: {typeName}" } } diff --git a/packages/composer-common/test/businessnetworkdefinition.js b/packages/composer-common/test/businessnetworkdefinition.js index 6ea59e80dc..182d64aa1f 100644 --- a/packages/composer-common/test/businessnetworkdefinition.js +++ b/packages/composer-common/test/businessnetworkdefinition.js @@ -98,12 +98,12 @@ describe('BusinessNetworkDefinition', () => { businessNetwork.getDescription().should.equal('A test business network.'); businessNetwork.getMetadata().getREADME().should.equal('This is a test'); businessNetwork.getMetadata().getPackageJson().customKey.should.equal('custom value'); - Object.keys(businessNetwork.modelManager.modelFiles).should.have.length(3); + Object.keys(businessNetwork.modelManager.modelFiles).should.have.length(4); Object.keys(businessNetwork.scriptManager.scripts).should.have.length(2); businessNetwork.aclManager.getAclRules().should.have.length(5); const intro = businessNetwork.getIntrospector(); - intro.getClassDeclarations().length.should.equal(25); + intro.getClassDeclarations().length.should.equal(29); const sm = businessNetwork.getScriptManager(); sm.getScripts().length.should.equal(2); }); @@ -113,11 +113,11 @@ describe('BusinessNetworkDefinition', () => { return BusinessNetworkDefinition.fromDirectory(__dirname + '/data/zip/test-archive-dotfolders').then(businessNetwork => { businessNetwork.should.be.BusinessNetworkDefinition; - Object.keys(businessNetwork.modelManager.modelFiles).should.have.length(3); + Object.keys(businessNetwork.modelManager.modelFiles).should.have.length(4); Object.keys(businessNetwork.scriptManager.scripts).should.have.length(2); const intro = businessNetwork.getIntrospector(); - intro.getClassDeclarations().length.should.equal(25); + intro.getClassDeclarations().length.should.equal(29); const sm = businessNetwork.getScriptManager(); sm.getScripts().length.should.equal(2); }); @@ -139,13 +139,13 @@ describe('BusinessNetworkDefinition', () => { businessNetwork.getDescription().should.equal('A test business network using npm model dependencies.'); - Object.keys(businessNetwork.modelManager.modelFiles).should.have.length(2); + Object.keys(businessNetwork.modelManager.modelFiles).should.have.length(3); Object.keys(businessNetwork.scriptManager.scripts).should.have.length(2); const intro = businessNetwork.getIntrospector(); - intro.getClassDeclarations().length.should.equal(13); - businessNetwork.getModelManager().getModelFiles().length.should.equal(2); + intro.getClassDeclarations().length.should.equal(17); + businessNetwork.getModelManager().getModelFiles().length.should.equal(3); const sm = businessNetwork.getScriptManager(); sm.getScripts().length.should.equal(2); }); @@ -162,7 +162,7 @@ describe('BusinessNetworkDefinition', () => { businessNetwork.getIdentifier().should.equal('test-archive@0.0.1'); businessNetwork.getDescription().should.equal('A test business network.'); businessNetwork.getMetadata().getREADME().should.equal('This is a test'); - Object.keys(businessNetwork.modelManager.modelFiles).should.have.length(3); + Object.keys(businessNetwork.modelManager.modelFiles).should.have.length(4); Object.keys(businessNetwork.scriptManager.scripts).should.have.length(2); return businessNetwork.toArchive().then(buffer => { diff --git a/packages/composer-common/test/codegen/loopbackvisitor.js b/packages/composer-common/test/codegen/loopbackvisitor.js index 91a5830451..a494b1b9b2 100644 --- a/packages/composer-common/test/codegen/loopbackvisitor.js +++ b/packages/composer-common/test/codegen/loopbackvisitor.js @@ -570,10 +570,6 @@ describe('LoopbackVisitor', () => { const modelFile = new ModelFile(modelManager, ` namespace org.acme transaction MyBaseTransaction { -<<<<<<< HEAD - -======= ->>>>>>> eb4143c0b619ffef099f1e12c400a279a16a3ecc } transaction MyTransaction extends MyBaseTransaction { o String theValue @@ -673,10 +669,6 @@ describe('LoopbackVisitor', () => { const modelFile = new ModelFile(modelManager, ` namespace org.acme abstract transaction MyBaseTransaction { -<<<<<<< HEAD - -======= ->>>>>>> eb4143c0b619ffef099f1e12c400a279a16a3ecc } transaction MyTransaction extends MyBaseTransaction { o String theValue diff --git a/packages/composer-common/test/concerto/i18n.js b/packages/composer-common/test/concerto/i18n.js index d8b8cb532d..289afd044c 100644 --- a/packages/composer-common/test/concerto/i18n.js +++ b/packages/composer-common/test/concerto/i18n.js @@ -30,8 +30,34 @@ describe('Globalization', function() { beforeEach(function() { }); - describe.skip('#check globalization library works', function() { - it('check formatters', function() { + describe('#check globalization library works', function() { + it('numbered variables using Array', function() { + const formatter = Globalize('en').messageFormatter('test-hello-array'); + formatter([ 'Wolfgang', 'Amadeus', 'Mozart' ]).should.equal('Hello, Wolfgang Amadeus Mozart'); + }); + + it('named variables using Object key-value pairs', function() { + const formatter = Globalize('en').messageFormatter('test-hello-object'); + formatter({ + first: 'Wolfgang', + middle: 'Amadeus', + last: 'Mozart' + }).should.equal('Hello, Wolfgang Amadeus Mozart'); + }); + + it('repeated numbered variables using Array', function() { + const formatter = Globalize('en').messageFormatter('test-repeat-array'); + formatter([ 'Bonkers' ]).should.equal('Bonkers Bonkers Bonkers'); + }); + + it('repeated named variables using Object key-value pairs', function() { + const formatter = Globalize('en').messageFormatter('test-repeat-object'); + formatter({ + value: 'Bonkers' + }).should.equal('Bonkers Bonkers Bonkers'); + }); + + it.skip('check formatters', function() { // Use Globalize to format a message with plural inflection. let like = Globalize.messageFormatter('like'); like(0).should.equal('Be the first to like this'); @@ -196,7 +222,7 @@ describe('Globalization', function() { }); }); - describe('#check Factory messages are correct', function() { + describe.skip('#check Factory messages are correct', function() { describe('check messages in newInstance()', function() { it('where namespace hasn\'t been registered in the model manager', function() { let formatter = Globalize.messageFormatter('factory-newinstance-notregisteredwithmm'); @@ -306,7 +332,7 @@ describe('Globalization', function() { }); }); - describe('#check ModelManager messages are correct', function() { + describe('ModelManager messages', function() { describe('check messages in resolveType()', function() { it('when no namespace is registered for type', function(){ let formatter = Globalize.messageFormatter('modelmanager-resolvetype-nonsfortype'); @@ -334,26 +360,26 @@ describe('Globalization', function() { }); }); - describe('check messages in getType()', function() { - it('check no type in namespace', function() { + describe('#getType()', function() { + it('Namespace does not exist', function() { let formatter = Globalize.messageFormatter('modelmanager-gettype-noregisteredns'); formatter({ - type: 'foo' - }).should.equal('No registered namespace for type foo'); + type: 'TYPE' + }).should.equal('Namespace is not defined for type TYPE'); const modelManager = new ModelManager(); expect(function() { - modelManager.getType('foo'); - }).to.throw(Error, 'No registered namespace for type foo'); + modelManager.getType('NAMESPACE.TYPE'); + }).to.throw(Error, 'Namespace is not defined for type NAMESPACE.TYPE'); }); it('check type exists in namespace', function() { let formatter = Globalize.messageFormatter('modelmanager-gettype-notypeinns'); formatter({ - type: 'foo', - namespace: 'bar' - }).should.equal('No type foo in namespace bar'); + type: 'TYPE', + namespace: 'NAMESPACE' + }).should.equal('Type TYPE is not defined in namespace NAMESPACE'); // Cannot be tested - will throw error on line 139 modelmanager.js first }); diff --git a/packages/composer-common/test/data/model/carlease.cto b/packages/composer-common/test/data/model/carlease.cto index cb8752293f..7a47e24394 100644 --- a/packages/composer-common/test/data/model/carlease.cto +++ b/packages/composer-common/test/data/model/carlease.cto @@ -78,7 +78,7 @@ participant Regulator extends Participant { // defines a Vehicle transaction type transaction VehicleTransaction { - + --> Vehicle vehicle // a VehicleTransaction is related to a Vehicle } @@ -108,6 +108,6 @@ transaction VehicleTransferredToScrapMerchant extends VehicleTransaction{ transaction VehicleScrapped extends VehicleTransaction{ } -event TestEvent identified by eventId { - o String eventId +event TestEvent { + } diff --git a/packages/composer-common/test/data/model/dependencies2/core.cto b/packages/composer-common/test/data/model/dependencies2/core.cto index aa85d806ea..e414039277 100644 --- a/packages/composer-common/test/data/model/dependencies2/core.cto +++ b/packages/composer-common/test/data/model/dependencies2/core.cto @@ -4,7 +4,6 @@ participant BaseMember identified by id { o String id } -transaction BaseTransaction identified by id { - o String id +transaction BaseTransaction { --> BaseMember invoker } diff --git a/packages/composer-common/test/data/zip/test-archive.zip b/packages/composer-common/test/data/zip/test-archive.zip index 0c18da48ed..8389bd5bdb 100644 Binary files a/packages/composer-common/test/data/zip/test-archive.zip and b/packages/composer-common/test/data/zip/test-archive.zip differ diff --git a/packages/composer-common/test/factory.js b/packages/composer-common/test/factory.js index ac3cfb60c5..da015a70ce 100644 --- a/packages/composer-common/test/factory.js +++ b/packages/composer-common/test/factory.js @@ -16,6 +16,7 @@ const Factory = require('../lib/factory'); const ModelManager = require('../lib/modelmanager'); +const TypeNotFoundException = require('../lib/typenotfoundexception'); const uuid = require('uuid'); const should = require('chai').should(); @@ -141,13 +142,13 @@ describe('Factory', () => { it('should throw if namespace missing', () => { (() => { factory.newConcept('org.acme.missing', 'MyConcept'); - }).should.throw(/ModelFile for namespace org.acme.missing has not been registered with the ModelManager/); + }).should.throw(TypeNotFoundException); }); it('should throw if Concept missing', () => { (() => { factory.newConcept('org.acme.test', 'MissingConcept'); - }).should.throw(/Type MissingConcept is not declared in namespace org.acme.test/); + }).should.throw(TypeNotFoundException); }); it('should throw if concept is abstract', () => { @@ -176,6 +177,23 @@ describe('Factory', () => { }); + describe('#newRelationship', function() { + it('should throw if namespace missing', function() { + (() => factory.newRelationship('org.acme.missing', 'MyAsset', 'id')). + should.throw(TypeNotFoundException, /org.acme.missing/); + }); + + it('should throw if type missing', function() { + (() => factory.newRelationship('org.acme.test', 'MissingType', 'id')). + should.throw(TypeNotFoundException, /MissingType/); + }); + + it('should succeed for a valid type', function() { + const relationship = factory.newRelationship('org.acme.test', 'MyAsset', 'id'); + relationship.isRelationship().should.be.true; + }); + }); + describe('#newTransaction', () => { it('should throw if ns not specified', () => { diff --git a/packages/composer-common/test/introspect/assetdeclaration.js b/packages/composer-common/test/introspect/assetdeclaration.js index 6700723b68..5ce520df86 100644 --- a/packages/composer-common/test/introspect/assetdeclaration.js +++ b/packages/composer-common/test/introspect/assetdeclaration.js @@ -15,6 +15,7 @@ 'use strict'; const AssetDeclaration = require('../../lib/introspect/assetdeclaration'); +const ClassDeclaration = require('../../lib/introspect/classdeclaration'); const ModelFile = require('../../lib/introspect/modelfile'); const ModelManager = require('../../lib/modelmanager'); const fs = require('fs'); @@ -25,11 +26,15 @@ const sinon = require('sinon'); describe('AssetDeclaration', () => { let mockModelManager; + let mockClassDeclaration; let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); - mockModelManager = sinon.createStubInstance(ModelManager); + mockModelManager = sinon.createStubInstance(ModelManager); + mockClassDeclaration = sinon.createStubInstance(ClassDeclaration); + mockModelManager.getType.returns(mockClassDeclaration); + mockClassDeclaration.getProperties.returns([]); }); afterEach(() => { @@ -41,6 +46,7 @@ describe('AssetDeclaration', () => { let modelFile = new ModelFile(mockModelManager, modelDefinitions); let assets = modelFile.getAssetDeclarations(); assets.should.have.lengthOf(1); + return assets[0]; }; @@ -107,6 +113,7 @@ describe('AssetDeclaration', () => { it('should throw when field has been duplicated in the same class', () => { let asset = loadAssetDeclaration('test/data/parser/assetdeclaration.dupesimp.cto'); + (() => { asset.validate(); }).should.throw(/more than one field named/); diff --git a/packages/composer-common/test/introspect/functiondeclaration.js b/packages/composer-common/test/introspect/functiondeclaration.js index cc188e6d6b..5a4bc8fad7 100644 --- a/packages/composer-common/test/introspect/functiondeclaration.js +++ b/packages/composer-common/test/introspect/functiondeclaration.js @@ -128,7 +128,7 @@ describe('FunctionDeclaration', () => { (() => { let func = loadFunctionDeclaration('test/data/parser/functiondeclaration.missingtx.js'); func.validate(); - }).should.throw(/No type org.acme.TestTransactionLulz/); + }).should.throw(/TestTransactionLulz/); }); it('should throw if the function refers to a transaction that is not a transaction', () => { diff --git a/packages/composer-common/test/introspect/modelfile.js b/packages/composer-common/test/introspect/modelfile.js index 8fd5b31f72..c2a08d29ac 100644 --- a/packages/composer-common/test/introspect/modelfile.js +++ b/packages/composer-common/test/introspect/modelfile.js @@ -15,9 +15,11 @@ 'use strict'; const AssetDeclaration = require('../../lib/introspect/assetdeclaration'); +const ClassDeclaration = require('../../lib/introspect/classdeclaration'); const ParticipantDeclaration = require('../../lib/introspect/participantdeclaration'); const TransactionDeclaration = require('../../lib/introspect/transactiondeclaration'); const EventDeclaration = require('../../lib/introspect/eventdeclaration'); +const IllegalModelException = require('../../lib/introspect/illegalmodelexception'); const ModelFile = require('../../lib/introspect/modelfile'); const ModelManager = require('../../lib/modelmanager'); const ParseException = require('../../lib/introspect/parseexception'); @@ -33,18 +35,32 @@ describe('ModelFile', () => { const carLeaseModel = fs.readFileSync(path.resolve(__dirname, '../data/model/carlease.cto'), 'utf8'); let mockModelManager; + let mockClassDeclaration; let sandbox; beforeEach(() => { - const mockSystemModel = ` - namespace org.hyperledger.composer.system - abstract asset Asset identified by assetId { - o String assetId - }`; + + + const SYSTEM_MODEL_CONTENTS = [ + 'namespace org.hyperledger.composer.system', + 'abstract asset $Asset { }', + 'abstract participant $Participant { }', + 'abstract transaction $Transaction identified by transactionId{', + ' o String transactionId', + ' o DateTime timestamp', + '}', + 'abstract event $Event identified by eventId{', + ' o String eventId', + /* ' --> _cst_Transaction transaction',*/ + ' }' + ]; + const mockSystemModel = SYSTEM_MODEL_CONTENTS.join('\n'); let mockSystemModelFile = new ModelFile(mockModelManager, mockSystemModel); mockModelManager = sinon.createStubInstance(ModelManager); mockModelManager.getModelFile.withArgs('org.hyperledger.composer.system').returns(mockSystemModelFile); - + mockClassDeclaration = sinon.createStubInstance(ClassDeclaration); + mockModelManager.getType.returns(mockClassDeclaration); + mockClassDeclaration.getProperties.returns([]); sandbox = sinon.sandbox.create(); }); @@ -186,7 +202,7 @@ describe('ModelFile', () => { let modelFile = new ModelFile(mockModelManager, model); (() => { modelFile.validate(); - }).should.throw(/No registered namespace for type org.acme.ext.MyAsset2/); + }).should.throw(IllegalModelException, /org.acme.ext/); }); it('should throw if a wildcard import exists for an invalid namespace', () => { @@ -199,7 +215,7 @@ describe('ModelFile', () => { let modelFile = new ModelFile(mockModelManager, model); (() => { modelFile.validate(); - }).should.throw(/No registered namespace for type org.acme.ext.\*/); + }).should.throw(IllegalModelException, /org.acme.ext/); }); it('should throw if an import exists for a type that does not exist in a valid namespace', () => { @@ -219,7 +235,7 @@ describe('ModelFile', () => { let modelFile2 = new ModelFile(mockModelManager, model2); (() => { modelFile2.validate(); - }).should.throw(/No type MyAsset3 in namespace org.acme.ext/); + }).should.throw(IllegalModelException, /MyAsset3/); }); it('should not throw if an import exists for a type that exists in a valid namespace', () => { @@ -237,7 +253,7 @@ describe('ModelFile', () => { let modelFile1 = new ModelFile(mockModelManager, model1); mockModelManager.getModelFile.withArgs('org.acme.ext').returns(modelFile1); let modelFile2 = new ModelFile(mockModelManager, model2); - modelFile2.validate(); + (() => modelFile2.validate()).should.not.throw(); }); it('should not throw if a wildcard import exists for a valid namespace', () => { @@ -255,7 +271,7 @@ describe('ModelFile', () => { let modelFile1 = new ModelFile(mockModelManager, model1); mockModelManager.getModelFile.withArgs('org.acme.ext').returns(modelFile1); let modelFile2 = new ModelFile(mockModelManager, model2); - modelFile2.validate(); + (() => modelFile2.validate()).should.not.throw(); }); }); @@ -373,7 +389,7 @@ describe('ModelFile', () => { const model = ` namespace org.acme`; let modelFile = new ModelFile(mockModelManager, model); - modelFile.resolveImport('Asset').should.equal('org.hyperledger.composer.system.Asset'); + modelFile.resolveImport('$Asset').should.equal('org.hyperledger.composer.system.$Asset'); }); it('should find the fully qualified name of the import', () => { diff --git a/packages/composer-common/test/introspect/relationshipdeclaration.js b/packages/composer-common/test/introspect/relationshipdeclaration.js index 75f4e9b807..7a1ed0478a 100644 --- a/packages/composer-common/test/introspect/relationshipdeclaration.js +++ b/packages/composer-common/test/introspect/relationshipdeclaration.js @@ -63,10 +63,9 @@ describe('RelationshipDeclaration', function () { it('should allow a relationship to a transaction in an event', () => { const model = `namespace ${ModelUtil.getSystemNamespace()} - transaction MyTransaction identified by transactionId{ - o String transactionId + transaction MyTransaction{ } - + event MyEvent identified by eventId { o String eventId --> MyTransaction Transaction diff --git a/packages/composer-common/test/modelmanager.js b/packages/composer-common/test/modelmanager.js index 72c3ec6c8f..182348e552 100644 --- a/packages/composer-common/test/modelmanager.js +++ b/packages/composer-common/test/modelmanager.js @@ -19,6 +19,7 @@ const EnumDeclaration = require('../lib/introspect/enumdeclaration'); const ModelFile = require('../lib/introspect/modelfile'); const ModelManager = require('../lib/modelmanager'); const ParticipantDeclaration = require('../lib/introspect/participantdeclaration'); +const TypeNotFoundException = require('../lib/typenotfoundexception'); const TransactionDeclaration = require('../lib/introspect/transactiondeclaration'); const fs = require('fs'); @@ -368,6 +369,41 @@ describe('ModelManager', () => { }); + describe('#getType', function() { + it('should throw an error for a primitive type', function() { + modelManager.addModelFile(modelBase); + (function() { + modelManager.getType('String'); + }).should.throw(TypeNotFoundException); + }); + + it('should throw an error for a namespace that does not exist', function() { + modelManager.addModelFile(modelBase); + (function() { + modelManager.getType('org.acme.nosuchns.SimpleAsset'); + }).should.throw(TypeNotFoundException, /org.acme.nosuchns/); + }); + + it('should throw an error for an empty namespace', function() { + modelManager.addModelFile(modelBase); + (function() { + modelManager.getType('NoSuchAsset'); + }).should.throw(TypeNotFoundException, /NoSuchAsset/); + }); + + it('should throw an error for a type that does not exist', function() { + modelManager.addModelFile(modelBase); + (function() { + modelManager.getType('org.acme.base.NoSuchAsset'); + }).should.throw(TypeNotFoundException, /NoSuchAsset/); + }); + + it('should return the class declaration for a valid type', function() { + modelManager.addModelFile(modelBase); + const declaration = modelManager.getType('org.acme.base.AbstractAsset'); + declaration.getFullyQualifiedName().should.equal('org.acme.base.AbstractAsset'); + }); + }); describe('#toJSON', () => { diff --git a/packages/composer-common/test/models/dependencies2.js b/packages/composer-common/test/models/dependencies2.js index 9514d959dd..5055e910f7 100644 --- a/packages/composer-common/test/models/dependencies2.js +++ b/packages/composer-common/test/models/dependencies2.js @@ -45,7 +45,6 @@ describe('Dependencies2 Model', function() { // const participant = factory.newResource('org.acme.base', 'ClientAdminMember', 'testadmin'); const transaction = factory.newTransaction('org.acme.core', 'BaseTransaction', 'testing'); - console.log(transaction); transaction.invoker = factory.newRelationship('org.acme.base', 'ClientAdminMember', 'testadmin'); transaction.validate(); const jsonObj = serializer.toJSON(transaction); diff --git a/packages/composer-common/test/models/test.js b/packages/composer-common/test/models/test.js index ce56ac3ce5..9f121c55d8 100644 --- a/packages/composer-common/test/models/test.js +++ b/packages/composer-common/test/models/test.js @@ -18,6 +18,7 @@ const Factory = require('../../lib/factory'); const ModelManager = require('../../lib/modelmanager'); const RelationshipDeclaration = require('../../lib/introspect/relationshipdeclaration'); const Serializer = require('../../lib/serializer'); +const TypeNotFoundException = require('../../lib/typenotfoundexception'); const fs = require('fs'); require('chai').should(); @@ -249,7 +250,7 @@ describe('Test Model', function(){ let vehicleDecl = modelManager.getType('org.acme.Vehicle'); vehicleDecl.should.not.be.null; vehicleDecl.getFullyQualifiedName().should.equal('org.acme.Vehicle'); - (modelManager.getType('String') === null).should.be.true; + (() => { modelManager.getType('String'); }).should.throw(TypeNotFoundException); modelManager.getType('org.acme.Base').getFullyQualifiedName().should.equal('org.acme.Base'); modelManager.getType('concerto.Participant').getName().should.equal('Participant'); diff --git a/packages/composer-common/test/modelutil.js b/packages/composer-common/test/modelutil.js index 8672f3d7cc..bbf01f96a4 100644 --- a/packages/composer-common/test/modelutil.js +++ b/packages/composer-common/test/modelutil.js @@ -156,4 +156,17 @@ describe('ModelUtil', function () { }); + describe('#getFullyQualifiedName', function() { + it('valid inputs', function() { + const result = ModelUtil.getFullyQualifiedName('a.namespace', 'type'); + result.should.equal('a.namespace.type'); + }); + + it('empty namespace should return the type with no leading dot', function() { + const result = ModelUtil.getFullyQualifiedName('', 'type'); + result.should.equal('type'); + }); + + }); + }); diff --git a/packages/composer-common/test/query/model.cto b/packages/composer-common/test/query/model.cto index 505c03a97d..61eacb523e 100644 --- a/packages/composer-common/test/query/model.cto +++ b/packages/composer-common/test/query/model.cto @@ -23,6 +23,6 @@ participant Regulator identified by email { o String lastName } -transaction Transaction identified by transactionId { - o String transactionId +transaction Transaction { + } diff --git a/packages/composer-common/test/serializer.js b/packages/composer-common/test/serializer.js index d811979b10..3b36e9a891 100644 --- a/packages/composer-common/test/serializer.js +++ b/packages/composer-common/test/serializer.js @@ -19,6 +19,7 @@ const ModelManager = require('../lib/modelmanager'); const Relationship = require('../lib/model/relationship'); const Resource = require('../lib/model/resource'); const Serializer = require('../lib/serializer'); +const TypeNotFoundException = require('../lib/typenotfoundexception'); require('chai').should(); const sinon = require('sinon'); @@ -97,7 +98,7 @@ describe('Serializer', () => { mockResource.getFullyQualifiedType.returns('org.acme.sample.NoSuchAsset'); (() => { serializer.toJSON(mockResource); - }).should.throw(/No type/); + }).should.throw(TypeNotFoundException, /NoSuchAsset/); }); it('should validate if the validate flag is set to false', () => { @@ -162,7 +163,7 @@ describe('Serializer', () => { let serializer = new Serializer(factory, modelManager); (() => { serializer.fromJSON(mockResource); - }).should.throw(/No type/); + }).should.throw(TypeNotFoundException, /NoSuchAsset/); }); it('should deserialize a valid asset', () => { diff --git a/packages/composer-common/test/serializer/jsonpopulator.js b/packages/composer-common/test/serializer/jsonpopulator.js index fc8831fedb..66f631c8d0 100644 --- a/packages/composer-common/test/serializer/jsonpopulator.js +++ b/packages/composer-common/test/serializer/jsonpopulator.js @@ -21,6 +21,7 @@ const ModelManager = require('../../lib/modelmanager'); const Relationship = require('../../lib/model/relationship'); const Resource = require('../../lib/model/resource'); const TypedStack = require('../../lib/serializer/typedstack'); +const TypeNotFoundException = require('../../lib/typenotfoundexception'); require('chai').should(); const sinon = require('sinon'); @@ -189,7 +190,7 @@ describe('JSONPopulator', () => { $class: 'org.acme.NOTAREALTYPE', assetId: 'asset1' }, options); - }).should.throw(/No type/); + }).should.throw(TypeNotFoundException, /NOTAREALTYPE/); }); it('should create a new resource from an object using a $class value that matches the model', () => { @@ -345,7 +346,7 @@ describe('JSONPopulator', () => { }; (() => { jsonPopulator.visitRelationshipDeclaration(relationshipDeclaration1, options); - }).should.throw(/No type/); + }).should.throw(TypeNotFoundException, /NoSuchClass/); }); it('should throw if the JSON data is an object with a class that does not exist', () => { @@ -358,10 +359,10 @@ describe('JSONPopulator', () => { factory: mockFactory, modelManager: modelManager }; - sandbox.stub(modelManager, 'getType').withArgs('org.acme.NoSuchClass').returns(null); + sandbox.stub(modelManager, 'getType').withArgs('org.acme.NoSuchClass').throws(new TypeNotFoundException('org.acme.NoSuchClass')); (() => { jsonPopulator.visitRelationshipDeclaration(relationshipDeclaration1, options); - }).should.throw(/Failed to find type/); + }).should.throw(TypeNotFoundException, /NoSuchClass/); }); it('should create a new relationship from an array of strings', () => { @@ -464,7 +465,7 @@ describe('JSONPopulator', () => { }; (() => { jsonPopulator.visitRelationshipDeclaration(relationshipDeclaration2, options); - }).should.throw(/No type/); + }).should.throw(TypeNotFoundException, /NoSuchClass/); }); it('should throw if the JSON data in the array is an object with a class that does not exist', () => { @@ -477,10 +478,10 @@ describe('JSONPopulator', () => { factory: mockFactory, modelManager: modelManager }; - sandbox.stub(modelManager, 'getType').withArgs('org.acme.NoSuchClass').returns(null); + sandbox.stub(modelManager, 'getType').withArgs('org.acme.NoSuchClass').throws(new TypeNotFoundException('org.acme.NoSuchClass')); (() => { jsonPopulator.visitRelationshipDeclaration(relationshipDeclaration2, options); - }).should.throw(/Failed to find type/); + }).should.throw(TypeNotFoundException, /NoSuchClass/); }); }); diff --git a/packages/composer-common/test/serializer/resourcevalidator.js b/packages/composer-common/test/serializer/resourcevalidator.js index 058af6bbba..7f56bba505 100644 --- a/packages/composer-common/test/serializer/resourcevalidator.js +++ b/packages/composer-common/test/serializer/resourcevalidator.js @@ -17,6 +17,7 @@ const ModelManager = require('../../lib/modelmanager'); const Factory = require('../../lib/factory'); const TypedStack = require('../../lib/serializer/typedstack'); +const TypeNotFoundException = require('../../lib/typenotfoundexception'); const ResourceValidator = require('../../lib/serializer/resourcevalidator'); const chai = require('chai'); @@ -311,8 +312,8 @@ describe('ResourceValidator', function () { modelManager.deleteModelFile('org.acme.l1'); (function () { - assetDeclaration.accept(resourceValidator,parameters ); - }).should.throw(/No registered namespace for type org.acme.l1.Base/); + assetDeclaration.accept(resourceValidator, parameters); + }).should.throw(TypeNotFoundException, /org.acme.l1/); }); it('should detect assigning to a missing type', function () { @@ -325,8 +326,8 @@ describe('ResourceValidator', function () { modelManager.deleteModelFile('org.acme.l3'); (function () { - assetDeclaration.accept(resourceValidator,parameters ); - }).should.throw(/No registered namespace for type org.acme.l3.Car/); + assetDeclaration.accept(resourceValidator, parameters); + }).should.throw(TypeNotFoundException, /org.acme.l3/); }); it('should detect assigning to an abstract type', function () { diff --git a/packages/composer-common/test/typenotfoundexception.js b/packages/composer-common/test/typenotfoundexception.js new file mode 100644 index 0000000000..9da71197aa --- /dev/null +++ b/packages/composer-common/test/typenotfoundexception.js @@ -0,0 +1,45 @@ +/* + * 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 TypeNotFoundException = require('../lib/typenotfoundexception'); + +const chai = require('chai'); +chai.should(); + +describe('TypeNotFoundException', function() { + describe('#constructor', function() { + it('with one argument', function() { + const typeName = 'namespace.TypeName'; + const obj = new TypeNotFoundException(typeName); + obj.toString().should.include(typeName); + }); + + it('with two arguments', function() { + const message = 'MESSAGE_TEXT'; + const obj = new TypeNotFoundException('namespace.TypeName', message); + obj.toString().should.include(message); + }); + }); + + describe('#getTypeName', function() { + it('should return the type', function() { + const typeName = 'namespace.TypeName'; + const obj = new TypeNotFoundException(typeName); + obj.getTypeName().should.equal(typeName); + }); + }); + +}); diff --git a/packages/composer-playground/src/app/app.module.ts b/packages/composer-playground/src/app/app.module.ts index 973641936f..47c5575f16 100644 --- a/packages/composer-playground/src/app/app.module.ts +++ b/packages/composer-playground/src/app/app.module.ts @@ -67,6 +67,7 @@ import { SampleBusinessNetworkService } from './services/samplebusinessnetwork.s import { AboutService } from './services/about.service'; import { AlertService } from './services/alert.service'; import { EditorService } from './services/editor.service'; +import { ScrollToElementDirective } from './directives/scroll'; let actionBasedIcons = require.context('../assets/svg/action-based', false, /.*\.svg$/); actionBasedIcons.keys().forEach(actionBasedIcons); @@ -145,6 +146,7 @@ type StoreType = { RegistryComponent, ReplaceComponent, ResourceComponent, + ScrollToElementDirective, SuccessComponent, SwitchIdentityComponent, TestComponent, diff --git a/packages/composer-playground/src/app/directives/scroll/index.ts b/packages/composer-playground/src/app/directives/scroll/index.ts new file mode 100644 index 0000000000..954325e72c --- /dev/null +++ b/packages/composer-playground/src/app/directives/scroll/index.ts @@ -0,0 +1 @@ +export * from './scroll-to-element.directive'; diff --git a/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.spec.ts b/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.spec.ts new file mode 100644 index 0000000000..9493f0da36 --- /dev/null +++ b/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.spec.ts @@ -0,0 +1,182 @@ +/* tslint:disable:no-unused-variable */ +/* tslint:disable:no-unused-expression */ +/* tslint:disable:no-var-requires */ +/* tslint:disable:max-classes-per-file */ +import { ComponentFixture, TestBed, async, fakeAsync, tick, inject } from '@angular/core/testing'; +import { Component, Renderer, QueryList, ElementRef } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +import * as sinon from 'sinon'; +import * as chai from 'chai'; +import { ScrollToElementDirective } from './scroll-to-element.directive'; + +let should = chai.should(); + +@Component({ + selector: 'editorFileList', + template: ` +
+ +
` +}) + +class TestComponent { + listItem: string = 'editorFileList1'; +} + +describe('ScrollToElementDirective', () => { + let component: TestComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent, ScrollToElementDirective], + providers: [Renderer] + }) + .compileComponents(); + })); + + it('should create the directive', async(fakeAsync(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + tick(); + + component.should.be.ok; + }))); + + describe('#retreiveSelectedItem', () => { + + it('should return an array of nativeElement.id\'s that match the selected', () => { + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + let matchItem: ElementRef[] = directiveInstance.retreiveSelectedItem(); + matchItem[0].nativeElement.id.should.equal('editorFileList1'); + }); + + it('should return an empty array if no matches for nativeElement.id\'s', () => { + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + directiveInstance.items = new QueryList(); + + let emptyList = new QueryList(); + let matchItem: ElementRef[] = directiveInstance.retreiveSelectedItem(); + matchItem.should.be.empty; + }); + }); + + describe('#performScrollAction', () => { + + it('should call stepVerticalScoll', async(fakeAsync(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + let stepSpy = sinon.spy(directiveInstance, 'stepVerticalScoll'); + + fixture.detectChanges(); + directiveInstance.performScrollAction(); + + // Big fake step to ensure all setTimeout actinos completed + tick(1000); + + // Check initial call based on test information was correct + stepSpy.getCall(0).args[0].should.equal(0.08, 0); + }))); + + it('should not action if no items matched for selected element', () => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + let stepSpy = sinon.spy(directiveInstance, 'stepVerticalScoll'); + + directiveInstance.items = new QueryList(); + + directiveInstance.performScrollAction(); + + // Check no call + stepSpy.should.not.have.been.called; + }); + }); + + describe('#isOvershoot', () => { + + it('should detect positive overshoot', () => { + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + directiveInstance.isOvershoot(1, 10, 1).should.be.true; + }); + + it('should detect negative overshoot', () => { + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + directiveInstance.isOvershoot(-1, -10, -1).should.be.true; + }); + + }); + + describe('inputs', () => { + + it('should do nothing if data not initialised', async(fakeAsync(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + component.listItem = 'editorFileList2'; + let scrollMock = sinon.stub(directiveInstance, 'performScrollAction'); + + fixture.detectChanges(); + tick(); + + scrollMock.should.not.have.been.called; + }))); + + it('should call performScrollAction and data initialised', async(fakeAsync(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + // initialise data + directiveInstance.ngAfterContentInit(); + + // set input + component.listItem = 'editorFileList2'; + let scrollMock = sinon.stub(directiveInstance, 'performScrollAction'); + + fixture.detectChanges(); + tick(); + + scrollMock.should.have.been.called; + }))); + + it('should update when selecteditem changes and data initialised', async(fakeAsync(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + let directiveEl = fixture.debugElement.query(By.directive(ScrollToElementDirective)); + let directiveInstance = directiveEl.injector.get(ScrollToElementDirective); + + // initialise data + directiveInstance.ngAfterContentInit(); + + component.listItem = 'editorFileList2'; + let scrollMock = sinon.stub(directiveInstance, 'performScrollAction'); + + fixture.detectChanges(); + tick(); + + scrollMock.should.have.been.called; + }))); + }); +}); diff --git a/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.ts b/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.ts new file mode 100644 index 0000000000..607fad4fc8 --- /dev/null +++ b/packages/composer-playground/src/app/directives/scroll/scroll-to-element.directive.ts @@ -0,0 +1,85 @@ +import { + Directive, + ElementRef, + Input, + Renderer, + ContentChildren, + AfterContentInit, + QueryList +} from '@angular/core'; + +@Directive({ + selector: '[scroll-to-element]', +}) + +export class ScrollToElementDirective implements AfterContentInit { + + @Input() + set elementId(elementId) { + this._thing = elementId; + if (this._thing && this.initialised) { + this.performScrollAction(); + } + } + + @ContentChildren('editorFileList') items: QueryList; + + private _thing = null; + private initialised = false; + + constructor(private el: ElementRef, private renderer: Renderer) { + } + + ngAfterContentInit() { + this.initialised = true; + } + + performScrollAction() { + let element = this.el; + let selectedItem = this.retreiveSelectedItem(); + if (selectedItem && selectedItem.length > 0) { + let parentOffset = element.nativeElement.offsetTop; + let selectOffset = selectedItem[0].nativeElement.offsetTop; + + let endScrollTop = selectOffset - parentOffset - 10; + let startScrollTop = element.nativeElement.scrollTop; + let scrollDiff = startScrollTop - endScrollTop; + + let steps = 100; let timer = 0; let slow = 4; + let step = scrollDiff / steps; + let stepTarget = startScrollTop - step; + while (steps > 0) { + this.stepVerticalScoll(stepTarget, slow * timer); + timer++; // slow on approach to target + steps--; // while condition + stepTarget -= step; + // Prevent overshoot + if ( this.isOvershoot(scrollDiff, endScrollTop, stepTarget) ) { + steps = 0; + } + // Final adjust + if (steps === 0) { + this.stepVerticalScoll(endScrollTop, slow * timer); + } + } + } + } + + isOvershoot(scrollDiff, endScrollTop, stepTarget) { + if (scrollDiff < 0) { + return stepTarget > endScrollTop; + } else { + return stepTarget < endScrollTop; + } + } + + retreiveSelectedItem() { + return this.items.filter( (item) => { return item.nativeElement.id === this._thing; }); + } + + stepVerticalScoll(yLocation, duration) { + setTimeout(() => { + this.renderer.setElementProperty(this.el.nativeElement, 'scrollTop', yLocation); + }, duration); + } +} diff --git a/packages/composer-playground/src/app/editor/editor.component.html b/packages/composer-playground/src/app/editor/editor.component.html index 36ce78daed..99380d13b0 100644 --- a/packages/composer-playground/src/app/editor/editor.component.html +++ b/packages/composer-playground/src/app/editor/editor.component.html @@ -1,9 +1,9 @@