From 741ec5e6c9c4938ce9a90f55d1b747705627480a Mon Sep 17 00:00:00 2001 From: EdMoffatt Date: Mon, 23 Oct 2017 18:41:26 +0300 Subject: [PATCH 1/9] Replace the empty registry image (#2451) Signed-off-by: E. P. Moffatt --- packages/composer-playground/src/assets/svg/other/Emptiness.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/composer-playground/src/assets/svg/other/Emptiness.svg b/packages/composer-playground/src/assets/svg/other/Emptiness.svg index b8578d68fb..d2ba45742b 100644 --- a/packages/composer-playground/src/assets/svg/other/Emptiness.svg +++ b/packages/composer-playground/src/assets/svg/other/Emptiness.svg @@ -1 +1 @@ -Emptiness \ No newline at end of file +Emptiness \ No newline at end of file From e8e9c1718ee423836de397aa1ced4adba4fee184 Mon Sep 17 00:00:00 2001 From: caroline-church Date: Tue, 24 Oct 2017 08:44:49 +0100 Subject: [PATCH 2/9] Update Playground Version Check (#2441) Made the playground version check happen sooner so that you won't get error messages, you will only get the message to clear local storage contributes to hyperledger/composer#2208 Signed-off-by: Caroline Church --- .../src/app/app.component.spec.ts | 30 +++--- .../src/app/app.component.ts | 14 +-- .../app/services/initialization.service.ts | 3 +- .../test/resource/resource.component.spec.ts | 14 +-- .../app/test/resource/resource.component.ts | 92 +++++++++---------- .../transaction/transaction.component.spec.ts | 16 +--- .../test/transaction/transaction.component.ts | 82 ++++++++--------- 7 files changed, 109 insertions(+), 142 deletions(-) diff --git a/packages/composer-playground/src/app/app.component.spec.ts b/packages/composer-playground/src/app/app.component.spec.ts index 564c503a83..cca9c6606b 100644 --- a/packages/composer-playground/src/app/app.component.spec.ts +++ b/packages/composer-playground/src/app/app.component.spec.ts @@ -148,7 +148,6 @@ describe('AppComponent', () => { let mockAlertService: MockAlertService; let mockModal; let mockAdminService; - let mockConnectionProfileService; let mockBusinessNetworkConnection; let mockIdCard; let mockIdentityService; @@ -165,6 +164,8 @@ describe('AppComponent', () => { let activatedRoute: ActivatedRouteStub; let routerStub: RouterStub; + let checkVersionStub; + beforeEach(async(() => { mockClientService = sinon.createStubInstance(ClientService); mockInitializationService = sinon.createStubInstance(InitializationService); @@ -217,9 +218,12 @@ describe('AppComponent', () => { beforeEach(async(() => { fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; + checkVersionStub = sinon.stub(component, 'checkVersion'); })); - function updateComponent() { + function updateComponent(checkVersion = true) { + checkVersionStub.returns(Promise.resolve(checkVersion)); + // trigger initial data binding fixture.detectChanges(); @@ -306,14 +310,13 @@ describe('AppComponent', () => { }); it('should check version and open version modal', fakeAsync(() => { - let checkVersionStub = sinon.stub(component, 'checkVersion').returns(Promise.resolve(false)); let openVersionModalStub = sinon.stub(component, 'openVersionModal'); mockClientService.ensureConnected.returns(Promise.resolve()); mockClientService.getBusinessNetworkName.returns('bob'); routerStub.eventParams = {url: '/bob', nav: 'end'}; - updateComponent(); + updateComponent(false); tick(); @@ -323,7 +326,6 @@ describe('AppComponent', () => { })); it('should check version and not open version modal', fakeAsync(() => { - let checkVersionStub = sinon.stub(component, 'checkVersion').returns(Promise.resolve(true)); let openVersionModalStub = sinon.stub(component, 'openVersionModal'); mockClientService.ensureConnected.returns(Promise.resolve()); mockClientService.getBusinessNetworkName.returns('bob'); @@ -340,7 +342,6 @@ describe('AppComponent', () => { })); it('should not do anything on non navigation end events', fakeAsync(() => { - let checkVersionStub = sinon.stub(component, 'checkVersion'); let welcomeModalStub = sinon.stub(component, 'openWelcomeModal'); routerStub.eventParams = {url: '/', nav: 'start'}; @@ -349,12 +350,10 @@ describe('AppComponent', () => { tick(); - checkVersionStub.should.not.have.been.called; welcomeModalStub.should.not.have.been.called; })); it('should show header links if logged in', fakeAsync(() => { - let checkVersionStub = sinon.stub(component, 'checkVersion').returns(Promise.resolve(true)); routerStub.eventParams = {url: '/editor', nav: 'end'}; mockClientService.ensureConnected.returns(Promise.resolve()); mockClientService.getBusinessNetworkName.returns('bob'); @@ -369,7 +368,6 @@ describe('AppComponent', () => { })); it('should not show header links if not logged in', fakeAsync(() => { - let checkVersionStub = sinon.stub(component, 'checkVersion').returns(Promise.resolve(true)); routerStub.eventParams = {url: '/login', nav: 'end'}; updateComponent(); @@ -382,7 +380,6 @@ describe('AppComponent', () => { })); it('should not show header links if redirected to login', fakeAsync(() => { - let checkVersionStub = sinon.stub(component, 'checkVersion').returns(Promise.resolve(true)); routerStub.eventParams = {url: '/editor', nav: 'end', urlAfterRedirects: '/login'}; updateComponent(); @@ -826,8 +823,6 @@ describe('AppComponent', () => { })); it('should open the welcome modal', fakeAsync(() => { - let checkVersionStub = sinon.stub(component, 'checkVersion').returns(Promise.resolve(true)); - activatedRoute.testParams = {}; updateComponent(); @@ -842,11 +837,9 @@ describe('AppComponent', () => { })); it('should open the version modal', fakeAsync(() => { - let checkVersionStub = sinon.stub(component, 'checkVersion').returns(Promise.resolve(false)); - activatedRoute.testParams = {}; - updateComponent(); + updateComponent(false); component['openWelcomeModal'](); @@ -895,7 +888,6 @@ describe('AppComponent', () => { mockOnError = sinon.stub(component, 'onErrorStatus'); mockOnTransactionEvent = sinon.stub(component, 'onTransactionEvent'); mockQueryParamsUpdated = sinon.stub(component, 'queryParamsUpdated'); - })); it('should check the version return true', fakeAsync(() => { @@ -906,6 +898,8 @@ describe('AppComponent', () => { updateComponent(); + checkVersionStub.restore(); + component['checkVersion']().then((result) => { result.should.equal(true); }); @@ -926,6 +920,8 @@ describe('AppComponent', () => { updateComponent(); + checkVersionStub.restore(); + component['checkVersion']().then((result) => { result.should.equal(true); }); @@ -947,6 +943,8 @@ describe('AppComponent', () => { updateComponent(); + checkVersionStub.restore(); + component['checkVersion']().then((result) => { result.should.equal(false); }); diff --git a/packages/composer-playground/src/app/app.component.ts b/packages/composer-playground/src/app/app.component.ts index 206213a596..08cf5b2eba 100644 --- a/packages/composer-playground/src/app/app.component.ts +++ b/packages/composer-playground/src/app/app.component.ts @@ -62,7 +62,7 @@ export class AppComponent implements OnInit, OnDestroy { private configService: ConfigService) { } - ngOnInit() { + ngOnInit(): Promise { this.subs = [ this.alertService.busyStatus$.subscribe((busyStatus) => { this.onBusyStatus(busyStatus); @@ -80,6 +80,12 @@ export class AppComponent implements OnInit, OnDestroy { this.processRouteEvent(e); }) ]; + + return this.checkVersion().then((success) => { + if (!success) { + this.openVersionModal(); + } + }); } ngOnDestroy() { @@ -101,12 +107,6 @@ export class AppComponent implements OnInit, OnDestroy { let welcomePromise; if (event['url'] === '/login' && this.showWelcome) { welcomePromise = this.openWelcomeModal(); - } else { - welcomePromise = this.checkVersion().then((success) => { - if (!success) { - this.openVersionModal(); - } - }); } if (event['url'] === '/login' || event['urlAfterRedirects'] === '/login') { diff --git a/packages/composer-playground/src/app/services/initialization.service.ts b/packages/composer-playground/src/app/services/initialization.service.ts index a6f2e7a7dd..42e8cd9dbf 100644 --- a/packages/composer-playground/src/app/services/initialization.service.ts +++ b/packages/composer-playground/src/app/services/initialization.service.ts @@ -15,8 +15,7 @@ export class InitializationService { private config; - constructor(private clientService: ClientService, - private alertService: AlertService, + constructor(private alertService: AlertService, private identityService: IdentityService, private identityCardService: IdentityCardService, private configService: ConfigService) { diff --git a/packages/composer-playground/src/app/test/resource/resource.component.spec.ts b/packages/composer-playground/src/app/test/resource/resource.component.spec.ts index 662f8f3c1a..254226a612 100644 --- a/packages/composer-playground/src/app/test/resource/resource.component.spec.ts +++ b/packages/composer-playground/src/app/test/resource/resource.component.spec.ts @@ -2,17 +2,14 @@ /* tslint:disable:no-unused-expression */ /* tslint:disable:no-var-requires */ /* tslint:disable:max-classes-per-file */ -import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { DebugElement, Component, Input } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { Component, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { TransactionComponent } from './transaction.component'; -import { CodemirrorComponent } from 'ng2-codemirror'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ClientService } from '../../services/client.service'; -import { InitializationService } from '../../services/initialization.service'; import { Resource, @@ -23,7 +20,6 @@ import { ParticipantDeclaration, TransactionDeclaration, ClassDeclaration, - Property, Factory, ModelFile } from 'composer-common'; @@ -50,7 +46,6 @@ describe('ResourceComponent', () => { let mockNgbActiveModal; let mockClientService; - let mockInitializationService; let mockBusinessNetworkConnection; let mockBusinessNetwork; @@ -66,11 +61,9 @@ describe('ResourceComponent', () => { mockNgbActiveModal = sinon.createStubInstance(NgbActiveModal); mockClientService = sinon.createStubInstance(ClientService); - mockInitializationService = sinon.createStubInstance(InitializationService); mockNgbActiveModal.open = sandbox.stub(); mockNgbActiveModal.close = sandbox.stub(); - mockInitializationService.initialize.returns(Promise.resolve()); mockResource = sinon.createStubInstance(Resource); mockBusinessNetworkConnection = sinon.createStubInstance(BusinessNetworkConnection); @@ -95,8 +88,7 @@ describe('ResourceComponent', () => { ], providers: [ {provide: NgbActiveModal, useValue: mockNgbActiveModal}, - {provide: ClientService, useValue: mockClientService}, - {provide: InitializationService, useValue: mockInitializationService} + {provide: ClientService, useValue: mockClientService} ] }); fixture = TestBed.createComponent(ResourceComponent); diff --git a/packages/composer-playground/src/app/test/resource/resource.component.ts b/packages/composer-playground/src/app/test/resource/resource.component.ts index 7ee11860ac..34d2acefa6 100644 --- a/packages/composer-playground/src/app/test/resource/resource.component.ts +++ b/packages/composer-playground/src/app/test/resource/resource.component.ts @@ -56,37 +56,31 @@ export class ResourceComponent implements OnInit { }; constructor(public activeModal: NgbActiveModal, - private clientService: ClientService, - private initializationService: InitializationService) { + private clientService: ClientService) { } - ngOnInit(): Promise { - return this.initializationService.initialize() - .then(() => { - - // Determine what resource declaration we are using and stub json decription - let introspector = this.clientService.getBusinessNetwork().getIntrospector(); - let modelClassDeclarations = introspector.getClassDeclarations(); - - modelClassDeclarations.forEach((modelClassDeclaration) => { - if (this.registryId === modelClassDeclaration.getFullyQualifiedName()) { - - // Set resource declaration - this.resourceDeclaration = modelClassDeclaration; - this.resourceType = this.retrieveResourceType(modelClassDeclaration); - - if (this.editMode()) { - this.resourceAction = 'Update'; - let serializer = this.clientService.getBusinessNetwork().getSerializer(); - this.resourceDefinition = JSON.stringify(serializer.toJSON(this.resource), null, 2); - } else { - // Stub out json definition - this.resourceAction = 'Create New'; - this.generateResource(); - } + ngOnInit() { + // Determine what resource declaration we are using and stub json decription + let introspector = this.clientService.getBusinessNetwork().getIntrospector(); + let modelClassDeclarations = introspector.getClassDeclarations(); + + modelClassDeclarations.forEach((modelClassDeclaration) => { + if (this.registryId === modelClassDeclaration.getFullyQualifiedName()) { + + // Set resource declaration + this.resourceDeclaration = modelClassDeclaration; + this.resourceType = this.retrieveResourceType(modelClassDeclaration); + + if (this.editMode()) { + this.resourceAction = 'Update'; + let serializer = this.clientService.getBusinessNetwork().getSerializer(); + this.resourceDefinition = JSON.stringify(serializer.toJSON(this.resource), null, 2); + } else { + // Stub out json definition + this.resourceAction = 'Create New'; + this.generateResource(); } - }); - + } }); } @@ -112,7 +106,7 @@ export class ResourceComponent implements OnInit { /** * Generate the json description of a resource */ - private generateResource(withSampleData?: boolean): void { + private generateResource(withSampleData ?: boolean): void { let businessNetworkDefinition = this.clientService.getBusinessNetwork(); let factory = businessNetworkDefinition.getFactory(); let idx = Math.round(Math.random() * 9999).toString(); @@ -152,25 +146,25 @@ export class ResourceComponent implements OnInit { private addOrUpdateResource(): void { this.actionInProgress = true; return this.retrieveResourceRegistry(this.resourceType) - .then((registry) => { - let json = JSON.parse(this.resourceDefinition); - let serializer = this.clientService.getBusinessNetwork().getSerializer(); - let resource = serializer.fromJSON(json); - resource.validate(); - if (this.editMode()) { - return registry.update(resource); - } else { - return registry.add(resource); - } - }) - .then(() => { - this.actionInProgress = false; - this.activeModal.close(); - }) - .catch((error) => { - this.definitionError = error.toString(); - this.actionInProgress = false; - }); + .then((registry) => { + let json = JSON.parse(this.resourceDefinition); + let serializer = this.clientService.getBusinessNetwork().getSerializer(); + let resource = serializer.fromJSON(json); + resource.validate(); + if (this.editMode()) { + return registry.update(resource); + } else { + return registry.add(resource); + } + }) + .then(() => { + this.actionInProgress = false; + this.activeModal.close(); + }) + .catch((error) => { + this.definitionError = error.toString(); + this.actionInProgress = false; + }); } /** @@ -213,7 +207,5 @@ export class ResourceComponent implements OnInit { }; return types[type](); - } - } diff --git a/packages/composer-playground/src/app/test/transaction/transaction.component.spec.ts b/packages/composer-playground/src/app/test/transaction/transaction.component.spec.ts index 3e9e01af08..5f0238baba 100644 --- a/packages/composer-playground/src/app/test/transaction/transaction.component.spec.ts +++ b/packages/composer-playground/src/app/test/transaction/transaction.component.spec.ts @@ -5,28 +5,26 @@ import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { By, BrowserModule } from '@angular/platform-browser'; import { Component, Input } from '@angular/core'; -import { FormsModule, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR, NgForm } from '@angular/forms'; +import { FormsModule } from '@angular/forms'; import { TransactionComponent } from './transaction.component'; -import { CodemirrorComponent } from 'ng2-codemirror'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ClientService } from '../../services/client.service'; -import { InitializationService } from '../../services/initialization.service'; import { TransactionDeclaration, BusinessNetworkDefinition, Serializer, Factory, - Resource, ModelFile, Introspector } from 'composer-common'; -import { BusinessNetworkConnection, AssetRegistry, TransactionRegistry } from 'composer-client'; +import { BusinessNetworkConnection } from 'composer-client'; import * as chai from 'chai'; import * as sinon from 'sinon'; + let should = chai.should(); @Component({ @@ -43,7 +41,6 @@ describe('TransactionComponent', () => { let element: HTMLElement; let mockNgbActiveModal; let mockClientService; - let mockInitializationService; let mockTransaction; let mockBusinessNetwork; let mockBusinessNetworkConnection; @@ -58,7 +55,6 @@ describe('TransactionComponent', () => { sandbox = sinon.sandbox.create(); mockNgbActiveModal = sinon.createStubInstance(NgbActiveModal); mockClientService = sinon.createStubInstance(ClientService); - mockInitializationService = sinon.createStubInstance(InitializationService); mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); mockBusinessNetworkConnection = sinon.createStubInstance(BusinessNetworkConnection); mockSerializer = sinon.createStubInstance(Serializer); @@ -77,7 +73,6 @@ describe('TransactionComponent', () => { mockBusinessNetworkConnection.submitTransaction = sandbox.stub(); mockTransaction = sinon.createStubInstance(TransactionDeclaration); mockNgbActiveModal.close = sandbox.stub(); - mockInitializationService.initialize.returns(Promise.resolve()); TestBed.configureTestingModule({ imports: [ @@ -90,11 +85,10 @@ describe('TransactionComponent', () => { ], providers: [ {provide: NgbActiveModal, useValue: mockNgbActiveModal}, - {provide: ClientService, useValue: mockClientService}, - {provide: InitializationService, useValue: mockInitializationService} + {provide: ClientService, useValue: mockClientService} ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { diff --git a/packages/composer-playground/src/app/test/transaction/transaction.component.ts b/packages/composer-playground/src/app/test/transaction/transaction.component.ts index dfab8752aa..8cce079fa1 100644 --- a/packages/composer-playground/src/app/test/transaction/transaction.component.ts +++ b/packages/composer-playground/src/app/test/transaction/transaction.component.ts @@ -1,9 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ClientService } from '../../services/client.service'; -import { InitializationService } from '../../services/initialization.service'; import { TransactionDeclaration } from 'composer-common'; -import leftPad = require('left-pad'); import 'codemirror/mode/javascript/javascript'; import 'codemirror/addon/fold/foldcode'; @@ -53,37 +51,31 @@ export class TransactionComponent implements OnInit { }; constructor(public activeModal: NgbActiveModal, - private clientService: ClientService, - private initializationService: InitializationService) { + private clientService: ClientService) { } - ngOnInit(): Promise { - return this.initializationService.initialize() - .then(() => { - - let introspector = this.clientService.getBusinessNetwork().getIntrospector(); - this.transactionTypes = introspector.getClassDeclarations() - .filter((modelClassDeclaration) => { - // Non-abstract, non-system transactions only please! - return !modelClassDeclaration.isAbstract() && - !modelClassDeclaration.isSystemType() && - modelClassDeclaration instanceof TransactionDeclaration; - }); - - // Set first in list as selectedTransaction - if (this.transactionTypes && this.transactionTypes.length > 0) { - this.selectedTransaction = this.transactionTypes[0]; - this.selectedTransactionName = this.selectedTransaction.getName(); - - // We wish to hide certain items in a transaction, set these here - this.hiddenTransactionItems.set(this.selectedTransaction.getIdentifierFieldName(), uuid.v4()); - this.hiddenTransactionItems.set('timestamp', new Date()); - - // Create a resource definition for the base item - this.generateTransactionDeclaration(); - } + ngOnInit() { + let introspector = this.clientService.getBusinessNetwork().getIntrospector(); + this.transactionTypes = introspector.getClassDeclarations() + .filter((modelClassDeclaration) => { + // Non-abstract, non-system transactions only please! + return !modelClassDeclaration.isAbstract() && + !modelClassDeclaration.isSystemType() && + modelClassDeclaration instanceof TransactionDeclaration; + }); + + // Set first in list as selectedTransaction + if (this.transactionTypes && this.transactionTypes.length > 0) { + this.selectedTransaction = this.transactionTypes[0]; + this.selectedTransactionName = this.selectedTransaction.getName(); - }); + // We wish to hide certain items in a transaction, set these here + this.hiddenTransactionItems.set(this.selectedTransaction.getIdentifierFieldName(), uuid.v4()); + this.hiddenTransactionItems.set('timestamp', new Date()); + + // Create a resource definition for the base item + this.generateTransactionDeclaration(); + } } /** @@ -151,20 +143,20 @@ export class TransactionComponent implements OnInit { private submitTransaction() { this.submitInProgress = true; return Promise.resolve() - .then(() => { - let json = JSON.parse(this.resourceDefinition); - let serializer = this.clientService.getBusinessNetwork().getSerializer(); - this.submittedTransaction = serializer.fromJSON(json); - return this.clientService.getBusinessNetworkConnection().submitTransaction(this.submittedTransaction); - }) - .then(() => { - this.submitInProgress = false; - this.definitionError = null; - this.activeModal.close(this.submittedTransaction); - }) - .catch((error) => { - this.definitionError = error.toString(); - this.submitInProgress = false; - }); + .then(() => { + let json = JSON.parse(this.resourceDefinition); + let serializer = this.clientService.getBusinessNetwork().getSerializer(); + this.submittedTransaction = serializer.fromJSON(json); + return this.clientService.getBusinessNetworkConnection().submitTransaction(this.submittedTransaction); + }) + .then(() => { + this.submitInProgress = false; + this.definitionError = null; + this.activeModal.close(this.submittedTransaction); + }) + .catch((error) => { + this.definitionError = error.toString(); + this.submitInProgress = false; + }); } } From 539d76be0be2b73276a4769952b7cc19b5802e56 Mon Sep 17 00:00:00 2001 From: Sam Winslet Date: Tue, 24 Oct 2017 16:44:53 +0100 Subject: [PATCH 3/9] holy moly lets hope this works (#2452) Signed-off-by: samwinslet --- .../composer-playground/src/app/app.module.ts | 2 +- ...odels.module.ts => basic-modals.module.ts} | 7 ++-- .../confirm/confirm.component.html | 16 ---------- .../confirm/confirm.component.scss | 9 ------ .../confirm/confirm.component.spec.ts | 32 ------------------- .../basic-modals/confirm/confirm.component.ts | 15 --------- .../src/app/basic-modals/confirm/index.ts | 1 - .../test/registry/registry.component.spec.ts | 3 +- .../app/test/registry/registry.component.ts | 12 ++++--- 9 files changed, 13 insertions(+), 84 deletions(-) rename packages/composer-playground/src/app/basic-modals/{basic-models.module.ts => basic-modals.module.ts} (63%) delete mode 100644 packages/composer-playground/src/app/basic-modals/confirm/confirm.component.html delete mode 100644 packages/composer-playground/src/app/basic-modals/confirm/confirm.component.scss delete mode 100644 packages/composer-playground/src/app/basic-modals/confirm/confirm.component.spec.ts delete mode 100644 packages/composer-playground/src/app/basic-modals/confirm/confirm.component.ts delete mode 100644 packages/composer-playground/src/app/basic-modals/confirm/index.ts diff --git a/packages/composer-playground/src/app/app.module.ts b/packages/composer-playground/src/app/app.module.ts index f14307bcf9..9e0801d5ce 100644 --- a/packages/composer-playground/src/app/app.module.ts +++ b/packages/composer-playground/src/app/app.module.ts @@ -18,7 +18,7 @@ import { AppComponent } from './app.component'; import { APP_RESOLVER_PROVIDERS } from './app.resolver'; import { AppState, InternalStateType } from './app.service'; import { AboutComponent } from './about'; -import { BasicModalsModule } from './basic-modals/basic-models.module'; +import { BasicModalsModule } from './basic-modals/basic-modals.module'; import { WelcomeComponent } from './welcome'; import { NoContentComponent } from './no-content'; import { VersionCheckComponent } from './version-check'; diff --git a/packages/composer-playground/src/app/basic-modals/basic-models.module.ts b/packages/composer-playground/src/app/basic-modals/basic-modals.module.ts similarity index 63% rename from packages/composer-playground/src/app/basic-modals/basic-models.module.ts rename to packages/composer-playground/src/app/basic-modals/basic-modals.module.ts index c744fa88d1..a2b1882cfe 100644 --- a/packages/composer-playground/src/app/basic-modals/basic-models.module.ts +++ b/packages/composer-playground/src/app/basic-modals/basic-modals.module.ts @@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { BusyComponent } from './busy/busy.component'; -import { ConfirmComponent } from './confirm/confirm.component'; import { DeleteComponent } from './delete-confirm/delete-confirm.component'; import { ErrorComponent } from './error/error.component'; import { ReplaceComponent } from './replace-confirm/replace-confirm.component'; @@ -13,10 +12,10 @@ import { TestModule } from './../test/test.module'; @NgModule({ imports: [CommonModule, NgbModule, TestModule], - entryComponents: [BusyComponent, ConfirmComponent, DeleteComponent, ErrorComponent, ReplaceComponent, SuccessComponent], - declarations: [BusyComponent, ConfirmComponent, DeleteComponent, ErrorComponent, ReplaceComponent, SuccessComponent], + entryComponents: [BusyComponent, DeleteComponent, ErrorComponent, ReplaceComponent, SuccessComponent], + declarations: [BusyComponent, DeleteComponent, ErrorComponent, ReplaceComponent, SuccessComponent], providers: [AlertService], - exports: [BusyComponent, ConfirmComponent, DeleteComponent, ErrorComponent, ReplaceComponent, SuccessComponent] + exports: [BusyComponent, DeleteComponent, ErrorComponent, ReplaceComponent, SuccessComponent] }) export class BasicModalsModule { diff --git a/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.html b/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.html deleted file mode 100644 index 4416e06597..0000000000 --- a/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.html +++ /dev/null @@ -1,16 +0,0 @@ -
- - -
- - -
-
\ No newline at end of file diff --git a/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.scss b/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.scss deleted file mode 100644 index 5fe1ae1f88..0000000000 --- a/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import '../../../assets/styles/base/_colors.scss'; - -.confirm { - .modal-header { - svg { - fill: $error-colour-1 - } - } -} diff --git a/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.spec.ts b/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.spec.ts deleted file mode 100644 index c63f2ace75..0000000000 --- a/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* tslint:disable:no-unused-variable */ -/* tslint:disable:no-unused-expression */ -/* tslint:disable:no-var-requires */ -/* tslint:disable:max-classes-per-file */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ConfirmComponent } from './confirm.component'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; - -import * as sinon from 'sinon'; -import * as chai from 'chai'; - -let should = chai.should(); - -describe('ConfirmComponent', () => { - let component: ConfirmComponent; - let fixture: ComponentFixture; - - let mockActiveModal = sinon.createStubInstance(NgbActiveModal); - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ConfirmComponent], - providers: [{provide: NgbActiveModal, useValue: mockActiveModal}] - }); - fixture = TestBed.createComponent(ConfirmComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - component.should.be.ok; - }); -}); diff --git a/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.ts b/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.ts deleted file mode 100644 index d57d2b5bb8..0000000000 --- a/packages/composer-playground/src/app/basic-modals/confirm/confirm.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, Input } from '@angular/core'; - -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; - -@Component({ - selector: 'confirm', - templateUrl: './confirm.component.html', - styleUrls: ['./confirm.component.scss'.toString()] -}) -export class ConfirmComponent { - @Input() confirm; - - constructor(public activeModal: NgbActiveModal) { - } -} diff --git a/packages/composer-playground/src/app/basic-modals/confirm/index.ts b/packages/composer-playground/src/app/basic-modals/confirm/index.ts deleted file mode 100644 index 6b33c78eff..0000000000 --- a/packages/composer-playground/src/app/basic-modals/confirm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './confirm.component'; diff --git a/packages/composer-playground/src/app/test/registry/registry.component.spec.ts b/packages/composer-playground/src/app/test/registry/registry.component.spec.ts index 1387678b0c..4d38eff69c 100644 --- a/packages/composer-playground/src/app/test/registry/registry.component.spec.ts +++ b/packages/composer-playground/src/app/test/registry/registry.component.spec.ts @@ -316,7 +316,7 @@ describe(`RegistryComponent`, () => { tick(); tick(); component.loadResources.should.be.called; - mockNgbModalRef.componentInstance.confirmMessage.should.equal('Please confirm that you want to delete Asset: ' + mockResource.getIdentifier()); + mockNgbModalRef.componentInstance.deleteMessage.should.equal('This action will be recorded in the Historian, and cannot be reversed. Are you sure you want to delete?'); })); it('should create a new error with the alert service', fakeAsync(() => { @@ -325,7 +325,6 @@ describe(`RegistryComponent`, () => { component.openDeleteResourceModal(mockResource); tick(); tick(); - mockNgbModalRef.componentInstance.confirmMessage.should.equal('Please confirm that you want to delete Asset: ' + mockResource.getIdentifier()); mockAlertService.errorStatus$.next.should.be.called; mockAlertService.errorStatus$.next.should.be .calledWith('Removing the selected item from the registry failed:error message'); diff --git a/packages/composer-playground/src/app/test/registry/registry.component.ts b/packages/composer-playground/src/app/test/registry/registry.component.ts index b1ab2e6f7f..862e0fe382 100644 --- a/packages/composer-playground/src/app/test/registry/registry.component.ts +++ b/packages/composer-playground/src/app/test/registry/registry.component.ts @@ -4,7 +4,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ClientService } from '../../services/client.service'; import { AlertService } from '../../basic-modals/alert.service'; import { ResourceComponent } from '../resource/resource.component'; -import { ConfirmComponent } from '../../basic-modals/confirm/confirm.component'; +import { DeleteComponent } from '../../basic-modals/delete-confirm/delete-confirm.component'; import { ViewTransactionComponent } from '../view-transaction/view-transaction.component'; @Component({ @@ -108,9 +108,12 @@ export class RegistryComponent { } openDeleteResourceModal(resource: any) { - const confirmModalRef = this.modalService.open(ConfirmComponent); - confirmModalRef.componentInstance.confirmMessage = 'Please confirm that you want to delete Asset: ' + resource.getIdentifier(); - + const confirmModalRef = this.modalService.open(DeleteComponent); + confirmModalRef.componentInstance.headerMessage = 'Delete Asset/Participant'; + confirmModalRef.componentInstance.deleteMessage = 'This action will be recorded in the Historian, and cannot be reversed. Are you sure you want to delete?'; + confirmModalRef.componentInstance.fileType = resource.$type; + confirmModalRef.componentInstance.fileName = resource.getIdentifier(); + confirmModalRef.componentInstance.action = 'delete'; confirmModalRef.result.then((result) => { if (result) { this._registry.remove(resource) @@ -125,6 +128,7 @@ export class RegistryComponent { } else { // TODO: we should always get called with a code for this usage of the // modal but will that always be true + } }); } From bc93f336db884c999492fbd3d70c9c7aa7815fb9 Mon Sep 17 00:00:00 2001 From: "Mark S. Lewis" Date: Wed, 25 Oct 2017 14:19:48 +0100 Subject: [PATCH 4/9] Improve portability of Connection Profiles (#2468) Remove portability constraint for connection profiles due to keyValStore path. Store Fabric client data in a defined path based on the name of the Business Network Card being used, rather than the absolute path specified in the keyValStore property of the connection profile. Signed-off-by: Mark S. Lewis --- .../lib/businessnetworkconnection.js | 10 +- .../test/businessnetworkconnection.js | 42 +++++-- .../lib/cardstore/filesystemcardstore.js | 18 +-- packages/composer-common/lib/idcard.js | 2 +- packages/composer-common/lib/util.js | 11 ++ .../test/cardstore/filesystemcardstore.js | 13 --- packages/composer-common/test/idcard.js | 8 ++ packages/composer-common/test/util.js | 21 +++- .../lib/hlfconnectionmanager.js | 109 +++++++++++------- .../test/hlfconnectionmanager.js | 76 +++++++++++- 10 files changed, 222 insertions(+), 88 deletions(-) diff --git a/packages/composer-client/lib/businessnetworkconnection.js b/packages/composer-client/lib/businessnetworkconnection.js index 50e6006c46..d07566bbec 100644 --- a/packages/composer-client/lib/businessnetworkconnection.js +++ b/packages/composer-client/lib/businessnetworkconnection.js @@ -447,9 +447,13 @@ class BusinessNetworkConnection extends EventEmitter { let card; return this.cardStore.get(cardName) - .then((card_)=>{ - card = card_; - return this.connectionProfileManager.connectWithData(card.getConnectionProfile(),card.getBusinessNetworkName(), additionalConnectOptions); + .then((retrievedCard)=>{ + card = retrievedCard; + if (!additionalConnectOptions) { + additionalConnectOptions = {}; + } + additionalConnectOptions.cardName = cardName; + return this.connectionProfileManager.connectWithData(card.getConnectionProfile(), card.getBusinessNetworkName(), additionalConnectOptions); }) .then((connection) => { LOG.exit(method); diff --git a/packages/composer-client/test/businessnetworkconnection.js b/packages/composer-client/test/businessnetworkconnection.js index d9693633a4..f5ae1f2bbc 100644 --- a/packages/composer-client/test/businessnetworkconnection.js +++ b/packages/composer-client/test/businessnetworkconnection.js @@ -231,14 +231,18 @@ describe('BusinessNetworkConnection', () => { }); describe('#connectWithCard',()=>{ + const userName = 'FredBloggs'; + const enrollmentSecret = 'password'; + const keyValStore = '/conga/conga/conga'; - it('Correct with with existing card name',()=>{ + beforeEach(() => { sandbox.stub(businessNetworkConnection.connectionProfileManager, 'connectWithData').resolves(mockConnection); let mockCardStore = sinon.createStubInstance(CardStore); let mockIdCard = sinon.createStubInstance(IdCard); mockCardStore.get.resolves(mockIdCard); - mockIdCard.getEnrollmentCredentials.returns({secret:'password'}); - mockIdCard.getUserName.returns('FredBloggs'); + mockIdCard.getEnrollmentCredentials.returns({secret: enrollmentSecret}); + mockIdCard.getUserName.returns(userName); + mockIdCard.getConnectionProfile.returns({ keyValStore: keyValStore }); businessNetworkConnection.cardStore = mockCardStore; mockConnection.login.resolves(mockSecurityContext); @@ -254,18 +258,42 @@ describe('BusinessNetworkConnection', () => { { $class: 'org.acme.sample.SampleEvent', eventId: 'event1' }, { $class: 'org.acme.sample.SampleEvent', eventId: 'event2' } ]); + }); + afterEach(() => { + sandbox.reset(); + }); + + it('Connect with existing card name',()=>{ return businessNetworkConnection.connectWithCard('cardName') .then((result)=>{ - sinon.assert.calledOnce(mockCardStore.get); - sinon.assert.calledWith(mockCardStore.get,'cardName'); - sinon.assert.calledWith(mockConnection.login,'FredBloggs','password'); + sinon.assert.calledWith(mockConnection.login, userName, enrollmentSecret); }); }); - }); + it('should add card name to connection profile additional options when additional options not specified', () => { + const cardName = 'CARD_NAME'; + return businessNetworkConnection.connectWithCard(cardName) + .then(result => { + sinon.assert.calledWith(businessNetworkConnection.connectionProfileManager.connectWithData, + sinon.match.any, + sinon.match.any, + sinon.match.has('cardName', cardName)); + }); + }); + it('should override cardName property specified in additional options', () => { + const cardName = 'CARD_NAME'; + return businessNetworkConnection.connectWithCard(cardName, { cardName: 'WRONG' }) + .then(result => { + sinon.assert.calledWith(businessNetworkConnection.connectionProfileManager.connectWithData, + sinon.match.any, + sinon.match.any, + sinon.match.has('cardName', cardName)); + }); + }); + }); describe('#disconnect', () => { diff --git a/packages/composer-common/lib/cardstore/filesystemcardstore.js b/packages/composer-common/lib/cardstore/filesystemcardstore.js index 0bc0a460ff..97680043ca 100644 --- a/packages/composer-common/lib/cardstore/filesystemcardstore.js +++ b/packages/composer-common/lib/cardstore/filesystemcardstore.js @@ -14,13 +14,13 @@ 'use strict'; +const composerUtil = require('../util'); const nodeFs = require('fs'); -const os = require('os'); const path = require('path'); const rimraf = require('rimraf'); const thenifyAll = require('thenify-all'); -const IdCard = require('../idcard'); const BusinessNetworkCardStore = require('./businessnetworkcardstore'); +const IdCard = require('../idcard'); const thenifyRimraf = thenifyAll(rimraf); @@ -50,19 +50,7 @@ class FileSystemCardStore extends BusinessNetworkCardStore { this.thenifyFs = thenifyAll(this.fs); this.rimrafOptions = Object.assign({}, this.fs); this.rimrafOptions.disableGlob = true; - this.storePath = options.storePath || FileSystemCardStore._defaultStorePath(os.homedir); - } - - /** - * Get the default store path based on the user's home directory, or based on the filesystem root - * directory if the supplied function does not exist or returns a falsy value. - * @private - * @param {Function} homedirFunction Function to obtain the user's home directory - * @returns {String} Absolute path - */ - static _defaultStorePath(homedirFunction) { - const homeDirectory = (homedirFunction && homedirFunction()) || path.sep; - return path.join(homeDirectory, '.composer', 'cards'); + this.storePath = options.storePath || path.join(composerUtil.homeDirectory(), '.composer', 'cards'); } /** diff --git a/packages/composer-common/lib/idcard.js b/packages/composer-common/lib/idcard.js index 85f63042fd..2bacacea6a 100644 --- a/packages/composer-common/lib/idcard.js +++ b/packages/composer-common/lib/idcard.js @@ -131,7 +131,7 @@ class IdCard { * @return {Object} connection profile. */ getConnectionProfile() { - return this.connectionProfile; + return Object.assign({}, this.connectionProfile); } /** diff --git a/packages/composer-common/lib/util.js b/packages/composer-common/lib/util.js index e1ab9bd635..3e13d69672 100644 --- a/packages/composer-common/lib/util.js +++ b/packages/composer-common/lib/util.js @@ -15,6 +15,8 @@ 'use strict'; const Globalize = require('./globalize'); +const os = require('os'); +const path = require('path'); const SecurityContext = require('./securitycontext'); const SecurityException = require('./securityexception'); const uuid = require('uuid'); @@ -122,6 +124,15 @@ class Util { } + /** + * Get the home directory path for the current user. Returns root directory for environments where there is no + * file system path available. + * @returns {String} A file system path. + */ + static homeDirectory() { + return (os.homedir && os.homedir()) || path.sep; + } + } module.exports = Util; diff --git a/packages/composer-common/test/cardstore/filesystemcardstore.js b/packages/composer-common/test/cardstore/filesystemcardstore.js index 0c7d3cbd74..347bf6f356 100644 --- a/packages/composer-common/test/cardstore/filesystemcardstore.js +++ b/packages/composer-common/test/cardstore/filesystemcardstore.js @@ -59,19 +59,6 @@ describe('FileSystemCardStore', function() { }); }); - describe('#_defaultStorePath', function() { - it('should handle undefined homedir function', function() { - FileSystemCardStore._defaultStorePath(undefined).should.be.a('String').that.is.not.empty; - }); - - it('should handle empty value returned from homedir function', function() { - const homedir = () => { - return null; - }; - FileSystemCardStore._defaultStorePath(homedir).should.be.a('String').that.is.not.empty; - }); - }); - describe('#get', function() { it('should get a valid identity card', function() { const options = { storePath: testStorePath }; diff --git a/packages/composer-common/test/idcard.js b/packages/composer-common/test/idcard.js index 329fa9d0ab..ccdd2d2ad1 100644 --- a/packages/composer-common/test/idcard.js +++ b/packages/composer-common/test/idcard.js @@ -473,4 +473,12 @@ describe('IdCard', function() { }); }); + describe('#getConnectionProfile', function() { + it('should make defensive copy of connection profile', function() { + const connectionProfile = minimalCard.getConnectionProfile(); + connectionProfile.CONGA = 'CONGA'; + minimalCard.getConnectionProfile().should.not.equal(connectionProfile); + }); + }); + }); diff --git a/packages/composer-common/test/util.js b/packages/composer-common/test/util.js index f5ffec1357..7d35eb1435 100644 --- a/packages/composer-common/test/util.js +++ b/packages/composer-common/test/util.js @@ -15,13 +15,19 @@ 'use strict'; const Connection = require('../lib/connection'); +const os = require('os'); +const path = require('path'); const SecurityContext = require('../lib/securitycontext'); const SecurityException = require('../lib/securityexception'); const Util = require('../lib/util'); const uuid = require('uuid'); -require('chai').should(); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); const sinon = require('sinon'); +chai.use(chaiAsPromised); +chai.should(); + describe('Util', function () { let mockConnection; @@ -198,4 +204,17 @@ describe('Util', function () { }); + describe('#homeDirectory', function() { + it('should return valid path', function() { + const result = Util.homeDirectory(); + path.isAbsolute(result).should.be.true; + }); + + it('should return root directory if os.homedir function returns undefined', function() { + sandbox.stub(os, 'homedir').returns(undefined); + const result = Util.homeDirectory(); + result.should.equal(path.sep); + }); + }); + }); diff --git a/packages/composer-connector-hlfv1/lib/hlfconnectionmanager.js b/packages/composer-connector-hlfv1/lib/hlfconnectionmanager.js index 13bd50c9e3..c9440a09ce 100644 --- a/packages/composer-connector-hlfv1/lib/hlfconnectionmanager.js +++ b/packages/composer-connector-hlfv1/lib/hlfconnectionmanager.js @@ -14,9 +14,11 @@ 'use strict'; +const composerUtil = require('composer-common').Util; const Logger = require('composer-common').Logger; const util = require('util'); const fs = require('fs'); +const path = require('path'); const LOG = Logger.getLog('HLFConnectionManager'); @@ -245,8 +247,8 @@ class HLFConnectionManager extends ConnectionManager { throw new Error('The peers array has not been specified in the connection profile'); } else if (!profileDefinition.peers.length) { throw new Error('No peer URLs have been specified in the connection profile'); - } else if (!wallet && !profileDefinition.keyValStore) { - throw new Error('No key value store directory or wallet has been specified'); + } else if (!wallet && !profileDefinition.keyValStore && !profileDefinition.cardName) { + throw new Error('No key value store directory, wallet or card name has been specified'); } else if (!profileDefinition.ca) { throw new Error('The certificate authority URL has not been specified in the connection profile'); } else if (!profileDefinition.channel) { @@ -263,53 +265,74 @@ class HLFConnectionManager extends ConnectionManager { * * @param {Client} client the fabric-client * @param {Wallet} wallet the wallet implementation or null/undefined - * @param {string} keyValStorePath a path for the fileKeyValStore to use or null/undefined if a wallet specified. + * @param {Object} profileData The connection profile. * @returns {Promise} resolves to a client configured with the required stores * * @memberOf HLFConnectionManager */ - _setupWallet(client, wallet, keyValStorePath) { - const method = '_setupWallet'; - // If a wallet has been specified, then we want to use that. - //let result; - LOG.entry(method, client, wallet, keyValStorePath); + _setupClientStore(client, wallet, profileData) { + const method = '_setupClientStore'; + LOG.entry(method, client, wallet, profileData); if (wallet) { - LOG.debug(method, 'A wallet has been specified, using wallet proxy'); - return new HLFWalletProxy(wallet) - .then((store) => { - let cryptostore = Client.newCryptoKeyStore(HLFWalletProxy, wallet); - client.setStateStore(store); - let cryptoSuite = Client.newCryptoSuite(); - cryptoSuite.setCryptoKeyStore(cryptostore); - client.setCryptoSuite(cryptoSuite); - return store; - }) - .catch((error) => { - LOG.error(method, error); - let newError = new Error('error trying to setup a wallet. ' + error); - throw newError; - }); + return this._setupWallet(client, wallet); + } + let storePath; + if (profileData.cardName) { + storePath = path.join(composerUtil.homeDirectory(), '.composer', 'client-data', profileData.cardName); } else { - // No wallet specified, so create a file based key value store. - LOG.debug(method, 'Using key value store', keyValStorePath); - return Client.newDefaultKeyValueStore({path: keyValStorePath}) - .then((store) => { - client.setStateStore(store); - - let cryptoSuite = Client.newCryptoSuite(); - cryptoSuite.setCryptoKeyStore(Client.newCryptoKeyStore({path: keyValStorePath})); - client.setCryptoSuite(cryptoSuite); - - return store; - }) - .catch((error) => { - LOG.error(method, error); - let newError = new Error('error trying to setup a keystore path. ' + error); - throw newError; - }); + storePath = profileData.keyValStore; } + + return this._setupFileStore(client, storePath); + + } + + /** + * Link a wallet to the fabric-client store and cryptostore + * @param {Client} client the fabric client + * @param {Wallet} wallet the wallet implementation + * @returns {Promise} resolves to a client configured with the required stores + */ + _setupWallet(client, wallet) { + const method = '_setupWallet'; + LOG.entry(method, client, wallet); + return new HLFWalletProxy(wallet).then((store) => { + const cryptostore = Client.newCryptoKeyStore(HLFWalletProxy, wallet); + client.setStateStore(store); + const cryptoSuite = Client.newCryptoSuite(); + cryptoSuite.setCryptoKeyStore(cryptostore); + client.setCryptoSuite(cryptoSuite); + return store; + }).catch((error) => { + LOG.error(method, error); + const newError = new Error('error trying to setup a wallet. ' + error); + throw newError; + }); + } + + /** + * Configure the Fabric client with a file-based store. + * @param {Client} client The Fabric client + * @param {String} keyValStorePath File system location to use for the store + * @returns {Promise} resolves to a client configured with the required stores + */ + _setupFileStore(client, keyValStorePath) { + const method = '_setupFileStore'; + LOG.entry(method, client, keyValStorePath); + return Client.newDefaultKeyValueStore({path: keyValStorePath}).then((store) => { + client.setStateStore(store); + const cryptoSuite = Client.newCryptoSuite(); + cryptoSuite.setCryptoKeyStore(Client.newCryptoKeyStore({path: keyValStorePath})); + client.setCryptoSuite(cryptoSuite); + return store; + }) + .catch((error) => { + LOG.error(method, error); + const newError = new Error('error trying to setup a keystore path. ' + error); + throw newError; + }); } /** @@ -398,7 +421,7 @@ class HLFConnectionManager extends ConnectionManager { let mspID = connectionOptions.mspID; const client = HLFConnectionManager.createClient(); - return this._setupWallet(client, wallet, connectionOptions.keyValStore) + return this._setupClientStore(client, wallet, connectionOptions) .then(() => { return client.createUser({ username: id, @@ -492,7 +515,7 @@ class HLFConnectionManager extends ConnectionManager { }); // Set up the wallet. - return this._setupWallet(client, wallet, connectOptions.keyValStore) + return this._setupClientStore(client, wallet, connectOptions) .then(() => { // Create a CA client. @@ -521,7 +544,7 @@ class HLFConnectionManager extends ConnectionManager { const method = 'exportIdentity'; LOG.entry(method, connectionProfileName, connectionOptions, id); const client = HLFConnectionManager.createClient(); - return this._setupWallet(client, connectionOptions.wallet, connectionOptions.keyValStore) + return this._setupClientStore(client, connectionOptions.wallet, connectionOptions) .then(() => { return client.getUserContext(id, true); }) diff --git a/packages/composer-connector-hlfv1/test/hlfconnectionmanager.js b/packages/composer-connector-hlfv1/test/hlfconnectionmanager.js index 7eb16da26b..0702ed94fb 100644 --- a/packages/composer-connector-hlfv1/test/hlfconnectionmanager.js +++ b/packages/composer-connector-hlfv1/test/hlfconnectionmanager.js @@ -26,6 +26,7 @@ const HLFWalletProxy = require('../lib/hlfwalletproxy'); const KeyValueStore = api.KeyValueStore; const CryptoSuite = api.CryptoSuite; const Orderer = require('fabric-client/lib/Orderer'); +const path = require('path'); const Peer = require('fabric-client/lib/Peer'); const User = require('fabric-client/lib/User'); const Wallet = require('composer-common').Wallet; @@ -545,7 +546,7 @@ describe('HLFConnectionManager', () => { mockCAClient = sinon.createStubInstance(FabricCAClientImpl); sandbox.stub(HLFConnectionManager, 'createCAClient').withArgs(sinon.match.string).returns(mockCAClient); mockKeyValueStore = sinon.createStubInstance(KeyValueStore); - sandbox.stub(Client, 'newDefaultKeyValueStore').resolves(mockKeyValueStore); + sandbox.stub(Client, 'newDefaultKeyValueStore').withArgs(sinon.match.has('path', sinon.match.string)).resolves(mockKeyValueStore); configSettingStub = sandbox.stub(Client, 'setConfigSetting'); mockWallet = sinon.createStubInstance(Wallet); }); @@ -645,11 +646,52 @@ describe('HLFConnectionManager', () => { }).should.throw(/The certificate authority URL has not been specified/); }); - it('should throw if keyValStore and wallet are not specified', () => { + it('should throw if none of keyValStore, wallet and cardName are specified', () => { delete connectOptions.keyValStore; + delete connectOptions.wallet; + delete connectOptions.cardName; + (() => { + connectionManager.connect('hlfabric1', 'org-acme-biznet', connectOptions); + }).should.throw(/No key value store directory, wallet or card name has been specified/); + }); + + it('should not throw if keyValStore specified but wallet and cardName are not', () => { + delete connectOptions.wallet; + delete connectOptions.cardName; (() => { connectionManager.connect('hlfabric1', 'org-acme-biznet', connectOptions); - }).should.throw(/No key value store directory or wallet has been specified/); + }).should.not.throw(); + }); + + it('should not throw if wallet specified but keyValStore and cardName are not', () => { + connectOptions.wallet = mockWallet; + delete connectOptions.keyValStore; + delete connectOptions.cardName; + (() => { + connectionManager.connect('hlfabric1', 'org-acme-biznet', connectOptions); + }).should.not.throw(); + }); + + it('should not throw if cardName specified but keyValStore and wallet are not', () => { + delete connectOptions.wallet; + delete connectOptions.keyValStore; + connectOptions.cardName = 'CONGA_CARD'; + (() => { + connectionManager.connect('hlfabric1', 'org-acme-biznet', connectOptions); + }).should.not.throw(); + }); + + it('should use cardName to build keyValStore path if cardName and keyValStore specified but wallet is not', () => { + delete connectOptions.wallet; + const cardName = 'CONGA_CARD'; + connectOptions.cardName = cardName; + connectOptions.keyValStore = 'KEY_VAL_STORE'; + return connectionManager.connect('hlfabric1', 'org-acme-biznet', connectOptions).then(connection => { + sinon.assert.calledWith(Client.newDefaultKeyValueStore, + sinon.match.has('path', + sinon.match(value => path.isAbsolute(value) && value.includes(connectOptions.cardName) + ))); + }); }); //TODO: should throw if wallet not of the right type. @@ -875,7 +917,9 @@ describe('HLFConnectionManager', () => { }); it('should handle an error creating a store using keyValStore', () => { + Client.newDefaultKeyValueStore.reset(); Client.newDefaultKeyValueStore.rejects('wow such fail'); + // sandbox.stub(Client, 'newDefaultKeyValueStore').rejects('wow such fail'); return connectionManager.connect('hlfabric1', 'org-acme-biznet', connectOptions) .should.be.rejectedWith(/wow such fail/); }); @@ -897,6 +941,16 @@ describe('HLFConnectionManager', () => { }); }); + it('should use specified wallet in preference to cardName and keyValStore', () => { + connectOptions.cardName = 'CONGA_CARD'; + connectOptions.keyValStore = 'KEY_VAL_STORE'; + connectOptions.wallet = mockWallet; + return connectionManager.connect('hlfabric1', 'org-acme-biznet', connectOptions) + .then((connection) => { + sinon.assert.calledWith(mockClient.setStateStore, sinon.match.instanceOf(HLFWalletProxy)); + }); + }); + it('should configure a wallet proxy if a singleton wallet is provided', () => { Wallet.setWallet(mockWallet); return connectionManager.connect('hlfabric1', 'org-acme-biznet', connectOptions) @@ -905,6 +959,16 @@ describe('HLFConnectionManager', () => { }); }); + it('should use singleton wallet in preference to cardName and keyValStore', () => { + Wallet.setWallet(mockWallet); + connectOptions.cardName = 'CONGA_CARD'; + connectOptions.keyValStore = 'KEY_VAL_STORE'; + return connectionManager.connect('hlfabric1', 'org-acme-biznet', connectOptions) + .then((connection) => { + sinon.assert.calledWith(mockClient.setStateStore, sinon.match.instanceOf(HLFWalletProxy)); + }); + }); + it('should set a default timeout', () => { delete connectOptions.timeout; return connectionManager.connect('hlfabric1', 'org-acme-biznet', connectOptions) @@ -1039,11 +1103,13 @@ describe('HLFConnectionManager', () => { }).should.throw(/No msp id defined/); }); - it('should throw if no keyValStore or wallet is not specified', () => { + it('should throw if none of keyValStore, wallet and cardName are specified', () => { delete profile.keyValStore; + delete profile.wallet; + delete profile.cardName; (() => { connectionManager.importIdentity('connprof1', profile, 'anid', 'acert', 'akey'); - }).should.throw(/No key value store directory or wallet has been specified/); + }).should.throw(/No key value store directory, wallet or card name has been specified/); }); it('should handle an error creating a default key value store', () => { From 0553288f1cc955c70ae258ecbd7492c86baccbc1 Mon Sep 17 00:00:00 2001 From: Nick Lincoln Date: Wed, 25 Oct 2017 16:42:23 +0100 Subject: [PATCH 5/9] move filter tests into single test file and scope other filter tests (#2467) Signed-off-by: Nick Lincoln --- packages/composer-rest-server/test/assets.js | 134 ----- packages/composer-rest-server/test/filter.js | 517 ++++++++++++++++++ .../composer-rest-server/test/participants.js | 32 -- 3 files changed, 517 insertions(+), 166 deletions(-) create mode 100644 packages/composer-rest-server/test/filter.js diff --git a/packages/composer-rest-server/test/assets.js b/packages/composer-rest-server/test/assets.js index c4d26b7503..cbc1363f54 100644 --- a/packages/composer-rest-server/test/assets.js +++ b/packages/composer-rest-server/test/assets.js @@ -185,140 +185,6 @@ const bfs_fs = BrowserFS.BFSRequire('fs'); ]); }); }); - it('should return the asset with a specified id', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter={"where":{"ISINCode":"ISIN_1"}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - assetData[0] - ]); - }); - }); - - it('should return the asset with a specified non-id property', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter={"where":{"bond.dayCountFraction":"EOM"}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - assetData[0] - ]); - }); - }); - - it('should return the assets with a specified non-id property in a different format', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter[where][bond.dayCountFraction]=EOM`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - assetData[0] - ]); - }); - }); - - it('should return the asset with multiple specified non-id properties', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter={"where":{"bond.dayCountFraction":"EOM", "bond.faceAmount":1000}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - assetData[0] - ]); - }); - }); - - it('should return the asset with multiple specified non-id properties including a datetime', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter={"where":{"bond.dayCountFraction":"EOM", "bond.maturity":"2018-02-27T21:03:52.000Z"}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - assetData[0] - ]); - }); - }); - - it('should return the assets with a range of specified properties for a datetime value', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter={"where":{"bond.maturity":{"between":["2018-02-27T21:03:52.000Z", "2018-12-27T21:03:52.000Z"]}}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - assetData[0], - assetData[1] - ]); - }); - }); - - it('should return the assets with a range of specified properties for a number value', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter={"where":{"bond.faceAmount":{"between":[1000, 1500]}}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - assetData[0] - ]); - }); - }); - - it('should return the assets with a range of specified properties for a string value', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter={"where":{"bond.dayCountFraction":{"between":["EOM", "EOY"]}}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - assetData[0], - assetData[1] - ]); - }); - }); - - it('should return the assets with a combination of the or operator with mutiple properties', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter={"where":{"or":[{"bond.dayCountFraction":"EOM"}, {"bond.maturity":"2018-12-27T21:03:52.000Z"}, {"bond.faceAmount":1000}]}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - assetData[0], - assetData[1] - ]); - }); - }); - - it('should return the assets with a combination of the and operator with mutiple properties', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter={"where":{"and":[{"bond.dayCountFraction":"EOM"},{"bond.maturity":{"lt":"2018-12-27T21:03:52.000Z"}}]}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - assetData[0] - ]); - }); - }); - - it('should return a 500 if the and|or operator has more than three properties which is a limitation of pouchdb', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter={"where":{"and":[{"bond.dayCountFraction":"EOM"},{"bond.faceAmount":{"lt":2000}}, {"bond.paymentFrequency.period":"YEAR"}]}}`) - .then(() => { - throw new Error('should not get here'); - }) - .catch((err) => { - err.response.should.have.status(500); - }); - }); - - it('should return the assets with a nested of the \'and\' and the \'or\' operator with mutiple properties', () => { - return chai.request(app) - .get(`/api/${prefix}BondAsset?filter={"where":{"and":[{"bond.dayCountFraction":"EOM"},{"or":[{"bond.maturity":"2018-12-27T21:03:52.000Z"}, {"bond.faceAmount":1000}]}]}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - assetData[0] - ]); - }); - }); }); diff --git a/packages/composer-rest-server/test/filter.js b/packages/composer-rest-server/test/filter.js new file mode 100644 index 0000000000..cf3a4f86d4 --- /dev/null +++ b/packages/composer-rest-server/test/filter.js @@ -0,0 +1,517 @@ +/* + * 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) => { + + const prefix = namespaces === 'always' ? 'org.acme.bond.' : ''; + + describe(`Filter REST API unit tests namespaces[${namespaces}]`, () => { + + const assetData = [{ + $class: 'org.acme.bond.BondAsset', + ISINCode: 'ISIN_1', + bond: { + $class: 'org.acme.bond.Bond', + description: 'A', + dayCountFraction: 'EOM', + exchangeId: [ + 'NYSE' + ], + faceAmount: 1000, + instrumentId: [ + 'AliceCorp' + ], + issuer: 'resource:org.acme.bond.Issuer#1', + maturity: '2018-01-27T21:03:52.000Z', + parValue: 1000, + paymentFrequency: { + $class: 'org.acme.bond.PaymentFrequency', + period: 'MONTH', + periodMultiplier: 1 + } + } + }, { + $class: 'org.acme.bond.BondAsset', + ISINCode: 'ISIN_2', + bond: { + $class: 'org.acme.bond.Bond', + description: 'B', + dayCountFraction: 'EOY', + exchangeId: [ + 'NYSE' + ], + faceAmount: 2000, + instrumentId: [ + 'BobCorp' + ], + issuer: 'resource:org.acme.bond.Issuer#2', + maturity: '2018-12-27T21:03:52.000Z', + parValue: 2000, + paymentFrequency: { + $class: 'org.acme.bond.PaymentFrequency', + period: 'YEAR', + periodMultiplier: 1 + } + } + }, { + $class: 'org.acme.bond.BondAsset', + ISINCode: 'ISIN_3', + bond: { + $class: 'org.acme.bond.Bond', + dayCountFraction: 'EOM', + description: 'C', + exchangeId: [ + 'NYSE' + ], + faceAmount: 1500, + instrumentId: [ + 'CharlieCorp' + ], + issuer: 'resource:org.acme.bond.Issuer#1', + maturity: '2017-02-27T21:03:52.000Z', + parValue: 3000, + paymentFrequency: { + $class: 'org.acme.bond.PaymentFrequency', + period: 'MONTH', + periodMultiplier: 4 + } + } + }, { + $class: 'org.acme.bond.BondAsset', + ISINCode: 'ISIN_4', + bond: { + $class: 'org.acme.bond.Bond', + dayCountFraction: 'EOY', + description: 'D', + exchangeId: [ + 'NYSE' + ], + faceAmount: 4000, + instrumentId: [ + 'DogeCorp' + ], + issuer: 'resource:org.acme.bond.Issuer#1', + maturity: '2016-02-27T21:03:52.000Z', + parValue: 4000, + 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((result) => { + app = result.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]), + serializer.fromJSON(assetData[2]), + serializer.fromJSON(assetData[3]) + ]); + }); + }); + + describe('Filter Equivalence', () => { + + // Identifier field + it('should return matches with a specified identifying field using json format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"ISINCode":"ISIN_1"}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[0] + ]); + }); + }); + + it('should return matches with a specified identifying using object format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter[where][ISINCode]=ISIN_1`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[0] + ]); + }); + }); + + // Non-identifier field + it('should return matches with a STRING property using json format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"bond.dayCountFraction":"EOM"}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[0], + assetData[2] + ]); + }); + }); + + xit('should return matches with a STRING property using json format', () => { + }); + + xit('should return matches with a DATETIME property using json format', () => { + }); + + xit('should return matches with a DOUBLE property using json format', () => { + }); + + xit('should return matches with an INTEGER CONCEPT property using json format', () => { + }); + + xit('should return matches with an ENUM CONCEPT property using json format', () => { + }); + + it('should return matches with multiple properties, STRING and DATETIME, using json format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"bond.dayCountFraction":"EOM", "bond.maturity":"2018-01-27T21:03:52.000Z"}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[0] + ]); + }); + }); + + it('should return matches with multiple properties, STRING and DOUBLE, using json format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"bond.dayCountFraction":"EOM", "bond.faceAmount":1000}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[0] + ]); + }); + }); + + it('should return an empty array if nothing matches the filter on an identifier field, using json format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"bond.dayCountFraction":"DOES_NOT_EXIST"}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([]); + }); + }); + + xit('should return an empty array if nothing matches the filter on a property field, using json format', () => { + }); + + it('should return an empty array if nothing matches the filter on an identifier field using object format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter[where][ISINCode]=DOES_NOT_EXIST`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([]); + }); + }); + }); + + describe('Filter Greater/Less Than', () => { + // valid only for numerical and date values + xit('should return GREATER THAN matches with a DOUBLE property, using json format', () => { + }); + + xit('should return GREATER THAN matches with an INTEGER property, using json format', () => { + }); + + xit('should return GREATER THAN matches with a DATETIME property, using json format', () => { + }); + + xit('should return LESS THAN matches with a DOUBLE property, using json format', () => { + }); + + xit('should return LESS THAN matches with an INTEGER property, using json format', () => { + }); + + xit('should return LESS THAN matches with a DATETIME property, using json format', () => { + }); + + xit('should return an empty array if no matching GREATER THAN DOUBLE property, using json format', () => { + }); + + xit('should return an empty array if no matching GREATER THAN DATETIME property, using json format', () => { + }); + + xit('should return an empty array if no matching LESS THAN DOUBLE property using, json format', () => { + }); + + xit('should return an empty array if no matching LESS THAN DATETIME property using,json format', () => { + }); + + }); + + describe('Filter AND', () => { + // interested in depth and combination + xit('should return matches with an identifier field AND non-identifier STRING property, using json format', () => { + }); + + xit('should return matches with an identifier field AND non-identifier DOUBLE property, using json format', () => { + }); + + xit('should return matches with an identifier field AND non-identifier DATETIME property, using json format', () => { + }); + + xit('should return matches with TWO non-identifier STRING properties, using json format', () => { + }); + + xit('should return matches with THREE non-identifier STRING properties, using json format', () => { + }); + + xit('should return matches with non-identifier STRING AND DOUBLE properties, using json format', () => { + + }); + + it('should return matches with non-identifier STRING AND LESS THAN DATETIME properties, using json format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"and":[{"bond.dayCountFraction":"EOM"},{"bond.maturity":{"lt":"2018-06-27T21:03:52.000Z"}}]}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[0], + assetData[2] + ]); + }); + }); + + xit('should return matches with non-identifier DOUBLE AND DATETIME properties, using json format', () => { + }); + + xit('should return matches with non-identifier STRING AND DOUBLE AND DATETIME properties, using json format', () => { + }); + + xit('should return an empty array if no matching AND properties, using json format', () => { + }); + }); + + describe('Filter OR', () => { + xit('should return matches on the identifier when filtering on the identifier field OR a property, using json format', () => { + }); + + xit('should return matches on the property when filtering on the identifier field OR a property, using json format', () => { + }); + + xit('should return matches on the property when filtering on the identifier field OR a property, using object format', () => { + }); + + xit('should return matches on a DOUBLE when filtering on DOUBLE OR STRING properties, using json format', () => { + }); + + xit('should return matches on a STRING when filtering on DOUBLE OR STRING properties, using json format', () => { + }); + + xit('should return matches on a DATETIME when filtering on DATETIME OR STRING properties, using json format', () => { + }); + + it('should return matches when filtering on DATETIME OR STRING OR DOUBLE properties, using json format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"or":[{"bond.dayCountFraction":"EOM"}, {"bond.maturity":"2018-01-27T21:03:52.000Z"}, {"bond.faceAmount":1000}]}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[0], + assetData[2] + ]); + }); + }); + + xit('should return matches on a DATETIME when filtering on DATETIME OR STRING properties, using object format', () => { + }); + + xit('should return an empty array if no matching OR properties, using json format', () => { + }); + + xit('should return an empty array if no matching OR properties, using object format', () => { + }); + + }); + + describe('Filter AND/OR', () => { + xit('should return matches when filtering on the identifier field AND a property OR property, using json format', () => { + // (IDENTIFIER) AND (PROPERTY OR PROPERTY) + }); + + xit('should return matches when filtering on the identifier field OR a property AND property, using json format', () => { + // (IDENTIFIER) OR (PROPERTY AND PROPERTY) + }); + + it('should return matches when filtering on the property AND a property OR property, using json format', () => { + // (PROPERTY) AND (PROPERTY OR PROPERTY) + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"and":[{"bond.dayCountFraction":"EOM"},{"or":[{"bond.maturity":"2018-12-27T21:03:52.000Z"}, {"bond.faceAmount":1000}]}]}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[0] + ]); + }); + }); + + xit('should return matches when filtering on the property OR a property AND property, using json format', () => { + // (PROPERTY) OR (PROPERTY AND PROPERTY) + }); + + xit('should return matches when filtering with compound AND/OR clauses on properties, using json format', () => { + // (PROPERTY AND PROPERTY) OR (PROPERTY AND PROPERTY) OR (PROPERTY AND PROPERTY) + }); + + xit('should return matches when filtering with compound AND/OR clauses on properties, using json format', () => { + // (PROPERTY OR PROPERTY) AND (PROPERTY OR PROPERTY) AND (PROPERTY OR PROPERTY) + }); + + xit('should return matches when filtering with nested AND/OR clauses on properties, using json format', () => { + // (PROPERTY OR (PROPERTY AND PROPERTY)) AND (PROPERTY AND (PROPERTY OR PROPERTY)) + }); + + xit('should return an empty array if no matching AND/OR properties, using object format', () => { + }); + }); + + describe('Filter BETWEEN', () => { + + xit('should return matches when filtering on the identifier field, using json format', () => { + }); + + it('should return matches when filtering on STRING property field, using json format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"bond.description":{"between":["C", "D"]}}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[2], + assetData[3] + ]); + }); + }); + + xit('should return matches when filtering on INTEGER property field, using json format', () => { + }); + + it('should return matches when filtering on DOUBLE property field, using json format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"bond.faceAmount":{"between":[900, 1501]}}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[0], + assetData[2] + ]); + }); + }); + + it('should return matches when filtering on DATETIME property field, using json format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"bond.maturity":{"between":["2017-09-27T21:03:52.000Z", "2018-12-27T21:03:52.000Z"]}}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([ + assetData[0], + assetData[1] + ]); + }); + }); + + it('should return an empty array if no matching BETWEEN STRING properties, using json format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"bond.description":{"between":["X", "Z"]}}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([]); + }); + }); + + it('should return an empty array if no matching BETWEEN DOUBLE properties, using object format', () => { + return chai.request(app) + .get(`/api/${prefix}BondAsset?filter={"where":{"bond.faceAmount":{"between":[9000, 10000]}}}`) + .then((res) => { + res.should.be.json; + res.body.should.deep.equal([]); + }); + }); + + }); + }); + + describe('Filter UNSUPPORTED', () => { + + xit('should return an error message when trying to use NEAR', () => { + }); + + xit('should return an error message when trying to use LIKE', () => { + }); + + xit('should return an error message when trying to use NLIKE', () => { + }); + + xit('should return an error message when trying to use REGEXP', () => { + }); + + }); +}); diff --git a/packages/composer-rest-server/test/participants.js b/packages/composer-rest-server/test/participants.js index d829013947..a5c234c920 100644 --- a/packages/composer-rest-server/test/participants.js +++ b/packages/composer-rest-server/test/participants.js @@ -117,38 +117,6 @@ const bfs_fs = BrowserFS.BFSRequire('fs'); }); }); - it('should return all of the participants with a specified property value', () => { - return chai.request(app) - .get(`/api/${prefix}Member?filter={"where": {"name": "Bob"}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - participantData[1] - ]); - }); - }); - - it('should return all of the participants with a range of property value', () => { - return chai.request(app) - .get(`/api/${prefix}Member?filter={"where": {"lastName": {"between":["A", "C"]}}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - participantData[1] - ]); - }); - }); - - it('should return an empty with a specified property value does not exsit in the registry', () => { - return chai.request(app) - .get(`/api/${prefix}Member?filter={"where": {"lastName": "Chow"}}`) - .then((res) => { - res.should.be.json; - res.body.should.deep.equal([ - ]); - }); - }); - }); describe(`POST / namespaces[${namespaces}]`, () => { From 59314011dde77b1d8c6f261f1d541797102fb1bb Mon Sep 17 00:00:00 2001 From: Simon Stone Date: Wed, 25 Oct 2017 18:37:42 +0100 Subject: [PATCH 6/9] Add CONTAINS operator to query language (#2471) * Add CONTAINS operator to Composer query language Signed-off-by: Simon Stone * Stop resetting environment inbetween query tests Signed-off-by: Simon Stone --- .../composer-common/lib/query/parser.pegjs | 10 +- .../lib/query/queryanalyzer.js | 95 +++++- .../lib/query/whereastvalidator.js | 114 +++++-- .../composer-common/test/data/query/model.cto | 12 + .../test/query/queryanalyzer.js | 103 ++++++- packages/composer-common/test/query/select.js | 27 +- .../test/query/whereastvalidator.js | 4 +- .../composer-runtime/lib/querycompiler.js | 108 ++++++- .../composer-runtime/test/querycompiler.js | 286 +++++++++++++++++- .../systest/data/queries.cto | 7 + packages/composer-systests/systest/queries.js | 256 ++++++++++++++-- .../systest/transactions.queries.js | 20 +- .../jekylldocs/business-network/query.md | 2 +- .../jekylldocs/reference/query-language.md | 1 + 14 files changed, 979 insertions(+), 66 deletions(-) diff --git a/packages/composer-common/lib/query/parser.pegjs b/packages/composer-common/lib/query/parser.pegjs index 89cad82117..bdaa28ac59 100644 --- a/packages/composer-common/lib/query/parser.pegjs +++ b/packages/composer-common/lib/query/parser.pegjs @@ -824,6 +824,14 @@ LogicalORExpressionNoIn LogicalOROperator = "OR" +ContainsExpression + = first:LogicalORExpression + rest:(__ ContainsOperator __ LogicalORExpression)* + { return buildBinaryExpression(first, rest); } + +ContainsOperator + = "CONTAINS" + ConditionalExpression = test:LogicalORExpression __ "?" __ consequent:AssignmentExpression __ @@ -836,7 +844,7 @@ ConditionalExpression alternate: alternate }; } - / LogicalORExpression + / ContainsExpression ConditionalExpressionNoIn = test:LogicalORExpressionNoIn __ diff --git a/packages/composer-common/lib/query/queryanalyzer.js b/packages/composer-common/lib/query/queryanalyzer.js index e32174b7e0..8dcec6ab97 100644 --- a/packages/composer-common/lib/query/queryanalyzer.js +++ b/packages/composer-common/lib/query/queryanalyzer.js @@ -87,6 +87,8 @@ class QueryAnalyzer { result = this.visitIdentifier(thing, parameters); } else if (thing.type === 'Literal') { result = this.visitLiteral(thing, parameters); + } else if (thing.type === 'ArrayExpression') { + result = this.visitArrayExpression(thing, parameters); } else if (thing.type === 'MemberExpression') { result = this.visitMemberExpression(thing, parameters); } else { @@ -251,6 +253,8 @@ class QueryAnalyzer { let result; if (arrayCombinationOperators.indexOf(ast.operator) !== -1) { result = this.visitArrayCombinationOperator(ast, parameters); + } else if (ast.operator === 'CONTAINS') { + result = this.visitContainsOperator(ast, parameters); } else { result = this.visitConditionOperator(ast, parameters); } @@ -279,6 +283,56 @@ class QueryAnalyzer { return result; } + /** + * Visitor design pattern; handle an contains operator. + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitContainsOperator(ast, parameters) { + const method = 'visitContainsOperator'; + LOG.entry(method, ast, parameters); + + // Check we haven't already entered a scope - let's keep it simple! + if (parameters.validationDisabled) { + throw new Error('A CONTAINS expression cannot be nested within another CONTAINS expression'); + } + + // Disable validation. + parameters.validationDisabled = true; + + // Resolve both the left and right sides of the expression. + let left = this.visit(ast.left, parameters); + let right = this.visit(ast.right, parameters); + + // Enable validation again. + parameters.validationDisabled = false; + + // Initialize the scopes array. + parameters.scopes = parameters.scopes || []; + + // Look for a scope name. + if (typeof left === 'string' && !left.startsWith('_$')) { + parameters.scopes.push(left); + } else if (typeof right === 'string' && !right.startsWith('_$')) { + parameters.scopes.push(right); + } else { + throw new Error('A property name is required on one side of a CONTAINS expression'); + } + + // Re-resolve both the left and right sides of the expression. + left = this.visit(ast.left, parameters); + right = this.visit(ast.right, parameters); + + // Pop the scope name off again. + parameters.scopes.pop(); + + const result = [left, right]; + LOG.exit(method, result); + return result; + } + /** * Visitor design pattern; handle a condition operator. * Condition operators are operators that compare two pieces of data, such @@ -298,11 +352,19 @@ class QueryAnalyzer { const rhs = this.visit(ast.right, parameters); const lhs = this.visit(ast.left, parameters); + // Bypass the following validation if required. This will be set during + // the first pass of a CONTAINS when we are trying to figure out the name + // of the current scope so we can correctly validate model references. + if (parameters.validationDisabled) { + LOG.exit(method, result); + return result; + } + // if the rhs is a string, it is the name of a property // and we infer the type of the lhs from the model // if the lhs is a parameter if (typeof rhs === 'string' && (lhs instanceof Array && lhs.length > 0)) { - lhs[0].type = this.getParameterType(rhs); + lhs[0].type = this.getParameterType(rhs, parameters); result = result.concat(lhs); } @@ -310,7 +372,7 @@ class QueryAnalyzer { // and we infer the type of the rhs from the model // if the rhs is a parameter if (typeof lhs === 'string' && (rhs instanceof Array && rhs.length > 0)) { - rhs[0].type = this.getParameterType(lhs); + rhs[0].type = this.getParameterType(lhs, parameters); result = result.concat(rhs); } @@ -368,6 +430,23 @@ class QueryAnalyzer { return result; } + /** + * Visitor design pattern; handle an array expression. + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitArrayExpression(ast, parameters) { + const method = 'visitArrayExpression'; + LOG.entry(method, ast, parameters); + const result = ast.elements.map((element) => { + return this.visit(element, parameters); + }); + LOG.exit(method, result); + return result; + } + /** * Visitor design pattern; handle a member expression. * @param {Object} ast The abstract syntax tree being visited. @@ -388,16 +467,24 @@ class QueryAnalyzer { /** * Get the parameter type for a property path on a resource * @param {string} parameterName The parameter name or name with nested structure e.g A.B.C + * @param {object} parameters The parameters * @return {string} The type to use for the parameter * @throws {Error} if the property does not exist or is of an unsupported type * @private */ - getParameterType(parameterName) { + getParameterType(parameterName, parameters) { const method = 'getParameterType'; LOG.entry(method, parameterName); + // If we have entered a scope, for example a CONTAINS, then we need + // to prepend the current scope to the property name. + let actualParameterName = parameterName; + if (parameters.scopes && parameters.scopes.length) { + actualParameterName = parameters.scopes.concat(parameterName).join('.'); + } + const classDeclaration = this.query.getSelect().getResourceClassDeclaration(); - const property = classDeclaration.getNestedProperty(parameterName); + const property = classDeclaration.getNestedProperty(actualParameterName); let result = null; diff --git a/packages/composer-common/lib/query/whereastvalidator.js b/packages/composer-common/lib/query/whereastvalidator.js index ab8ca9a8bc..9085b69e4e 100644 --- a/packages/composer-common/lib/query/whereastvalidator.js +++ b/packages/composer-common/lib/query/whereastvalidator.js @@ -22,7 +22,7 @@ const InvalidQueryException = require('./invalidqueryexception'); const Globalize = require('../globalize'); const BOOLEAN_OPERATORS = ['==', '!=']; -const OPERATORS = ['>', '>=', '==', '!=', '<=', '<']; +const OPERATORS = ['>', '>=', '==', '!=', '<=', '<', 'CONTAINS']; /** * The query validator visits the AST for a WHERE and checks that all the model references exist @@ -59,6 +59,8 @@ class WhereAstValidator { result = this.visitIdentifier(thing, parameters); } else if (thing.type === 'Literal') { result = this.visitLiteral(thing, parameters); + } else if (thing.type === 'ArrayExpression') { + result = this.visitArrayExpression(thing, parameters); } else if (thing.type === 'MemberExpression') { result = this.visitMemberExpression(thing, parameters); } else { @@ -84,6 +86,8 @@ class WhereAstValidator { const arrayCombinationOperators = ['AND', 'OR']; if (arrayCombinationOperators.indexOf(ast.operator) !== -1) { this.visitArrayCombinationOperator(ast, parameters); + } else if (ast.operator === 'CONTAINS') { + this.visitContainsOperator(ast, parameters); } else { this.visitConditionOperator(ast, parameters); } @@ -111,6 +115,55 @@ class WhereAstValidator { return null; } + /** + * Visitor design pattern; handle an contains operator. + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitContainsOperator(ast, parameters) { + const method = 'visitContainsOperator'; + LOG.entry(method, ast, parameters); + + // Check we haven't already entered a scope - let's keep it simple! + if (parameters.validationDisabled) { + throw new Error('A CONTAINS expression cannot be nested within another CONTAINS expression'); + } + + // Disable validation. + parameters.validationDisabled = true; + + // Resolve both the left and right sides of the expression. + let left = this.visit(ast.left, parameters); + let right = this.visit(ast.right, parameters); + + // Enable validation again. + parameters.validationDisabled = false; + + // Initialize the scopes array. + parameters.scopes = parameters.scopes || []; + + // Look for a scope name. + if (typeof left === 'string' && !left.startsWith('_$')) { + parameters.scopes.push(left); + } else if (typeof right === 'string' && !right.startsWith('_$')) { + parameters.scopes.push(right); + } else { + throw new Error('A property name is required on one side of a CONTAINS expression'); + } + + // Re-resolve both the left and right sides of the expression. + left = this.visit(ast.left, parameters); + right = this.visit(ast.right, parameters); + + // Pop the scope name off again. + parameters.scopes.pop(); + + LOG.exit(method); + return null; + } + /** * Visitor design pattern; handle a condition operator. * Condition operators are operators that compare two pieces of data, such @@ -123,32 +176,37 @@ class WhereAstValidator { visitConditionOperator(ast, parameters) { const method = 'visitConditionOperator'; LOG.entry(method, ast, parameters); + + // Resolve both the left and right sides of the expression. const left = this.visit(ast.left, parameters); const right = this.visit(ast.right, parameters); - // console.log('ast: ' + JSON.stringify(ast)); - // console.log('left: ' + JSON.stringify(left)); - // console.log('right: ' + JSON.stringify(right)); + // Bypass the following validation if required. This will be set during + // the first pass of a CONTAINS when we are trying to figure out the name + // of the current scope so we can correctly validate model references. + if (parameters.validationDisabled) { + LOG.exit(method); + return null; + } if (typeof left === 'string') { - if (!left.startsWith('_$')) { - const property = this.verifyProperty(left); + const property = this.verifyProperty(left, parameters); this.verifyOperator(property, ast.operator); } - if (right.type === 'Literal') { - const property = this.verifyProperty(left); + if (right && right.type === 'Literal') { + const property = this.verifyProperty(left, parameters); this.verifyTypeCompatibility(property, right.value); } } if (typeof right === 'string') { if (!right.startsWith('_$')) { - const property = this.verifyProperty(right); + const property = this.verifyProperty(right, parameters); this.verifyOperator(property, ast.operator); } - if (left.type === 'Literal') { - const property = this.verifyProperty(right); + if (left && left.type === 'Literal') { + const property = this.verifyProperty(right, parameters); this.verifyTypeCompatibility(property, left.value); } } @@ -161,13 +219,21 @@ class WhereAstValidator { /** * Checks that a property exists on the class declaration * @param {string} propertyName The property path + * @param {object} parameters The parameters * @throws {IllegalModelException} if property path does not resolve to a property * @returns {Property} the property * @private */ - verifyProperty(propertyName) { + verifyProperty(propertyName, parameters) { - const property = this.classDeclaration.getNestedProperty(propertyName); + // If we have entered a scope, for example a CONTAINS, then we need + // to prepend the current scope to the property name. + let actualPropertyName = propertyName; + if (parameters.scopes && parameters.scopes.length) { + actualPropertyName = parameters.scopes.concat(propertyName).join('.'); + } + + const property = this.classDeclaration.getNestedProperty(actualPropertyName); if (!property) { throw new IllegalModelException('Property ' + propertyName + ' not found on type ' + this.classDeclaration, this.classDeclaration.getModelFile(), this.classDeclaration. ast.location); } @@ -206,11 +272,7 @@ class WhereAstValidator { */ verifyTypeCompatibility(property, value) { - // console.log('property: ' + property); - // console.log('value: ' + value); - let dataType = typeof value; - // console.log('dataType: ' + dataType); if (dataType === 'undefined' || dataType === 'symbol') { WhereAstValidator.reportIncompatibleType(this.classDeclaration, property, value); @@ -218,6 +280,7 @@ class WhereAstValidator { let invalid = false; + // Arrays of concepts can't currently be specified as literal values. if (property.isArray()) { WhereAstValidator.reportUnsupportedType(this.classDeclaration, property, value); } @@ -300,6 +363,23 @@ class WhereAstValidator { return ast; } + /** + * Visitor design pattern; handle an array expression. + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitArrayExpression(ast, parameters) { + const method = 'visitArrayExpression'; + LOG.entry(method, ast, parameters); + const selector = ast.elements.map((element) => { + return this.visit(element, parameters); + }); + LOG.exit(method, selector); + return selector; + } + /** * Visitor design pattern; handle a member expression. * @param {Object} ast The abstract syntax tree being visited. diff --git a/packages/composer-common/test/data/query/model.cto b/packages/composer-common/test/data/query/model.cto index d644c4ae88..acfc835f3b 100644 --- a/packages/composer-common/test/data/query/model.cto +++ b/packages/composer-common/test/data/query/model.cto @@ -43,3 +43,15 @@ participant Regulator identified by email { transaction MyTransaction { } + +concept TestConcept { + o String value + o String[] values +} + +asset TestAsset identified by assetId { + o String assetId + o String[] stringValues + o TestConcept conceptValue + o TestConcept[] conceptValues +} diff --git a/packages/composer-common/test/query/queryanalyzer.js b/packages/composer-common/test/query/queryanalyzer.js index 03d10819e2..d712a69a3b 100644 --- a/packages/composer-common/test/query/queryanalyzer.js +++ b/packages/composer-common/test/query/queryanalyzer.js @@ -64,6 +64,17 @@ describe('QueryAnalyzer', () => { o String vin --> Driver driver } + + concept TestConcept { + o String value + } + + asset TestAsset identified by assetId { + o String assetId + o String[] stringValues + o TestConcept conceptValue + o TestConcept[] conceptValues + } `, 'test'); mockQuery = sinon.createStubInstance(Query); @@ -287,5 +298,95 @@ describe('QueryAnalyzer', () => { }).should.throw(/Unrecognised type/); }); + it('should process select with a contains and a literal value', () => { + const ast = parser.parse('SELECT org.acme.TestAsset WHERE (stringValues CONTAINS "foo")', { 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); + }); + + it('should process select with a contains and an array value', () => { + const ast = parser.parse('SELECT org.acme.TestAsset WHERE (stringValues CONTAINS ["foo", "bar"])', { 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); + }); + + it('should process select with a contains and a parameter value', () => { + const ast = parser.parse('SELECT org.acme.TestAsset WHERE (stringValues CONTAINS _$inputStringValue)', { 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); + }); + + it('should process select with a contains and a nested expression', () => { + const ast = parser.parse('SELECT org.acme.TestAsset WHERE (conceptValues CONTAINS (value == "foo"))', { 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); + }); + + it('should process select with a contains and a nested expression with a parameter value', () => { + const ast = parser.parse('SELECT org.acme.TestAsset WHERE (conceptValues CONTAINS (value == _$inputStringValue))', { 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); + }); + + it('should process select with a contains and a nested expression with a reversed parameter value', () => { + const ast = parser.parse('SELECT org.acme.TestAsset WHERE ((value == _$inputStringValue) CONTAINS conceptValues)', { 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); + }); + + it('should throw for a select with a contains without a property name', () => { + (() => { + const ast = parser.parse('SELECT org.acme.TestAsset WHERE ("foo" CONTAINS "moo")', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + queryAnalyzer.visit(mockQuery, {}); + }).should.throw(/A property name is required on one side of a CONTAINS expression/); + }); + + it('should throw for a select with a contains and an invalid nested expression with a parameter value', () => { + (() => { + const ast = parser.parse('SELECT org.acme.TestAsset WHERE (conceptValues CONTAINS (LULZ == _$inputStringValue))', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + queryAnalyzer.visit(mockQuery, {}); + }).should.throw(/Property LULZ does not exist/); + }); + + it('should throw for a select with a contains and an invalid nested contains', () => { + (() => { + const ast = parser.parse('SELECT org.acme.TestAsset WHERE (conceptValues CONTAINS (value CONTAINS "blah"))', { startRule: 'SelectStatement' }); + const select = new Select(mockQuery, ast); + mockQuery.getSelect.returns(select); + queryAnalyzer = new QueryAnalyzer( mockQuery ); + queryAnalyzer.visit(mockQuery, {}); + }).should.throw(/A CONTAINS expression cannot be nested within another CONTAINS expression/); + }); + }); -}); \ No newline at end of file +}); diff --git a/packages/composer-common/test/query/select.js b/packages/composer-common/test/query/select.js index f0d9d1fd5a..e48338ce6c 100644 --- a/packages/composer-common/test/query/select.js +++ b/packages/composer-common/test/query/select.js @@ -146,6 +146,13 @@ describe('Select', () => { new Select(mockQuery, toSelectAst('SELECT org.acme.Driver WHERE (dob == "2017-07-24")')).validate(); new Select(mockQuery, toSelectAst('SELECT org.acme.Driver WHERE ("2017-07-24" == dob)')).validate(); new Select(mockQuery, toSelectAst('SELECT org.acme.Driver WHERE ("2017-07-24" > dob)')).validate(); + + new Select(mockQuery, toSelectAst('SELECT org.acme.TestAsset WHERE (stringValues CONTAINS "moo")')).validate(); + new Select(mockQuery, toSelectAst('SELECT org.acme.TestAsset WHERE ("moo" CONTAINS stringValues)')).validate(); + new Select(mockQuery, toSelectAst('SELECT org.acme.TestAsset WHERE (stringValues CONTAINS ["foo","moo"])')).validate(); + new Select(mockQuery, toSelectAst('SELECT org.acme.TestAsset WHERE (["foo","moo"] CONTAINS stringValues)')).validate(); + new Select(mockQuery, toSelectAst('SELECT org.acme.TestAsset WHERE (conceptValues CONTAINS (value == "foo"))')).validate(); + new Select(mockQuery, toSelectAst('SELECT org.acme.TestAsset WHERE ((value == "foo") CONTAINS conceptValues)')).validate(); }); it('should throw for arrays', () => { @@ -202,12 +209,30 @@ describe('Select', () => { }).should.throw(/Property foo does not exist on org.acme.Car/); }); - it('should throw on missing property', () => { + it('should throw on missing nested property', () => { (() => { new Select(mockQuery, toSelectAst('SELECT org.acme.Driver WHERE (address.goo == 10)')).validate(); }).should.throw(/Property goo does not exist on org.acme.Address/); }); + it('should throw on missing nested property in contains', () => { + (() => { + new Select(mockQuery, toSelectAst('SELECT org.acme.TestAsset WHERE (conceptValues CONTAINS (LULZ == "moo"))')).validate(); + }).should.throw(/Property LULZ does not exist on org.acme.TestConcept/); + }); + + it('should throw on nested contains in contains', () => { + (() => { + new Select(mockQuery, toSelectAst('SELECT org.acme.TestAsset WHERE (conceptValues CONTAINS (value CONTAINS "moo"))')).validate(); + }).should.throw(/A CONTAINS expression cannot be nested within another CONTAINS expression/); + }); + + it('should throw on invalid contains', () => { + (() => { + new Select(mockQuery, toSelectAst('SELECT org.acme.TestAsset WHERE ("moo" CONTAINS "foo")')).validate(); + }).should.throw(/A property name is required on one side of a CONTAINS expression/); + }); + it('should throw for concept resources', () => { const s = new Select(mockQuery, selectConcept); (() => { diff --git a/packages/composer-common/test/query/whereastvalidator.js b/packages/composer-common/test/query/whereastvalidator.js index 3f1696a2eb..4d250590ea 100644 --- a/packages/composer-common/test/query/whereastvalidator.js +++ b/packages/composer-common/test/query/whereastvalidator.js @@ -121,14 +121,14 @@ describe('WhereAstValidator', () => { it('No error path',()=>{ mockAssetDeclaration.getNestedProperty.returns(mockStringProperty); let wv = new WhereAstValidator(mockAssetDeclaration); - let result = wv.verifyProperty(mockBooleanProperty); + let result = wv.verifyProperty(mockBooleanProperty, {}); result.should.equal(mockStringProperty); }); it('Error path',()=>{ mockAssetDeclaration.getNestedProperty.returns(null); let wv = new WhereAstValidator(mockAssetDeclaration); ( ()=>{ - wv.verifyProperty(mockBooleanProperty); + wv.verifyProperty(mockBooleanProperty, {}); }).should.throw(/not found/); }); }); diff --git a/packages/composer-runtime/lib/querycompiler.js b/packages/composer-runtime/lib/querycompiler.js index 3a80382928..4e184ee1d1 100644 --- a/packages/composer-runtime/lib/querycompiler.js +++ b/packages/composer-runtime/lib/querycompiler.js @@ -86,6 +86,8 @@ class QueryCompiler { result = this.visitIdentifier(thing, parameters); } else if (thing.type === 'Literal') { result = this.visitLiteral(thing, parameters); + } else if (thing.type === 'ArrayExpression') { + result = this.visitArrayExpression(thing, parameters); } else if (thing.type === 'MemberExpression') { result = this.visitMemberExpression(thing, parameters); } else { @@ -458,6 +460,8 @@ class QueryCompiler { let result; if (arrayCombinationOperators.indexOf(ast.operator) !== -1) { result = this.visitArrayCombinationOperator(ast, parameters); + } else if (ast.operator === 'CONTAINS') { + result = this.visitContainsOperator(ast, parameters); } else if (conditionOperators.indexOf(ast.operator) !== -1) { result = this.visitConditionOperator(ast, parameters); } else { @@ -491,17 +495,99 @@ class QueryCompiler { throw new Error('The query compiler does not support this operator'); } + // Visit the left and right sides of the expression. + let left = this.visit(ast.left, parameters); + let right = this.visit(ast.right, parameters); + // Build the Mango selector for this operator. const result = {}; result[operator] = [ - this.visit(ast.left, parameters), - this.visit(ast.right, parameters) + left, + right ]; LOG.exit(method, result); return result; } + /** + * Visitor design pattern; handle an contains operator. + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitContainsOperator(ast, parameters) { + const method = 'visitContainsOperator'; + LOG.entry(method, ast, parameters); + + // Visit the left and right sides of the expression. + let left = this.visit(ast.left, parameters); + let right = this.visit(ast.right, parameters); + + // Grab the left hand side of this expression. + const leftIsIdentifier = (ast.left.type === 'Identifier' && typeof left !== 'function'); + const leftIsMemberExpression = (ast.left.type === 'MemberExpression'); + const leftIsVariable = leftIsIdentifier || leftIsMemberExpression; + + // Grab the right hand side of this expression. + const rightIsIdentifier = (ast.right.type === 'Identifier' && typeof right !== 'function'); + const rightIsMemberExpression = (ast.right.type === 'MemberExpression'); + const rightIsVariable = rightIsIdentifier || rightIsMemberExpression; + + // Ensure the arguments are valid. + if (leftIsVariable) { + // This is OK. + } else if (rightIsVariable) { + // This is OK, but swap the arguments. + const temp = left; + left = right; + right = temp; + } else { + throw new Error(`The operator ${ast.operator} requires a property name`); + } + + // Check to see if we have a selector, in which case this is an $elemMatch. + let operator = '$all'; + if (!Array.isArray(right) && typeof right === 'object') { + operator = '$elemMatch'; + } + + // We have to coerce the right hand side into an array for an $all. + if (operator === '$all' && !Array.isArray(right)) { + if (typeof right === 'function') { + const originalRight = right; + right = () => { + const value = originalRight(); + if (Array.isArray(value)) { + return value; + } else { + return [ value ]; + } + }; + } else { + right = [ right ]; + } + } + + // Build the Mango selector for this operator. + const result = {}; + result[left] = {}; + const property = { + enumerable: true, + configurable: false + }; + if (typeof right === 'function') { + property.get = right; + } else { + property.value = right; + } + Object.defineProperty(result[left], operator, property); + + LOG.exit(method, result); + return result; + } + /** * Visitor design pattern; handle a condition operator. * Condition operators are operators that compare two pieces of data, such @@ -642,6 +728,24 @@ class QueryCompiler { return selector; } + /** + * Visitor design pattern; handle an array expression. + * @param {Object} ast The abstract syntax tree being visited. + * @param {Object} parameters The parameters. + * @return {Object} The result of visiting, or null. + * @private + */ + visitArrayExpression(ast, parameters) { + const method = 'visitArrayExpression'; + LOG.entry(method, ast, parameters); + const selector = ast.elements.map((element) => { + const result = this.visit(element, parameters); + return result; + }); + LOG.exit(method, selector); + return selector; + } + /** * Visitor design pattern; handle a member expression. * @param {Object} ast The abstract syntax tree being visited. diff --git a/packages/composer-runtime/test/querycompiler.js b/packages/composer-runtime/test/querycompiler.js index e7c55b16b5..08cea95cb7 100644 --- a/packages/composer-runtime/test/querycompiler.js +++ b/packages/composer-runtime/test/querycompiler.js @@ -68,6 +68,8 @@ describe('QueryCompiler', () => { o String foo o String bar o Baa baa + o String[] noises + o Meow[] meows } participant SampleParticipant identified by participantId { @@ -168,6 +170,30 @@ describe('QueryCompiler', () => { SELECT org.hyperledger.composer.system.HistorianRecord FROM HistorianRegistry } + query Q16 { + description: "Simple Historian Query" + statement: + SELECT org.acme.sample.SampleAsset + WHERE (noises CONTAINS "baa") + } + query Q17 { + description: "Simple Historian Query" + statement: + SELECT org.acme.sample.SampleAsset + WHERE (noises CONTAINS ["baa","moo"]) + } + query Q18 { + description: "Simple Historian Query" + statement: + SELECT org.acme.sample.SampleAsset + WHERE (meows CONTAINS (woof == "foo")) + } + query Q19 { + description: "Simple Historian Query" + statement: + SELECT org.acme.sample.SampleAsset + WHERE (meows CONTAINS ((woof == "foo") OR (woof == "noo"))) + } `); queryFile1.validate(); queries = {}; @@ -195,7 +221,7 @@ describe('QueryCompiler', () => { const compiledQueryBundle = queryCompiler.compile(queryManager); compiledQueryBundle.queryCompiler.should.equal(queryCompiler); compiledQueryBundle.compiledQueries.should.be.an('array'); - compiledQueryBundle.compiledQueries.should.have.lengthOf(15); + compiledQueryBundle.compiledQueries.should.have.lengthOf(19); compiledQueryBundle.compiledQueries.should.all.have.property('name'); compiledQueryBundle.compiledQueries.should.all.have.property('hash'); compiledQueryBundle.compiledQueries.should.all.have.property('generator'); @@ -208,7 +234,7 @@ describe('QueryCompiler', () => { it('should visit all of the things', () => { const compiled = queryCompiler.visit(queryManager, {}); compiled.should.be.an('array'); - compiled.should.have.lengthOf(15); + compiled.should.have.lengthOf(19); compiled.should.all.have.property('name'); compiled.should.all.have.property('hash'); compiled.should.all.have.property('generator'); @@ -227,7 +253,7 @@ describe('QueryCompiler', () => { it('should compile all queries in the query manager', () => { const compiled = queryCompiler.visitQueryManager(queryManager, {}); compiled.should.be.an('array'); - compiled.should.have.lengthOf(15); + compiled.should.have.lengthOf(19); compiled.should.all.have.property('name'); compiled.should.all.have.property('hash'); compiled.should.all.have.property('generator'); @@ -247,7 +273,7 @@ describe('QueryCompiler', () => { it('should compile all queries in the query file', () => { const compiled = queryCompiler.visitQueryFile(queryFile1, {}); compiled.should.be.an('array'); - compiled.should.have.lengthOf(15); + compiled.should.have.lengthOf(19); compiled.should.all.have.property('name'); compiled.should.all.have.property('hash'); compiled.should.all.have.property('generator'); @@ -672,6 +698,244 @@ describe('QueryCompiler', () => { }); + describe('#visitContainsOperator', () => { + + it('should throw for an expression with two literals', () => { + (() => { + queryCompiler.visitContainsOperator({ + type: 'BinaryExpression', + operator: 'CONTAINS', + left: { + type: 'Literal', + value: 'foo' + }, + right: { + type: 'Literal', + value: 'bar' + } + }); + }).should.throw(/The operator CONTAINS requires a property name/); + }); + + it('should compile an expression with a literal', () => { + const result = queryCompiler.visitContainsOperator({ + type: 'BinaryExpression', + operator: 'CONTAINS', + left: { + type: 'Identifier', + name: 'someArray' + }, + right: { + type: 'Literal', + value: 'bar' + } + }); + result.should.deep.equal({ + someArray: { + $all: [ 'bar' ] + } + }); + }); + + it('should compile an expression with a reversed literal', () => { + const result = queryCompiler.visitContainsOperator({ + type: 'BinaryExpression', + operator: 'CONTAINS', + left: { + type: 'Literal', + value: 'bar' + }, + right: { + type: 'Identifier', + name: 'someArray' + } + }); + result.should.deep.equal({ + someArray: { + $all: [ 'bar' ] + } + }); + }); + + it('should compile an expression with a parameter', () => { + const parameters = { + requiredParameters: [], + parametersToUse: { + myvar: 'bar' + } + }; + const result = queryCompiler.visitContainsOperator({ + type: 'BinaryExpression', + operator: 'CONTAINS', + left: { + type: 'Identifier', + name: 'someArray' + }, + right: { + type: 'Identifier', + name: '_$myvar' + } + }, parameters); + result.should.deep.equal({ + someArray: { + $all: [ 'bar' ] + } + }); + }); + + it('should compile an expression with a reversed parameter', () => { + const parameters = { + requiredParameters: [], + parametersToUse: { + myvar: 'bar' + } + }; + const result = queryCompiler.visitContainsOperator({ + type: 'BinaryExpression', + operator: 'CONTAINS', + left: { + type: 'Identifier', + name: '_$myvar' + }, + right: { + type: 'Identifier', + name: 'someArray' + } + }, parameters); + result.should.deep.equal({ + someArray: { + $all: [ 'bar' ] + } + }); + }); + + it('should compile an expression with an array literal', () => { + const result = queryCompiler.visitContainsOperator({ + type: 'BinaryExpression', + operator: 'CONTAINS', + left: { + type: 'Identifier', + name: 'someArray' + }, + right: { + type: 'ArrayExpression', + elements: [ + { + type: 'Literal', + value: 'foo' + }, + { + type: 'Literal', + value: 'bar' + } + ] + } + }); + result.should.deep.equal({ + someArray: { + $all: [ 'foo', 'bar' ] + } + }); + }); + + it('should compile an expression with an array parameter', () => { + const parameters = { + requiredParameters: [], + parametersToUse: { + myvar: ['foo', 'bar'] + } + }; + const result = queryCompiler.visitContainsOperator({ + type: 'BinaryExpression', + operator: 'CONTAINS', + left: { + type: 'Identifier', + name: 'someArray' + }, + right: { + type: 'Identifier', + name: '_$myvar' + } + }, parameters); + result.should.deep.equal({ + someArray: { + $all: [ 'foo', 'bar' ] + } + }); + }); + + it('should compile an expression with a nested expression with literals', () => { + const result = queryCompiler.visitContainsOperator({ + type: 'BinaryExpression', + operator: 'CONTAINS', + left: { + type: 'Identifier', + name: 'someArray' + }, + right: { + type: 'BinaryExpression', + operator: '==', + left: { + type: 'Identifier', + name: 'someProp' + }, + right: { + type: 'Literal', + value: 'foo' + } + } + }); + result.should.deep.equal({ + someArray: { + $elemMatch: { + someProp: { + $eq: 'foo' + } + } + } + }); + }); + + it('should compile an expression with a nested expression with parameter', () => { + const parameters = { + requiredParameters: [], + parametersToUse: { + myvar: 'bar' + } + }; + const result = queryCompiler.visitContainsOperator({ + type: 'BinaryExpression', + operator: 'CONTAINS', + left: { + type: 'Identifier', + name: 'someArray' + }, + right: { + type: 'BinaryExpression', + operator: '==', + left: { + type: 'Identifier', + name: 'someProp' + }, + right: { + type: 'Identifier', + name: '_$myvar' + } + } + }, parameters); + result.should.deep.equal({ + someArray: { + $elemMatch: { + someProp: { + $eq: 'bar' + } + } + } + }); + }); + + }); + describe('#visitConditionOperator', () => { it('should throw for an unrecognized operator', () => { @@ -1064,6 +1328,20 @@ describe('QueryCompiler', () => { }); + describe('#visitArrayExpression', () => { + + it('should return the array value', () => { + queryCompiler.visitArrayExpression({ + elements: [ + { type: 'Literal', value: 1234 }, + { type: 'Literal', value: 2345 }, + { type: 'Literal', value: 3456 } + ], + }, {}).should.deep.equal([1234, 2345, 3456]); + }); + + }); + describe('#visitMemberExpression', () => { it('should return a nested property value', () => { diff --git a/packages/composer-systests/systest/data/queries.cto b/packages/composer-systests/systest/data/queries.cto index 039a7e77f1..941cb3c648 100644 --- a/packages/composer-systests/systest/data/queries.cto +++ b/packages/composer-systests/systest/data/queries.cto @@ -13,6 +13,7 @@ enum SampleEnum { concept SampleConcept { o String stringValue + o String[] stringArrayValue o Double doubleValue o Integer integerValue o Long longValue @@ -25,7 +26,9 @@ asset SampleAsset identified by assetId { o String assetId --> SampleParticipant participant o SampleConcept conceptValue + o SampleConcept[] conceptArrayValue o String stringValue + o String[] stringArrayValue o Double doubleValue o Integer integerValue o Long longValue @@ -38,7 +41,9 @@ participant SampleParticipant identified by participantId { o String participantId --> SampleAsset asset o SampleConcept conceptValue + o SampleConcept[] conceptArrayValue o String stringValue + o String[] stringArrayValue o Double doubleValue o Integer integerValue o Long longValue @@ -51,7 +56,9 @@ transaction SampleTransaction { --> SampleAsset asset --> SampleParticipant participant o SampleConcept conceptValue + o SampleConcept[] conceptArrayValue o String stringValue + o String[] stringArrayValue o Double doubleValue o Integer integerValue o Long longValue diff --git a/packages/composer-systests/systest/queries.js b/packages/composer-systests/systest/queries.js index db60a65050..f290cd2c23 100644 --- a/packages/composer-systests/systest/queries.js +++ b/packages/composer-systests/systest/queries.js @@ -26,10 +26,7 @@ chai.should(); chai.use(require('chai-as-promised')); describe('Query system tests', () => { - let bnID; - beforeEach(() => { - return TestUtil.resetBusinessNetwork(bnID); - }); + let businessNetworkDefinition; let client; let assetsAsJSON; @@ -50,6 +47,12 @@ describe('Query system tests', () => { conceptValue: { $class: 'systest.queries.SampleConcept', stringValue: 'string ' + (i % 4), + stringArrayValue: [ + 'array string 0_' + (i % 4), + 'array string 1_' + (i % 4), + 'array string 2_' + (i % 4), + 'array string 3_' + (i % 4) + ], doubleValue: 2.5 * (i % 8), integerValue: 1000 * (i % 16), longValue: 100000 * (i % 32), @@ -57,7 +60,79 @@ describe('Query system tests', () => { booleanValue: (i % 2) ? true : false, enumValue: 'VALUE_' + (i % 8) }, + conceptArrayValue: [ + { + $class: 'systest.queries.SampleConcept', + stringValue: 'string ' + (i % 4), + stringArrayValue: [ + 'array string 0_' + (i % 4), + 'array string 1_' + (i % 4), + 'array string 2_' + (i % 4), + 'array string 3_' + (i % 4) + ], + doubleValue: 2.5 * (i % 8), + integerValue: 1000 * (i % 16), + longValue: 100000 * (i % 32), + dateTimeValue: new Date(100000 * (i % 16)).toISOString(), + booleanValue: (i % 2) ? true : false, + enumValue: 'VALUE_' + (i % 8) + }, + { + $class: 'systest.queries.SampleConcept', + stringValue: 'string ' + (i % 4), + stringArrayValue: [ + 'array string 0_' + (i % 4), + 'array string 1_' + (i % 4), + 'array string 2_' + (i % 4), + 'array string 3_' + (i % 4) + ], + doubleValue: 2.5 * (i % 8), + integerValue: 1000 * (i % 16), + longValue: 100000 * (i % 32), + dateTimeValue: new Date(100000 * (i % 16)).toISOString(), + booleanValue: (i % 2) ? true : false, + enumValue: 'VALUE_' + (i % 8) + }, + { + $class: 'systest.queries.SampleConcept', + stringValue: 'string ' + (i % 4), + stringArrayValue: [ + 'array string 0_' + (i % 4), + 'array string 1_' + (i % 4), + 'array string 2_' + (i % 4), + 'array string 3_' + (i % 4) + ], + doubleValue: 2.5 * (i % 8), + integerValue: 1000 * (i % 16), + longValue: 100000 * (i % 32), + dateTimeValue: new Date(100000 * (i % 16)).toISOString(), + booleanValue: (i % 2) ? true : false, + enumValue: 'VALUE_' + (i % 8) + }, + { + $class: 'systest.queries.SampleConcept', + stringValue: 'string ' + (i % 4), + stringArrayValue: [ + 'array string 0_' + (i % 4), + 'array string 1_' + (i % 4), + 'array string 2_' + (i % 4), + 'array string 3_' + (i % 4) + ], + doubleValue: 2.5 * (i % 8), + integerValue: 1000 * (i % 16), + longValue: 100000 * (i % 32), + dateTimeValue: new Date(100000 * (i % 16)).toISOString(), + booleanValue: (i % 2) ? true : false, + enumValue: 'VALUE_' + (i % 8) + } + ], stringValue: 'string ' + (i % 4), + stringArrayValue: [ + 'array string 0_' + (i % 4), + 'array string 1_' + (i % 4), + 'array string 2_' + (i % 4), + 'array string 3_' + (i % 4) + ], doubleValue: 2.5 * (i % 8), integerValue: 1000 * (i % 16), longValue: 100000 * (i % 32), @@ -97,11 +172,11 @@ describe('Query system tests', () => { return result; } - /** - * Generate a transaction. - * @param {Number} i The index. - * @return {Object} The generated transaction. - */ + /** + * Generate a transaction. + * @param {Number} i The index. + * @return {Object} The generated transaction. + */ function generateTransaction(i) { let result = { $class: 'systest.queries.SampleTransaction', @@ -134,7 +209,6 @@ describe('Query system tests', () => { let scriptManager = businessNetworkDefinition.getScriptManager(); scriptManager.addScript(scriptManager.createScript(scriptFile.identifier, 'JS', scriptFile.contents)); }); - bnID = businessNetworkDefinition.getName(); return TestUtil.deploy(businessNetworkDefinition, true) .then(() => { return TestUtil.getClient('systest-queries') @@ -164,15 +238,8 @@ describe('Query system tests', () => { participantsAsJSON.sort((a, b) => { return a.participantId.localeCompare(b.participantId); }); - }); - }); - - after(function () { - return TestUtil.undeploy(businessNetworkDefinition); - }); - - beforeEach(function () { - return client.getAssetRegistry('systest.queries.SampleAsset') + return client.getAssetRegistry('systest.queries.SampleAsset'); + }) .then((assetRegistry) => { return assetRegistry.addAll(assetsAsResources); }) @@ -207,6 +274,10 @@ describe('Query system tests', () => { }); }); + after(function () { + return TestUtil.undeploy(businessNetworkDefinition); + }); + ['assets', 'participants', 'transactions'].forEach((type) => { describe('#' + type, () => { @@ -329,6 +400,153 @@ describe('Query system tests', () => { }); }); + it('should execute a dynamic query on a nested string property using a parameter', () => { + const query = client.buildQuery(`SELECT ${resource} WHERE (conceptValue.stringValue == _$inputStringValue)`); + return client.query(query, { inputStringValue: 'string 1' }) + .then((resources) => { + const actual = resources.map((resource) => { + return serializer.toJSON(resource); + }); + actual.should.deep.equal(expected.filter((thing) => { + return thing.conceptValue.stringValue === 'string 1'; + })); + }); + }); + + it('should execute a dynamic query on a string array property using CONTAINS with a string literal', () => { + const query = client.buildQuery(`SELECT ${resource} WHERE (stringArrayValue CONTAINS 'array string 0_3')`); + return client.query(query) + .then((resources) => { + const actual = resources.map((resource) => { + return serializer.toJSON(resource); + }); + actual.should.deep.equal(expected.filter((thing) => { + return thing.stringArrayValue.indexOf('array string 0_3') > -1; + })); + }); + }); + + it('should execute a dynamic query on a string array property using CONTAINS with a string array literal', () => { + const query = client.buildQuery(`SELECT ${resource} WHERE (stringArrayValue CONTAINS ['array string 0_1', 'array string 0_3'])`); + return client.query(query) + .then((resources) => { + const actual = resources.map((resource) => { + return serializer.toJSON(resource); + }); + actual.should.deep.equal(expected.filter((thing) => { + return thing.stringArrayValue.indexOf('array string 0_1') > -1 && thing.stringArrayValue.indexOf('array string 0_3') > -1; + })); + }); + }); + + it('should execute a dynamic query on a string array property using CONTAINS with a string parameter', () => { + const query = client.buildQuery(`SELECT ${resource} WHERE (stringArrayValue CONTAINS _$inputStringArrayValue)`); + return client.query(query, { inputStringArrayValue: 'array string 1_2' }) + .then((resources) => { + const actual = resources.map((resource) => { + return serializer.toJSON(resource); + }); + actual.should.deep.equal(expected.filter((thing) => { + return thing.stringArrayValue.indexOf('array string 1_2') > -1; + })); + }); + }); + + it('should execute a dynamic query on a string array property using CONTAINS with a string array parameter', () => { + const query = client.buildQuery(`SELECT ${resource} WHERE (stringArrayValue CONTAINS _$inputStringArrayValue)`); + return client.query(query, { inputStringArrayValue: [ 'array string 1_0', 'array string 1_2' ] }) + .then((resources) => { + const actual = resources.map((resource) => { + return serializer.toJSON(resource); + }); + actual.should.deep.equal(expected.filter((thing) => { + return thing.stringArrayValue.indexOf('array string 1_0') > -1 && thing.stringArrayValue.indexOf('array string 1_2') > -1; + })); + }); + }); + + it('should execute a dynamic query on a nested string array property using CONTAINS with a string literal', () => { + const query = client.buildQuery(`SELECT ${resource} WHERE (conceptValue.stringArrayValue CONTAINS 'array string 0_3')`); + return client.query(query) + .then((resources) => { + const actual = resources.map((resource) => { + return serializer.toJSON(resource); + }); + actual.should.deep.equal(expected.filter((thing) => { + return thing.conceptValue.stringArrayValue.indexOf('array string 0_3') > -1; + })); + }); + }); + + it('should execute a dynamic query on a nested string array property using CONTAINS with a string array literal', () => { + const query = client.buildQuery(`SELECT ${resource} WHERE (conceptValue.stringArrayValue CONTAINS ['array string 0_1', 'array string 0_3'])`); + return client.query(query) + .then((resources) => { + const actual = resources.map((resource) => { + return serializer.toJSON(resource); + }); + actual.should.deep.equal(expected.filter((thing) => { + return thing.conceptValue.stringArrayValue.indexOf('array string 0_1') > -1 && thing.conceptValue.stringArrayValue.indexOf('array string 0_3') > -1; + })); + }); + }); + + it('should execute a dynamic query on a nested string array property using CONTAINS with a string parameter', () => { + const query = client.buildQuery(`SELECT ${resource} WHERE (conceptValue.stringArrayValue CONTAINS _$inputStringArrayValue)`); + return client.query(query, { inputStringArrayValue: 'array string 1_2' }) + .then((resources) => { + const actual = resources.map((resource) => { + return serializer.toJSON(resource); + }); + actual.should.deep.equal(expected.filter((thing) => { + return thing.conceptValue.stringArrayValue.indexOf('array string 1_2') > -1; + })); + }); + }); + + it('should execute a dynamic query on a nested string array property using CONTAINS with a string array parameter', () => { + const query = client.buildQuery(`SELECT ${resource} WHERE (conceptValue.stringArrayValue CONTAINS _$inputStringArrayValue)`); + return client.query(query, { inputStringArrayValue: [ 'array string 1_0', 'array string 1_2' ] }) + .then((resources) => { + const actual = resources.map((resource) => { + return serializer.toJSON(resource); + }); + actual.should.deep.equal(expected.filter((thing) => { + return thing.conceptValue.stringArrayValue.indexOf('array string 1_0') > -1 && thing.conceptValue.stringArrayValue.indexOf('array string 1_2') > -1; + })); + }); + }); + + it('should execute a dynamic query on a concept array property using CONTAINS with a nested expression', () => { + const query = client.buildQuery(`SELECT ${resource} WHERE (conceptArrayValue CONTAINS (stringValue == 'string 2'))`); + return client.query(query) + .then((resources) => { + const actual = resources.map((resource) => { + return serializer.toJSON(resource); + }); + actual.should.deep.equal(expected.filter((thing) => { + return thing.conceptArrayValue.some((concept) => { + return concept.stringValue === 'string 2'; + }); + })); + }); + }); + + it('should execute a dynamic query on a concept array property using CONTAINS with a nested expression', () => { + const query = client.buildQuery(`SELECT ${resource} WHERE (conceptArrayValue CONTAINS (stringValue == _$inputStringValue))`); + return client.query(query, { inputStringValue: 'string 3' }) + .then((resources) => { + const actual = resources.map((resource) => { + return serializer.toJSON(resource); + }); + actual.should.deep.equal(expected.filter((thing) => { + return thing.conceptArrayValue.some((concept) => { + return concept.stringValue === 'string 3'; + }); + })); + }); + }); + // Hyperledger Fabric v1.0.0 is dumb and overwrites any limit/skip fields we send in. // https://jira.hyperledger.org/browse/FAB-5369 if (!TestUtil.isHyperledgerFabricV1()) { diff --git a/packages/composer-systests/systest/transactions.queries.js b/packages/composer-systests/systest/transactions.queries.js index 1b308b2e39..6d6bfc0cf2 100644 --- a/packages/composer-systests/systest/transactions.queries.js +++ b/packages/composer-systests/systest/transactions.queries.js @@ -26,10 +26,6 @@ chai.should(); chai.use(require('chai-as-promised')); describe('Transaction (query specific) system tests', () => { - let bnID; - beforeEach(() => { - return TestUtil.resetBusinessNetwork(bnID); - }); let businessNetworkDefinition; let client; @@ -119,7 +115,6 @@ describe('Transaction (query specific) system tests', () => { let scriptManager = businessNetworkDefinition.getScriptManager(); scriptManager.addScript(scriptManager.createScript(scriptFile.identifier, 'JS', scriptFile.contents)); }); - bnID = businessNetworkDefinition.getName(); return TestUtil.deploy(businessNetworkDefinition, true) .then(() => { return TestUtil.getClient('systest-transactions-queries') @@ -146,15 +141,8 @@ describe('Transaction (query specific) system tests', () => { participantsAsJSON.sort(function (a, b) { return a.participantId.localeCompare(b.participantId); }); - }); - }); - - after(function () { - return TestUtil.undeploy(businessNetworkDefinition); - }); - - beforeEach(function () { - return client.getAssetRegistry('systest.transactions.queries.SampleAsset') + return client.getAssetRegistry('systest.transactions.queries.SampleAsset'); + }) .then((assetRegistry) => { return assetRegistry.addAll(assetsAsResources); }) @@ -166,6 +154,10 @@ describe('Transaction (query specific) system tests', () => { }); }); + after(function () { + return TestUtil.undeploy(businessNetworkDefinition); + }); + ['assets', 'participants'].forEach((type) => { describe('#' + type, () => { diff --git a/packages/composer-website/jekylldocs/business-network/query.md b/packages/composer-website/jekylldocs/business-network/query.md index d63c0d9a17..40325d911f 100644 --- a/packages/composer-website/jekylldocs/business-network/query.md +++ b/packages/composer-website/jekylldocs/business-network/query.md @@ -1,6 +1,6 @@ --- layout: default -title: Using Filters and Queries with Business Network Data +title: Using Queries and Filters with Business Network Data category: tasks section: business-network index-order: 507 diff --git a/packages/composer-website/jekylldocs/reference/query-language.md b/packages/composer-website/jekylldocs/reference/query-language.md index 32cc74f7ca..00a772e210 100644 --- a/packages/composer-website/jekylldocs/reference/query-language.md +++ b/packages/composer-website/jekylldocs/reference/query-language.md @@ -28,6 +28,7 @@ The `statement` property contains the defining rules of the query, and can have - `WHERE` is an optional operator which defines the conditions to be applied to the registry data. - `AND` is an optional operator which defines additional conditions. - `OR` is an optional operator which defines alternative conditions. +- `CONTAINS` is an optional operator that defines conditions for array values - `ORDER BY` is an optional operator which defines the sorting or results. - `SKIP` is an optional operator which defines the number of results to skip. - `LIMIT` is an optional operator which defines the maximum number of results to return from a query, by default limit is set at 25. From dacd121886341a51dfae0b23ce123f2ff0c677e5 Mon Sep 17 00:00:00 2001 From: fabric-composer-app Date: Thu, 26 Oct 2017 11:27:43 +0000 Subject: [PATCH 7/9] Automatic version bump to 0.14.3 --- lerna.json | 2 +- package.json | 2 +- packages/composer-admin/package.json | 6 +++--- packages/composer-cli/package.json | 10 +++++----- packages/composer-client/package.json | 6 +++--- packages/composer-common/package.json | 2 +- .../composer-connector-embedded/package.json | 8 ++++---- packages/composer-connector-hlfv1/package.json | 6 +++--- packages/composer-connector-proxy/package.json | 4 ++-- .../composer-connector-server/package.json | 8 ++++---- packages/composer-connector-web/package.json | 8 ++++---- packages/composer-cucumber-steps/package.json | 10 +++++----- packages/composer-playground-api/package.json | 6 +++--- packages/composer-playground/package.json | 18 +++++++++--------- packages/composer-rest-server/package.json | 12 ++++++------ .../composer-runtime-embedded/package.json | 8 ++++---- packages/composer-runtime-hlfv1/package.json | 4 ++-- packages/composer-runtime-pouchdb/package.json | 6 +++--- packages/composer-runtime-web/package.json | 8 ++++---- packages/composer-runtime/package.json | 4 ++-- packages/composer-systests/package.json | 16 ++++++++-------- packages/composer-website/package.json | 10 +++++----- .../package.json | 10 +++++----- .../loopback-connector-composer/package.json | 10 +++++----- 24 files changed, 92 insertions(+), 92 deletions(-) diff --git a/lerna.json b/lerna.json index d77859bfcd..a248f08393 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "packages/*" ], - "version": "0.14.2", + "version": "0.14.3", "hoist": true } diff --git a/package.json b/package.json index 7f5e6820df..cc8134a63c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "name": "composer", "description": "You must install [Lerna](https://lernajs.io) to build this multi-package repository.", - "version": "0.14.2", + "version": "0.14.3", "main": "index.js", "private": true, "scripts": { diff --git a/packages/composer-admin/package.json b/packages/composer-admin/package.json index d64b1d90e6..ad8146dfd9 100644 --- a/packages/composer-admin/package.json +++ b/packages/composer-admin/package.json @@ -1,6 +1,6 @@ { "name": "composer-admin", - "version": "0.14.2", + "version": "0.14.3", "description": "Hyperledger Composer Admin, code that manages business networks deployed to Hyperledger Fabric", "engines": { "node": ">=6", @@ -41,8 +41,8 @@ "sinon": "2.3.8" }, "dependencies": { - "composer-common": "0.14.2", - "composer-connector-hlfv1": "0.14.2" + "composer-common": "0.14.3", + "composer-connector-hlfv1": "0.14.3" }, "license-check-config": { "src": [ diff --git a/packages/composer-cli/package.json b/packages/composer-cli/package.json index 92215df95c..61e4f358ab 100644 --- a/packages/composer-cli/package.json +++ b/packages/composer-cli/package.json @@ -1,6 +1,6 @@ { "name": "composer-cli", - "version": "0.14.2", + "version": "0.14.3", "description": "Hyperledger Composer command line interfaces (CLIs)", "engines": { "node": ">=6", @@ -41,10 +41,10 @@ "dependencies": { "chalk": "1.1.3", "cli-table": "0.3.1", - "composer-admin": "0.14.2", - "composer-client": "0.14.2", - "composer-common": "0.14.2", - "composer-rest-server": "0.14.2", + "composer-admin": "0.14.3", + "composer-client": "0.14.3", + "composer-common": "0.14.3", + "composer-rest-server": "0.14.3", "homedir": "0.6.0", "mkdirp": "0.5.1", "npm-paths": "0.1.3", diff --git a/packages/composer-client/package.json b/packages/composer-client/package.json index b9d2bc0e3b..550b84708d 100644 --- a/packages/composer-client/package.json +++ b/packages/composer-client/package.json @@ -1,6 +1,6 @@ { "name": "composer-client", - "version": "0.14.2", + "version": "0.14.3", "description": "The node.js client library for Hyperledger Composer, a development framework for Hyperledger Fabric", "engines": { "node": ">=6", @@ -44,8 +44,8 @@ "logError": true }, "dependencies": { - "composer-common": "0.14.2", - "composer-connector-hlfv1": "0.14.2", + "composer-common": "0.14.3", + "composer-connector-hlfv1": "0.14.3", "uuid": "3.0.1" }, "devDependencies": { diff --git a/packages/composer-common/package.json b/packages/composer-common/package.json index d00000faef..1dc406edae 100644 --- a/packages/composer-common/package.json +++ b/packages/composer-common/package.json @@ -1,6 +1,6 @@ { "name": "composer-common", - "version": "0.14.2", + "version": "0.14.3", "description": "Hyperledger Composer Common, code that is common across client, admin and runtime.", "engines": { "node": ">=6", diff --git a/packages/composer-connector-embedded/package.json b/packages/composer-connector-embedded/package.json index bb6906d58b..3a5ca8abbf 100644 --- a/packages/composer-connector-embedded/package.json +++ b/packages/composer-connector-embedded/package.json @@ -1,6 +1,6 @@ { "name": "composer-connector-embedded", - "version": "0.14.2", + "version": "0.14.3", "description": "The embedded client connector for Hyperledger Composer", "engines": { "node": ">=6", @@ -51,9 +51,9 @@ "watchify": "3.7.0" }, "dependencies": { - "composer-common": "0.14.2", - "composer-runtime": "0.14.2", - "composer-runtime-embedded": "0.14.2" + "composer-common": "0.14.3", + "composer-runtime": "0.14.3", + "composer-runtime-embedded": "0.14.3" }, "nyc": { "exclude": [ diff --git a/packages/composer-connector-hlfv1/package.json b/packages/composer-connector-hlfv1/package.json index d9001d9ce3..dbbe091b8e 100644 --- a/packages/composer-connector-hlfv1/package.json +++ b/packages/composer-connector-hlfv1/package.json @@ -1,6 +1,6 @@ { "name": "composer-connector-hlfv1", - "version": "0.14.2", + "version": "0.14.3", "description": "The Hyperledger Fabric v1.x Client connector for Hyperledger Composer", "engines": { "node": ">=6", @@ -40,8 +40,8 @@ "logError": true }, "dependencies": { - "composer-common": "0.14.2", - "composer-runtime-hlfv1": "0.14.2", + "composer-common": "0.14.3", + "composer-runtime-hlfv1": "0.14.3", "fabric-ca-client": "1.0.2", "fabric-client": "1.0.2", "fs-extra": "1.0.0", diff --git a/packages/composer-connector-proxy/package.json b/packages/composer-connector-proxy/package.json index c633bc3025..19919306b0 100644 --- a/packages/composer-connector-proxy/package.json +++ b/packages/composer-connector-proxy/package.json @@ -1,6 +1,6 @@ { "name": "composer-connector-proxy", - "version": "0.14.2", + "version": "0.14.3", "description": "The proxying client connector for Hyperledger Composer", "engines": { "node": ">=6", @@ -53,7 +53,7 @@ "watchify": "3.7.0" }, "dependencies": { - "composer-common": "0.14.2", + "composer-common": "0.14.3", "socket.io-client": "1.7.3" }, "nyc": { diff --git a/packages/composer-connector-server/package.json b/packages/composer-connector-server/package.json index e88c838fb6..349b909339 100644 --- a/packages/composer-connector-server/package.json +++ b/packages/composer-connector-server/package.json @@ -1,6 +1,6 @@ { "name": "composer-connector-server", - "version": "0.14.2", + "version": "0.14.3", "description": "The remote connector server for Hyperledger Composer", "engines": { "node": ">=6", @@ -44,8 +44,8 @@ "logError": true }, "dependencies": { - "composer-common": "0.14.2", - "composer-connector-hlfv1": "0.14.2", + "composer-common": "0.14.3", + "composer-connector-hlfv1": "0.14.3", "serializerr": "1.0.3", "socket.io": "1.7.3", "uuid": "3.0.1", @@ -53,7 +53,7 @@ }, "devDependencies": { "chai": "3.5.0", - "composer-connector-embedded": "0.14.2", + "composer-connector-embedded": "0.14.3", "eslint": "3.17.1", "jsdoc": "3.4.3", "license-check": "1.1.5", diff --git a/packages/composer-connector-web/package.json b/packages/composer-connector-web/package.json index 8395297b72..68bbec344c 100644 --- a/packages/composer-connector-web/package.json +++ b/packages/composer-connector-web/package.json @@ -1,6 +1,6 @@ { "name": "composer-connector-web", - "version": "0.14.2", + "version": "0.14.3", "description": "The web client connector for Hyperledger Composer", "engines": { "node": ">=6", @@ -61,9 +61,9 @@ "watchify": "3.7.0" }, "dependencies": { - "composer-common": "0.14.2", - "composer-runtime": "0.14.2", - "composer-runtime-web": "0.14.2", + "composer-common": "0.14.3", + "composer-runtime": "0.14.3", + "composer-runtime-web": "0.14.3", "uuid": "3.0.1" } } diff --git a/packages/composer-cucumber-steps/package.json b/packages/composer-cucumber-steps/package.json index de38002bf2..5a1b7f31f2 100644 --- a/packages/composer-cucumber-steps/package.json +++ b/packages/composer-cucumber-steps/package.json @@ -1,6 +1,6 @@ { "name": "composer-cucumber-steps", - "version": "0.14.2", + "version": "0.14.3", "description": "A library of Cucumber steps for testing Hyperledger Composer", "main": "index.js", "scripts": { @@ -70,10 +70,10 @@ "dependencies": { "browserfs": "1.1.0", "chai": "3.5.0", - "composer-admin": "0.14.2", - "composer-client": "0.14.2", - "composer-common": "0.14.2", - "composer-connector-embedded": "0.14.2", + "composer-admin": "0.14.3", + "composer-client": "0.14.3", + "composer-common": "0.14.3", + "composer-connector-embedded": "0.14.3", "thenify-all": "1.6.0" } } diff --git a/packages/composer-playground-api/package.json b/packages/composer-playground-api/package.json index 2e3899c08a..80f2e6e573 100644 --- a/packages/composer-playground-api/package.json +++ b/packages/composer-playground-api/package.json @@ -1,6 +1,6 @@ { "name": "composer-playground-api", - "version": "0.14.2", + "version": "0.14.3", "description": "The REST API for the Hyperledger Composer Playground", "engines": { "node": ">=6", @@ -58,8 +58,8 @@ "dependencies": { "async": "2.5.0", "body-parser": "1.17.0", - "composer-common": "0.14.2", - "composer-connector-server": "0.14.2", + "composer-common": "0.14.3", + "composer-connector-server": "0.14.3", "dotenv": "4.0.0", "express": "4.15.2", "http-status": "1.0.1", diff --git a/packages/composer-playground/package.json b/packages/composer-playground/package.json index 36678c3b7d..b40350cfdc 100644 --- a/packages/composer-playground/package.json +++ b/packages/composer-playground/package.json @@ -1,6 +1,6 @@ { "name": "composer-playground", - "version": "0.14.2", + "version": "0.14.3", "description": "A test harness/UI for the web runtime container for Hyperledger Composer", "engines": { "node": ">=6", @@ -77,8 +77,8 @@ "dependencies": { "@ng-bootstrap/ng-bootstrap": "1.0.0-beta.2", "cheerio": "0.22.0", - "composer-common": "0.14.2", - "composer-playground-api": "0.14.2", + "composer-common": "0.14.3", + "composer-playground-api": "0.14.3", "express": "4.15.2", "fast-json-patch": "1.1.8", "file-saver": "1.3.3", @@ -131,12 +131,12 @@ "chai": "3.5.0", "codelyzer": "2.0.1", "codemirror": "5.26.0", - "composer-admin": "0.14.2", - "composer-client": "0.14.2", - "composer-connector-proxy": "0.14.2", - "composer-connector-web": "0.14.2", - "composer-runtime": "0.14.2", - "composer-runtime-web": "0.14.2", + "composer-admin": "0.14.3", + "composer-client": "0.14.3", + "composer-connector-proxy": "0.14.3", + "composer-connector-web": "0.14.3", + "composer-runtime": "0.14.3", + "composer-runtime-web": "0.14.3", "copy-webpack-plugin": "4.0.1", "core-js": "2.4.1", "css-loader": "0.26.1", diff --git a/packages/composer-rest-server/package.json b/packages/composer-rest-server/package.json index c6dcb41b40..039e327142 100644 --- a/packages/composer-rest-server/package.json +++ b/packages/composer-rest-server/package.json @@ -1,6 +1,6 @@ { "name": "composer-rest-server", - "version": "0.14.2", + "version": "0.14.3", "description": "Hyperledger Composer REST server that uses the Hyperledger Composer LoopBack connector", "engines": { "node": ">=6", @@ -35,7 +35,7 @@ "dependencies": { "body-parser": "1.17.0", "clui": "0.3.1", - "composer-common": "0.14.2", + "composer-common": "0.14.3", "compression": "1.0.3", "connect-ensure-login": "0.1.1", "cookie-parser": "1.4.3", @@ -50,7 +50,7 @@ "loopback-boot": "2.25.0", "loopback-component-explorer": "4.1.1", "loopback-component-passport": "3.2.0", - "loopback-connector-composer": "0.14.2", + "loopback-connector-composer": "0.14.3", "passport-local": "1.0.0", "serve-favicon": "2.0.1", "strong-error-handler": "1.0.1", @@ -63,9 +63,9 @@ "chai-as-promised": "6.0.0", "chai-http": "3.0.0", "clone": "2.1.1", - "composer-admin": "0.14.2", - "composer-client": "0.14.2", - "composer-connector-embedded": "0.14.2", + "composer-admin": "0.14.3", + "composer-client": "0.14.3", + "composer-connector-embedded": "0.14.3", "eslint": "3.17.1", "jsdoc": "3.4.3", "ldapjs": "1.0.1", diff --git a/packages/composer-runtime-embedded/package.json b/packages/composer-runtime-embedded/package.json index d62f0e6b1e..f074da3633 100644 --- a/packages/composer-runtime-embedded/package.json +++ b/packages/composer-runtime-embedded/package.json @@ -1,6 +1,6 @@ { "name": "composer-runtime-embedded", - "version": "0.14.2", + "version": "0.14.3", "description": "The embedded runtime container for Hyperledger Composer", "engines": { "node": ">=6", @@ -51,9 +51,9 @@ "logError": true }, "dependencies": { - "composer-common": "0.14.2", - "composer-runtime": "0.14.2", - "composer-runtime-pouchdb": "0.14.2", + "composer-common": "0.14.3", + "composer-runtime": "0.14.3", + "composer-runtime-pouchdb": "0.14.3", "debug": "2.6.2", "istanbul-lib-instrument": "1.7.2", "pouchdb-adapter-memory": "6.2.0", diff --git a/packages/composer-runtime-hlfv1/package.json b/packages/composer-runtime-hlfv1/package.json index bbc5305f96..cd6402a2b5 100644 --- a/packages/composer-runtime-hlfv1/package.json +++ b/packages/composer-runtime-hlfv1/package.json @@ -1,6 +1,6 @@ { "name": "composer-runtime-hlfv1", - "version": "0.14.2", + "version": "0.14.3", "description": "The Hyperledger Fabric v1.x runtime container for Hyperledger Composer", "engines": { "node": ">=6", @@ -28,7 +28,7 @@ "babelify": "7.3.0", "browserify": "13.3.0", "browserify-replace": "0.9.0", - "composer-runtime": "0.14.2", + "composer-runtime": "0.14.3", "exorcist": "0.4.0", "fs-extra": "1.0.0", "uglify-js": "2.7.5" diff --git a/packages/composer-runtime-pouchdb/package.json b/packages/composer-runtime-pouchdb/package.json index 35d6ae9dc6..2f456616f3 100644 --- a/packages/composer-runtime-pouchdb/package.json +++ b/packages/composer-runtime-pouchdb/package.json @@ -1,6 +1,6 @@ { "name": "composer-runtime-pouchdb", - "version": "0.14.2", + "version": "0.14.3", "description": "Common PouchDB based runtime container code for Hyperledger Composer", "engines": { "node": ">=6", @@ -51,8 +51,8 @@ "logError": true }, "dependencies": { - "composer-common": "0.14.2", - "composer-runtime": "0.14.2", + "composer-common": "0.14.3", + "composer-runtime": "0.14.3", "debug": "2.6.2", "istanbul-lib-instrument": "1.7.2", "pouchdb-collate": "6.2.0", diff --git a/packages/composer-runtime-web/package.json b/packages/composer-runtime-web/package.json index cc69e5ac1f..6a24363fe3 100644 --- a/packages/composer-runtime-web/package.json +++ b/packages/composer-runtime-web/package.json @@ -1,6 +1,6 @@ { "name": "composer-runtime-web", - "version": "0.14.2", + "version": "0.14.3", "description": "The web runtime container for Hyperledger Composer", "engines": { "node": ">=6", @@ -61,9 +61,9 @@ "logError": true }, "dependencies": { - "composer-common": "0.14.2", - "composer-runtime": "0.14.2", - "composer-runtime-pouchdb": "0.14.2", + "composer-common": "0.14.3", + "composer-runtime": "0.14.3", + "composer-runtime-pouchdb": "0.14.3", "pouchdb-adapter-idb": "6.2.0", "pouchdb-adapter-websql": "6.2.0", "uuid": "3.0.1", diff --git a/packages/composer-runtime/package.json b/packages/composer-runtime/package.json index 1008a1904a..34ae17928a 100644 --- a/packages/composer-runtime/package.json +++ b/packages/composer-runtime/package.json @@ -1,6 +1,6 @@ { "name": "composer-runtime", - "version": "0.14.2", + "version": "0.14.3", "description": "The runtime execution environment for Hyperledger Composer", "engines": { "node": ">=6", @@ -63,7 +63,7 @@ "logError": true }, "dependencies": { - "composer-common": "0.14.2", + "composer-common": "0.14.3", "debug": "2.6.2", "fast-json-patch": "1.1.8", "lru-cache": "4.0.2", diff --git a/packages/composer-systests/package.json b/packages/composer-systests/package.json index 51b646fbe3..1502578aee 100644 --- a/packages/composer-systests/package.json +++ b/packages/composer-systests/package.json @@ -1,6 +1,6 @@ { "name": "composer-systests", - "version": "0.14.2", + "version": "0.14.3", "private": true, "description": "System tests and automation for Hyperledger Composer", "engines": { @@ -42,13 +42,13 @@ "chai": "3.5.0", "chai-as-promised": "6.0.0", "chai-subset": "1.3.0", - "composer-admin": "0.14.2", - "composer-client": "0.14.2", - "composer-common": "0.14.2", - "composer-connector-embedded": "0.14.2", - "composer-connector-proxy": "0.14.2", - "composer-connector-server": "0.14.2", - "composer-connector-web": "0.14.2", + "composer-admin": "0.14.3", + "composer-client": "0.14.3", + "composer-common": "0.14.3", + "composer-connector-embedded": "0.14.3", + "composer-connector-proxy": "0.14.3", + "composer-connector-server": "0.14.3", + "composer-connector-web": "0.14.3", "dockerode": "2.5.1", "eslint": "3.17.1", "homedir": "0.6.0", diff --git a/packages/composer-website/package.json b/packages/composer-website/package.json index 3a037a7bd4..e1b230e816 100644 --- a/packages/composer-website/package.json +++ b/packages/composer-website/package.json @@ -1,6 +1,6 @@ { "name": "composer-website", - "version": "0.14.2", + "version": "0.14.3", "private": true, "description": "Hyperledger Composer is a blockchain development framework for Hyperledger Fabric: a library of assets/functions for creating blockchain-based applications.", "engines": { @@ -32,10 +32,10 @@ "author": "Hyperledger Composer", "license": "Apache-2.0", "devDependencies": { - "composer-admin": "0.14.2", - "composer-client": "0.14.2", - "composer-common": "0.14.2", - "composer-runtime": "0.14.2", + "composer-admin": "0.14.3", + "composer-client": "0.14.3", + "composer-common": "0.14.3", + "composer-runtime": "0.14.3", "jsdoc": "3.4.3", "mkdirp": "0.5.1", "node-plantuml": "0.5.0", diff --git a/packages/generator-hyperledger-composer/package.json b/packages/generator-hyperledger-composer/package.json index 7d6b5320e8..87fd34c7a7 100755 --- a/packages/generator-hyperledger-composer/package.json +++ b/packages/generator-hyperledger-composer/package.json @@ -1,6 +1,6 @@ { "name": "generator-hyperledger-composer", - "version": "0.14.2", + "version": "0.14.3", "description": "Generates projects from Hyperledger Composer business network definitions", "engines": { "node": ">=6", @@ -15,8 +15,8 @@ "liveNetworkTest": "mocha -t 0 test/angular-network.js" }, "dependencies": { - "composer-client": "0.14.2", - "composer-common": "0.14.2", + "composer-client": "0.14.3", + "composer-common": "0.14.3", "shelljs": "0.7.7", "underscore.string": "3.3.4", "yeoman-generator": "0.24.1" @@ -29,8 +29,8 @@ "license": "Apache-2.0", "devDependencies": { "@angular/cli": "1.0.0-rc.0", - "composer-admin": "0.14.2", - "composer-connector-embedded": "0.14.2", + "composer-admin": "0.14.3", + "composer-connector-embedded": "0.14.3", "mocha": "3.4.2", "typings": "2.1.0", "yeoman-assert": "3.0.0", diff --git a/packages/loopback-connector-composer/package.json b/packages/loopback-connector-composer/package.json index cd9457e2d7..1a1625080c 100644 --- a/packages/loopback-connector-composer/package.json +++ b/packages/loopback-connector-composer/package.json @@ -1,6 +1,6 @@ { "name": "loopback-connector-composer", - "version": "0.14.2", + "version": "0.14.3", "description": "A Loopback connector for Hyperledger Composer", "engines": { "node": ">=6", @@ -27,8 +27,8 @@ "author": "Hyperledger Composer", "license": "Apache-2.0", "dependencies": { - "composer-client": "0.14.2", - "composer-common": "0.14.2", + "composer-client": "0.14.3", + "composer-common": "0.14.3", "loopback": "3.4.0", "loopback-connector": "4.0.0", "node-cache": "4.1.1" @@ -36,8 +36,8 @@ "devDependencies": { "chai": "3.5.0", "chai-as-promised": "6.0.0", - "composer-admin": "0.14.2", - "composer-connector-embedded": "0.14.2", + "composer-admin": "0.14.3", + "composer-connector-embedded": "0.14.3", "eslint": "3.17.1", "jsdoc": "3.4.3", "license-check": "1.1.5", From ea60b77d4d5f01c6bba07cfe339cf45687dea57c Mon Sep 17 00:00:00 2001 From: Dave Kelsey Date: Thu, 26 Oct 2017 13:46:19 +0100 Subject: [PATCH 8/9] Deploy fails if already deployed or undeployed (#2474) Addresses issue https://github.com/hyperledger/composer/issues/2384 to stop deploy from reporting it was successful if business network is already deployed or has been previously undeployed. Signed-off-by: Dave Kelsey --- .../lib/hlfconnection.js | 30 +++++++-- .../test/hlfconnection.js | 64 +++++++++++-------- 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/packages/composer-connector-hlfv1/lib/hlfconnection.js b/packages/composer-connector-hlfv1/lib/hlfconnection.js index 66a9458c0c..4863bf7c44 100644 --- a/packages/composer-connector-hlfv1/lib/hlfconnection.js +++ b/packages/composer-connector-hlfv1/lib/hlfconnection.js @@ -311,7 +311,8 @@ class HLFConnection extends Connection { * @param {any} securityContext the security context * @param {string} businessNetworkIdentifier the business network name * @param {object} installOptions any relevant install options - * @returns {Promise} a promise for install completion + * @returns {Promise} a promise which resolves to true if chaincode was installed, false otherwise (if ignoring installed errors) + * @throws {Error} if chaincode was not installed and told not to ignore this scenario */ install(securityContext, businessNetworkIdentifier, installOptions) { const method = 'install'; @@ -370,15 +371,21 @@ class HLFConnection extends Connection { .then((results) => { LOG.debug(method, `Received ${results.length} results(s) from installing the chaincode`, results); if (installOptions && installOptions.ignoreCCInstalled) { - this._validateResponses(results[0], false, /chaincode .+ exists/); + let errorIgnored = this._validateResponses(results[0], false, /chaincode .+ exists/); LOG.debug(method, 'chaincode installed, or already installed'); + + // if the error was ignored then no chaincode was installed + return !errorIgnored; } else { this._validateResponses(results[0], false); LOG.debug(method, 'chaincode installed'); + return true; } }) - .then(() => { - LOG.exit(method); + .then((chaincodeInstalled) => { + LOG.exit(method, chaincodeInstalled); + return chaincodeInstalled; + }) .catch((error) => { const newError = new Error('Error trying install composer runtime. ' + error); @@ -525,9 +532,11 @@ class HLFConnection extends Connection { } LOG.debug(method, 'installing composer runtime chaincode'); + let chaincodeInstalled; return this.install(securityContext, businessNetworkIdentifier, {ignoreCCInstalled: true}) - .then(() => { + .then((chaincodeInstalled_) => { // check to see if the chaincode is already instantiated + chaincodeInstalled = chaincodeInstalled_; return this.channel.queryInstantiatedChaincodes(); }) .then((queryResults) => { @@ -537,6 +546,10 @@ class HLFConnection extends Connection { }); if (alreadyInstantiated) { LOG.debug(method, 'chaincode already instantiated'); + if (!chaincodeInstalled) { + // chaincode was not installed so must have been installed already. + throw new Error('Business network has already been deployed or undeployed and cannot be deployed again.'); + } return Promise.resolve(); } return this.start(securityContext, businessNetworkIdentifier, deployTransaction, deployOptions); @@ -558,6 +571,7 @@ class HLFConnection extends Connection { * @param {any} responses the responses from the install, instantiate or invoke * @param {boolean} isProposal true is the responses are from a proposal * @param {regexp} pattern optional regular expression for message which isn't an error + * @return {boolean} true if error was ignored as per pattern request, false otherwise * @throws if not valid */ _validateResponses(responses, isProposal, pattern) { @@ -568,6 +582,7 @@ class HLFConnection extends Connection { throw new Error('No results were returned from the request'); } + let errorsIgnored = false; responses.forEach((responseContent) => { if (responseContent instanceof Error) { // check to see if we should ignore the error, this also means we cannot verify the proposal @@ -575,6 +590,7 @@ class HLFConnection extends Connection { if (!pattern || !pattern.test(responseContent.message)) { throw responseContent; } + errorsIgnored = true; } else { // not an error, if it is from a proposal, verify the response @@ -588,7 +604,6 @@ class HLFConnection extends Connection { throw new Error('Unexpected response of ' + responseContent.response.status + '. payload was :' +responseContent.response.payload); } } - }); // if it was a proposal and all the responses were good, check that they compare @@ -598,7 +613,8 @@ class HLFConnection extends Connection { if (isProposal && !this.channel.compareProposalResponseResults(responses)) { LOG.warn('Peers do not agree, Read Write sets differ'); } - LOG.exit(method); + LOG.exit(method, errorsIgnored); + return errorsIgnored; } /** diff --git a/packages/composer-connector-hlfv1/test/hlfconnection.js b/packages/composer-connector-hlfv1/test/hlfconnection.js index e67c1c39eb..d2f54eb6ba 100644 --- a/packages/composer-connector-hlfv1/test/hlfconnection.js +++ b/packages/composer-connector-hlfv1/test/hlfconnection.js @@ -948,7 +948,8 @@ describe('HLFConnection', () => { mockChannel.compareProposalResponseResults.returns(true); (function() { - connection._validateResponses(responses, true); + const errorsIgnored = connection._validateResponses(responses, true); + errorsIgnored.should.be.false; }).should.not.throw(); }); @@ -962,7 +963,8 @@ describe('HLFConnection', () => { mockChannel.compareProposalResponseResults.returns(true); (function() { - connection._validateResponses(responses, false, /chaincode exists/); + const errorsIgnored = connection._validateResponses(responses, false, /chaincode exists/); + errorsIgnored.should.be.true; }).should.not.throw(); }); @@ -1044,12 +1046,6 @@ describe('HLFConnection', () => { mockChannel.compareProposalResponseResults.returns(true); connection._validateResponses(responses, true); sinon.assert.calledWith(logWarnSpy, 'Response from peer was not valid'); - - - //(function() { - // connection._validateResponses(responses, true); - //}).should.throw(/Response from peer was not valid/); - }); it('should throw if compareProposals returns false', () => { @@ -1066,12 +1062,6 @@ describe('HLFConnection', () => { mockChannel.compareProposalResponseResults.returns(false); connection._validateResponses(responses, true); sinon.assert.calledWith(logWarnSpy, 'Peers do not agree, Read Write sets differ'); - - - //(function() { - // connection._validateResponses(responses, true); - //}).should.throw(/Peers do not agree/); - }); it('should not try to check proposal responses if not a response from a proposal', () => { @@ -1107,8 +1097,8 @@ describe('HLFConnection', () => { sandbox.stub(connection.fs, 'outputFile').resolves(); sandbox.stub(process, 'on').withArgs('exit').yields(); sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); - sandbox.stub(connection, '_validateResponses').returns(); sandbox.stub(connection, '_initializeChannel').resolves(); + sandbox.stub(connection, '_validateResponses').returns(false); connection._connectToEventHubs(); }); @@ -1428,7 +1418,6 @@ describe('HLFConnection', () => { .should.be.rejectedWith(/Error trying parse endorsement policy/); }); - it('should deploy the business network with no debug level specified', () => { sandbox.stub(global, 'setTimeout'); // This is the deployment proposal and response (from the peers). @@ -1505,6 +1494,7 @@ describe('HLFConnection', () => { mockChannel.sendTransaction.withArgs({ proposalResponses: instantiateResponses, proposal: proposal, header: header }).resolves(response); // This is the event hub response. mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID.toString(), 'VALID'); + connection._validateResponses.withArgs(installResponses).returns(true); return connection.deploy(mockSecurityContext, 'org-acme-biznet', '{"start":"json"}') .then(() => { sinon.assert.calledOnce(connection.fs.copy); @@ -1572,7 +1562,6 @@ describe('HLFConnection', () => { // This is the event hub response. mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'VALID'); - return connection.deploy(mockSecurityContext, 'org-acme-biznet', '{"start":"json"}') .then(() => { sinon.assert.calledOnce(connection.fs.copy); @@ -1598,6 +1587,28 @@ describe('HLFConnection', () => { }); }); + it('should throw an error is already installed and instantiated', () => { + sandbox.stub(global, 'setTimeout'); + const errorResp = new Error('Error installing chaincode code systest-participants:0.5.11(chaincode /var/hyperledger/production/chaincodes/systest-participants.0.5.11 exists)'); + const installResponses = [errorResp]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockClient.installChaincode.resolves([ installResponses, proposal, header ]); + // query instantiate response shows chaincode already instantiated + const queryInstantiatedResponse = { + chaincodes: [ + { + path: 'composer', + name: 'org-acme-biznet' + } + ] + }; + mockChannel.queryInstantiatedChaincodes.resolves(queryInstantiatedResponse); + connection._validateResponses.withArgs(installResponses).returns(true); + return connection.deploy(mockSecurityContext, 'org-acme-biznet', '{"start":"json"}') + .should.be.rejectedWith(/already been deployed/); + }); + it('should throw if install fails to validate', () => { sandbox.stub(global, 'setTimeout'); // This is the deployment proposal and response (from the peers). @@ -2486,18 +2497,15 @@ describe('HLFConnection', () => { }); describe('#createTransactionID', ()=>{ - - beforeEach(() => { - mockChannel.initialize.resolves(); - }); - it('should create a transaction id', () => { - connection.initialized = true; - - connection.createTransactionId().then((result) =>{ - sinon.assert.calledOnce(mockClient.getTransactionID); - result.should.deep.equal('00000000-0000-0000-0000-000000000000'); - }); + return connection.createTransactionId() + .then((result) =>{ + sinon.assert.calledOnce(mockClient.newTransactionID); + result.should.deep.equal({ + id: mockTransactionID, + idStr: '00000000-0000-0000-0000-000000000000' + }); + }); }); }); From bb65aeedc1e49ef4eee78f7139aea8946e0e0a5b Mon Sep 17 00:00:00 2001 From: caroline-church Date: Thu, 26 Oct 2017 15:06:33 +0100 Subject: [PATCH 9/9] Update Historian In Playground (#2465) Swapped columns around Updated data shown by columns Updated column widths contributes to hyperledger/composer#1895 Signed-off-by: Caroline Church --- .../app/test/registry/registry.component.html | 22 +++++++++---------- .../app/test/registry/registry.component.scss | 12 +++++++++- .../test/registry/registry.component.spec.ts | 10 +++++++++ .../src/app/test/test.component.scss | 2 +- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/composer-playground/src/app/test/registry/registry.component.html b/packages/composer-playground/src/app/test/registry/registry.component.html index b80654b99e..be2e543a3c 100644 --- a/packages/composer-playground/src/app/test/registry/registry.component.html +++ b/packages/composer-playground/src/app/test/registry/registry.component.html @@ -1,6 +1,6 @@
-

{{_registry.name}}

+

{{_registry.name}}