From 0ae679e8ad11cf0ea272ac3bd05842d0ed072aba Mon Sep 17 00:00:00 2001 From: Ehsan Rezaei Date: Thu, 28 Nov 2024 13:19:49 +0100 Subject: [PATCH 1/2] Revert "Revert AAE-20794 - new websocket refactoring (#10436)" This reverts commit b09f4cacf949a3646ebb7bd812fadd1e0675d7f5. --- .../.storybook/tsconfig.json | 2 +- .../process-filter-cloud.service.spec.ts | 11 +- .../notification-cloud.service.spec.ts | 51 ++--- .../services/notification-cloud.service.ts | 101 +-------- .../src/lib/services/public-api.ts | 1 + .../lib/services/web-socket.service.spec.ts | 135 +++++++++++ .../src/lib/services/web-socket.service.ts | 211 ++++++++++++++++++ ...ervice-task-filter-cloud.component.spec.ts | 14 +- .../edit-task-filter-cloud.component.spec.ts | 14 +- .../task-filters-cloud.component.spec.ts | 15 +- .../task-filter-cloud.service.spec.ts | 11 +- package-lock.json | 20 +- package.json | 3 +- 13 files changed, 441 insertions(+), 148 deletions(-) create mode 100644 lib/process-services-cloud/src/lib/services/web-socket.service.spec.ts create mode 100644 lib/process-services-cloud/src/lib/services/web-socket.service.ts diff --git a/lib/process-services-cloud/.storybook/tsconfig.json b/lib/process-services-cloud/.storybook/tsconfig.json index 83c6a0ca455..30f9f5e443a 100644 --- a/lib/process-services-cloud/.storybook/tsconfig.json +++ b/lib/process-services-cloud/.storybook/tsconfig.json @@ -6,5 +6,5 @@ }, "exclude": ["../**/*.spec.ts" ], - "include": ["../src/**/*", "*.js"] + "include": ["../src/**/*", "*.js", "../../core/feature-flags"] } diff --git a/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.spec.ts index df1abc04513..2d76b472ddb 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.spec.ts @@ -32,6 +32,7 @@ import { import { ProcessFilterCloudModel } from '../models/process-filter-cloud.model'; import { IdentityUserService } from '../../../people/services/identity-user.service'; import { NotificationCloudService } from '../../../services/notification-cloud.service'; +import { provideMockFeatureFlags } from '@alfresco/adf-core/feature-flags'; describe('ProcessFilterCloudService', () => { let service: ProcessFilterCloudService; @@ -52,7 +53,10 @@ describe('ProcessFilterCloudService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ProcessServiceCloudTestingModule], - providers: [{ provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] + providers: [ + { provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, + provideMockFeatureFlags({ ['studio-ws-graphql-subprotocol']: false }) + ] }); service = TestBed.inject(ProcessFilterCloudService); @@ -68,7 +72,7 @@ describe('ProcessFilterCloudService', () => { }); it('should create processfilter key by using appName and the username', (done) => { - service.getProcessFilters('mock-appName').subscribe((res: any) => { + service.getProcessFilters('mock-appName').subscribe((res: ProcessFilterCloudModel[]) => { expect(res).toBeDefined(); expect(getCurrentUserInfoSpy).toHaveBeenCalled(); done(); @@ -137,7 +141,7 @@ describe('ProcessFilterCloudService', () => { it('should create the process filters in case the filters are not exist in the user preferences', (done) => { getPreferencesSpy.and.returnValue(of(fakeProcessCloudFilterWithDifferentEntries)); - service.getProcessFilters('mock-appName').subscribe((res: any) => { + service.getProcessFilters('mock-appName').subscribe((res: ProcessFilterCloudModel[]) => { expect(res).toBeDefined(); expect(res).not.toBeNull(); expect(res.length).toBe(3); @@ -243,6 +247,7 @@ describe('ProcessFilterCloudService', () => { it('should reset filters to default values', async () => { const changedFilter = new ProcessFilterCloudModel(fakeProcessCloudFilters[0]); changedFilter.processDefinitionKey = 'modifiedProcessDefinitionKey'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any spyOn(service, 'defaultProcessFilters').and.returnValue(fakeProcessCloudFilters); await service.resetProcessFilterToDefaults('mock-appName', changedFilter).toPromise(); diff --git a/lib/process-services-cloud/src/lib/services/notification-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/services/notification-cloud.service.spec.ts index 9374153402f..3139b393730 100644 --- a/lib/process-services-cloud/src/lib/services/notification-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/services/notification-cloud.service.spec.ts @@ -18,17 +18,12 @@ import { TestBed } from '@angular/core/testing'; import { ProcessServiceCloudTestingModule } from '../testing/process-service-cloud.testing.module'; import { NotificationCloudService } from './notification-cloud.service'; -import { Apollo } from 'apollo-angular'; +import { WebSocketService } from './web-socket.service'; +import { provideMockFeatureFlags } from '@alfresco/adf-core/feature-flags'; describe('NotificationCloudService', () => { let service: NotificationCloudService; - let apollo: Apollo; - let apolloCreateSpy: jasmine.Spy; - let apolloSubscribeSpy: jasmine.Spy; - - const useMock: any = { - subscribe: () => {} - }; + let wsService: WebSocketService; const queryMock = ` subscription { @@ -43,39 +38,25 @@ describe('NotificationCloudService', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ProcessServiceCloudTestingModule] + imports: [ProcessServiceCloudTestingModule], + providers: [WebSocketService, provideMockFeatureFlags({ ['studio-ws-graphql-subprotocol']: false })] }); service = TestBed.inject(NotificationCloudService); - apollo = TestBed.inject(Apollo); - - service.appsListening = []; - apolloCreateSpy = spyOn(apollo, 'createNamed'); - apolloSubscribeSpy = spyOn(apollo, 'use').and.returnValue(useMock); + wsService = TestBed.inject(WebSocketService); }); - it('should not create more than one websocket per app if it was already created', () => { - service.makeGQLQuery('myAppName', queryMock); - expect(service.appsListening.length).toBe(1); - expect(service.appsListening[0]).toBe('myAppName'); - - service.makeGQLQuery('myAppName', queryMock); - expect(service.appsListening.length).toBe(1); - expect(service.appsListening[0]).toBe('myAppName'); - - expect(apolloCreateSpy).toHaveBeenCalledTimes(1); - expect(apolloSubscribeSpy).toHaveBeenCalledTimes(2); - }); + it('should call getSubscription with the correct parameters', () => { + const getSubscriptionSpy = spyOn(wsService, 'getSubscription').and.callThrough(); - it('should create new websocket if it is subscribing to new app', () => { service.makeGQLQuery('myAppName', queryMock); - expect(service.appsListening.length).toBe(1); - expect(service.appsListening[0]).toBe('myAppName'); - - service.makeGQLQuery('myOtherAppName', queryMock); - expect(service.appsListening.length).toBe(2); - expect(service.appsListening[1]).toBe('myOtherAppName'); - expect(apolloCreateSpy).toHaveBeenCalledTimes(2); - expect(apolloSubscribeSpy).toHaveBeenCalledTimes(2); + expect(getSubscriptionSpy).toHaveBeenCalledWith({ + apolloClientName: 'myAppName', + wsUrl: 'myAppName/notifications', + httpUrl: 'myAppName/notifications/graphql', + subscriptionOptions: { + query: jasmine.any(Object) + } + }); }); }); diff --git a/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts b/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts index afb1cd3bbef..1646b3c97d9 100644 --- a/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts @@ -15,101 +15,24 @@ * limitations under the License. */ -import { Apollo } from 'apollo-angular'; -import { HttpLink } from 'apollo-angular/http'; -import { split, gql, InMemoryCache, ApolloLink, InMemoryCacheConfig } from '@apollo/client/core'; -import { WebSocketLink } from '@apollo/client/link/ws'; -import { onError } from '@apollo/client/link/error'; -import { getMainDefinition } from '@apollo/client/utilities'; +import { gql } from '@apollo/client/core'; import { Injectable } from '@angular/core'; -import { AuthenticationService } from '@alfresco/adf-core'; -import { BaseCloudService } from './base-cloud.service'; -import { AdfHttpClient } from '@alfresco/adf-core/api'; +import { WebSocketService } from './web-socket.service'; @Injectable({ providedIn: 'root' }) -export class NotificationCloudService extends BaseCloudService { - appsListening = []; - - constructor(public apollo: Apollo, private http: HttpLink, private authService: AuthenticationService, protected adfHttpClient: AdfHttpClient) { - super(adfHttpClient); - } - - private get webSocketHost() { - return this.contextRoot.split('://')[1]; - } - - private get protocol() { - return this.contextRoot.split('://')[0] === 'https' ? 'wss' : 'ws'; - } - - initNotificationsForApp(appName: string) { - if (!this.appsListening.includes(appName)) { - this.appsListening.push(appName); - const httpLink = this.http.create({ - uri: `${this.getBasePath(appName)}/notifications/graphql` - }); - - const webSocketLink = new WebSocketLink({ - uri: `${this.protocol}://${this.webSocketHost}/${appName}/notifications/ws/graphql`, - options: { - reconnect: true, - lazy: true, - connectionParams: { - kaInterval: 2000, - // eslint-disable-next-line @typescript-eslint/naming-convention - 'X-Authorization': 'Bearer ' + this.authService.getToken() - } - } - }); - - const link = split( - ({ query }) => { - const definition = getMainDefinition(query); - return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'; - }, - webSocketLink, - httpLink - ); - - const errorLink = onError(({ graphQLErrors, operation, forward }) => { - if (graphQLErrors) { - for (const err of graphQLErrors) { - switch (err.extensions.code) { - case 'UNAUTHENTICATED': { - const oldHeaders = operation.getContext().headers; - operation.setContext({ - headers: { - ...oldHeaders, - // eslint-disable-next-line @typescript-eslint/naming-convention - 'X-Authorization': 'Bearer ' + this.authService.getToken() - } - }); - forward(operation); - break; - } - default: - break; - } - } - } - }); - - this.apollo.createNamed(appName, { - link: ApolloLink.from([errorLink, link]), - cache: new InMemoryCache({ merge: true } as InMemoryCacheConfig), - defaultOptions: { - watchQuery: { - errorPolicy: 'all' - } - } - }); - } - } +export class NotificationCloudService { + constructor(private readonly webSocketService: WebSocketService) {} makeGQLQuery(appName: string, gqlQuery: string) { - this.initNotificationsForApp(appName); - return this.apollo.use(appName).subscribe({ query: gql(gqlQuery) }); + return this.webSocketService.getSubscription({ + apolloClientName: appName, + wsUrl: `${appName}/notifications`, + httpUrl: `${appName}/notifications/graphql`, + subscriptionOptions: { + query: gql(gqlQuery) + } + }); } } diff --git a/lib/process-services-cloud/src/lib/services/public-api.ts b/lib/process-services-cloud/src/lib/services/public-api.ts index 5da40b1da78..40684e03a66 100644 --- a/lib/process-services-cloud/src/lib/services/public-api.ts +++ b/lib/process-services-cloud/src/lib/services/public-api.ts @@ -24,3 +24,4 @@ export * from './form-fields.interfaces'; export * from './base-cloud.service'; export * from './task-list-cloud.service.interface'; export * from './variable-mapper.sevice'; +export * from './web-socket.service'; diff --git a/lib/process-services-cloud/src/lib/services/web-socket.service.spec.ts b/lib/process-services-cloud/src/lib/services/web-socket.service.spec.ts new file mode 100644 index 00000000000..4ca6d691e32 --- /dev/null +++ b/lib/process-services-cloud/src/lib/services/web-socket.service.spec.ts @@ -0,0 +1,135 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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. + */ + +import { AppConfigService, AuthenticationService } from '@alfresco/adf-core'; +import { TestBed } from '@angular/core/testing'; +import { Apollo, gql } from 'apollo-angular'; +import { of, Subject } from 'rxjs'; +import { WebSocketService } from './web-socket.service'; +import { SubscriptionOptions } from '@apollo/client/core'; +import { FeaturesServiceToken, IFeaturesService, provideMockFeatureFlags } from '@alfresco/adf-core/feature-flags'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('WebSocketService', () => { + let service: WebSocketService; + let featureService: IFeaturesService; + const onLogoutSubject: Subject = new Subject(); + + const apolloMock = jasmine.createSpyObj('Apollo', ['use', 'createNamed']); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: Apollo, + useValue: apolloMock + }, + { + provide: AppConfigService, + useValue: { + get: () => 'wss://testHost' + } + }, + { + provide: AuthenticationService, + useValue: { + getToken: () => 'testToken', + onLogout: onLogoutSubject.asObservable() + } + }, + provideMockFeatureFlags({ ['studio-ws-graphql-subprotocol']: true }) + ] + }); + service = TestBed.inject(WebSocketService); + featureService = TestBed.inject(FeaturesServiceToken); + spyOn(featureService, 'isOn$').and.returnValue(of(true)); + apolloMock.use.and.returnValues(undefined, { subscribe: () => of({}) }); + }); + + afterEach(() => { + apolloMock.use.calls.reset(); + apolloMock.createNamed.calls.reset(); + }); + + it('should not create a new Apollo client if it is already in use', (done) => { + const apolloClientName = 'testClient'; + const subscriptionOptions: SubscriptionOptions = { query: gql(`subscription {testQuery}`) }; + const wsOptions = { apolloClientName, wsUrl: 'testUrl', subscriptionOptions }; + + apolloMock.use.and.returnValues(true, { subscribe: () => of({}) }); + + service.getSubscription(wsOptions).subscribe(() => { + expect(apolloMock.use).toHaveBeenCalledTimes(2); + expect(apolloMock.use).toHaveBeenCalledWith(apolloClientName); + expect(apolloMock.createNamed).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should subscribe to Apollo client if not already in use', (done) => { + const apolloClientName = 'testClient'; + const expectedApolloClientName = 'testClient'; + const subscriptionOptions: SubscriptionOptions = { query: gql(`subscription {testQuery}`) }; + const wsOptions = { apolloClientName, wsUrl: 'testUrl', subscriptionOptions }; + + service.getSubscription(wsOptions).subscribe(() => { + expect(apolloMock.use).toHaveBeenCalledWith(expectedApolloClientName); + expect(apolloMock.use).toHaveBeenCalledTimes(2); + expect(apolloMock.createNamed).toHaveBeenCalledTimes(1); + expect(apolloMock.createNamed).toHaveBeenCalledWith(expectedApolloClientName, jasmine.any(Object)); + done(); + }); + }); + + it('should create named client with the right authentication token when FF is on', (done) => { + let headers = {}; + const expectedHeaders = { Authorization: 'Bearer testToken' }; + const apolloClientName = 'testClient'; + const subscriptionOptions: SubscriptionOptions = { query: gql(`subscription {testQuery}`) }; + const wsOptions = { apolloClientName, wsUrl: 'testUrl', subscriptionOptions }; + apolloMock.createNamed.and.callFake((_, options) => { + headers = options.headers; + }); + + service.getSubscription(wsOptions).subscribe(() => { + expect(apolloMock.use).toHaveBeenCalledTimes(2); + expect(apolloMock.createNamed).toHaveBeenCalled(); + expect(headers).toEqual(expectedHeaders); + done(); + }); + }); + + it('should create named client with the right authentication token when FF is off', (done) => { + featureService.isOn$ = jasmine.createSpy().and.returnValue(of(false)); + let headers = {}; + const expectedHeaders = { 'X-Authorization': 'Bearer testToken' }; + const apolloClientName = 'testClient'; + const subscriptionOptions: SubscriptionOptions = { query: gql(`subscription {testQuery}`) }; + const wsOptions = { apolloClientName, wsUrl: 'testUrl', subscriptionOptions }; + apolloMock.createNamed.and.callFake((_, options) => { + headers = options.headers; + }); + + service.getSubscription(wsOptions).subscribe(() => { + expect(apolloMock.use).toHaveBeenCalledTimes(2); + expect(apolloMock.createNamed).toHaveBeenCalled(); + expect(headers).toEqual(expectedHeaders); + done(); + }); + }); +}); diff --git a/lib/process-services-cloud/src/lib/services/web-socket.service.ts b/lib/process-services-cloud/src/lib/services/web-socket.service.ts new file mode 100644 index 00000000000..0b10f549255 --- /dev/null +++ b/lib/process-services-cloud/src/lib/services/web-socket.service.ts @@ -0,0 +1,211 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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. + */ + +import { createClient } from 'graphql-ws'; +import { Inject, Injectable } from '@angular/core'; +import { AppConfigService, AuthenticationService } from '@alfresco/adf-core'; +import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; +import { WebSocketLink } from '@apollo/client/link/ws'; +import { + DefaultContext, + FetchResult, + from, + HttpLink, + InMemoryCache, + InMemoryCacheConfig, + NextLink, + Operation, + split, + SubscriptionOptions +} from '@apollo/client/core'; +import { getMainDefinition } from '@apollo/client/utilities'; +import { Kind, OperationTypeNode } from 'graphql'; +import { Apollo } from 'apollo-angular'; +import { onError } from '@apollo/client/link/error'; +import { RetryLink } from '@apollo/client/link/retry'; +import { Observable } from 'rxjs'; +import { switchMap, take, tap } from 'rxjs/operators'; +import { FeaturesServiceToken, IFeaturesService } from '@alfresco/adf-core/feature-flags'; + +interface serviceOptions { + apolloClientName: string; + wsUrl: string; + httpUrl?: string; + subscriptionOptions: SubscriptionOptions; +} + +@Injectable({ + providedIn: 'root' +}) +export class WebSocketService { + private host = ''; + private subscriptionProtocol: 'graphql-ws' | 'transport-ws' = 'transport-ws'; + private wsLink: GraphQLWsLink | WebSocketLink; + private httpLink: HttpLink; + + constructor( + private readonly apollo: Apollo, + private readonly appConfigService: AppConfigService, + private readonly authService: AuthenticationService, + @Inject(FeaturesServiceToken) private featuresService: IFeaturesService + ) { + this.host = this.appConfigService.get('bpmHost', ''); + } + + public getSubscription(options: serviceOptions): Observable> { + const { apolloClientName, subscriptionOptions } = options; + this.authService.onLogout.pipe(take(1)).subscribe(() => { + if (this.apollo.use(apolloClientName)) { + this.apollo.removeClient(apolloClientName); + } + }); + + return this.featuresService.isOn$('studio-ws-graphql-subprotocol').pipe( + tap((isOn) => { + if (isOn) { + this.subscriptionProtocol = 'graphql-ws'; + } + }), + switchMap(() => { + if (this.apollo.use(apolloClientName) === undefined) { + this.initSubscriptions(options); + } + return this.apollo.use(apolloClientName).subscribe({ errorPolicy: 'all', ...subscriptionOptions }); + }) + ); + } + + private createWsUrl(serviceUrl: string): string { + const url = new URL(serviceUrl, this.host); + const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; + url.protocol = protocol; + + return url.href; + } + + private createHttpUrl(serviceUrl: string): string { + const url = new URL(serviceUrl, this.host); + + return url.href; + } + + private initSubscriptions(options: serviceOptions): void { + switch (this.subscriptionProtocol) { + case 'graphql-ws': + this.createGraphQLWsLink(options); + break; + case 'transport-ws': + this.createTransportWsLink(options); + break; + default: + throw new Error('Unknown subscription protocol'); + } + + this.httpLink = options.httpUrl + ? new HttpLink({ + uri: this.createHttpUrl(options.httpUrl) + }) + : undefined; + + const link = split( + ({ query }) => { + const definition = getMainDefinition(query); + return definition.kind === Kind.OPERATION_DEFINITION && definition.operation === OperationTypeNode.SUBSCRIPTION; + }, + this.wsLink, + this.httpLink + ); + + const authLink = (operation: Operation, forward: NextLink) => { + operation.setContext(({ headers }: DefaultContext) => ({ + headers: { + ...headers, + ...(this.subscriptionProtocol === 'graphql-ws' && { Authorization: `Bearer ${this.authService.getToken()}` }), + ...(this.subscriptionProtocol === 'transport-ws' && { 'X-Authorization': `Bearer ${this.authService.getToken()}` }) + } + })); + return forward(operation); + }; + + const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => { + if (graphQLErrors) { + for (const error of graphQLErrors) { + if (error.extensions && error.extensions['code'] === 'UNAUTHENTICATED') { + authLink(operation, forward); + } + } + } + + if (networkError) { + console.error(`[Network error]: ${networkError}`); + } + }); + + const retryLink = new RetryLink({ + delay: { + initial: 300, + max: Number.POSITIVE_INFINITY, + jitter: true + }, + attempts: { + max: 5, + retryIf: (error) => !!error + } + }); + + this.apollo.createNamed(options.apolloClientName, { + headers: { + ...(this.subscriptionProtocol === 'graphql-ws' && { Authorization: `Bearer ${this.authService.getToken()}` }), + ...(this.subscriptionProtocol === 'transport-ws' && { 'X-Authorization': `Bearer ${this.authService.getToken()}` }) + }, + link: from([authLink, retryLink, errorLink, link]), + cache: new InMemoryCache({ merge: true } as InMemoryCacheConfig) + }); + } + + private createTransportWsLink(options: serviceOptions) { + this.wsLink = new WebSocketLink({ + uri: this.createWsUrl(options.wsUrl) + '/ws/graphql', + options: { + reconnect: true, + lazy: true, + connectionParams: { + kaInterval: 2000, + 'X-Authorization': 'Bearer ' + this.authService.getToken() + } + } + }); + } + + private createGraphQLWsLink(options: serviceOptions) { + this.wsLink = new GraphQLWsLink( + createClient({ + url: this.createWsUrl(options.wsUrl) + '/v2/ws/graphql', + connectionParams: { + Authorization: 'Bearer ' + this.authService.getToken() + }, + on: { + error: () => { + this.apollo.removeClient(options.apolloClientName); + this.initSubscriptions(options); + } + }, + lazy: true + }) + ); + } +} diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter-cloud.component.spec.ts index 9905455fc93..b97686b4ee6 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter-cloud.component.spec.ts @@ -39,6 +39,7 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatExpansionPanelHarness } from '@angular/material/expansion/testing'; import { MatSelectHarness } from '@angular/material/select/testing'; import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing'; +import { provideMockFeatureFlags } from '@alfresco/adf-core/feature-flags'; describe('EditServiceTaskFilterCloudComponent', () => { let loader: HarnessLoader; @@ -50,12 +51,16 @@ describe('EditServiceTaskFilterCloudComponent', () => { let getTaskFilterSpy: jasmine.Spy; let getDeployedApplicationsSpy: jasmine.Spy; let taskService: TaskCloudService; - const afterClosedSubject = new Subject(); + const afterClosedSubject = new Subject(); beforeEach(() => { TestBed.configureTestingModule({ imports: [ProcessServiceCloudTestingModule, TaskFiltersCloudModule, MatIconTestingModule], - providers: [MatDialog, { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] + providers: [ + MatDialog, + { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, + provideMockFeatureFlags({ ['studio-ws-graphql-subprotocol']: false }) + ] }); fixture = TestBed.createComponent(EditServiceTaskFilterCloudComponent); component = fixture.componentInstance; @@ -63,9 +68,8 @@ describe('EditServiceTaskFilterCloudComponent', () => { appsService = TestBed.inject(AppsProcessCloudService); taskService = TestBed.inject(TaskCloudService); dialog = TestBed.inject(MatDialog); - const dialogRefMock: any = { - afterClosed: () => afterClosedSubject - }; + const dialogRefMock = jasmine.createSpyObj('MatDialogRef', ['afterClosed']); + dialogRefMock.afterClosed.and.returnValue(afterClosedSubject); spyOn(dialog, 'open').and.returnValue(dialogRefMock); getTaskFilterSpy = spyOn(service, 'getTaskFilterById').and.returnValue(of(fakeServiceFilter)); getDeployedApplicationsSpy = spyOn(appsService, 'getDeployedApplicationsByStatus').and.returnValue(of(fakeApplicationInstance)); diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter-cloud.component.spec.ts index 8e0481f505e..a5093faa82d 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter-cloud.component.spec.ts @@ -57,6 +57,7 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatSelectHarness } from '@angular/material/select/testing'; import { MatExpansionPanelHarness } from '@angular/material/expansion/testing'; import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing'; +import { provideMockFeatureFlags } from '@alfresco/adf-core/feature-flags'; describe('EditTaskFilterCloudComponent', () => { let loader: HarnessLoader; @@ -70,12 +71,16 @@ describe('EditTaskFilterCloudComponent', () => { let getTaskFilterSpy: jasmine.Spy; let getDeployedApplicationsSpy: jasmine.Spy; let taskService: TaskCloudService; - const afterClosedSubject = new Subject(); + const afterClosedSubject = new Subject(); beforeEach(() => { TestBed.configureTestingModule({ imports: [ProcessServiceCloudTestingModule, TaskFiltersCloudModule, PeopleCloudModule, MatIconTestingModule], - providers: [MatDialog, { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] + providers: [ + MatDialog, + { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, + provideMockFeatureFlags({ ['studio-ws-graphql-subprotocol']: false }) + ] }); fixture = TestBed.createComponent(EditTaskFilterCloudComponent); component = fixture.componentInstance; @@ -85,9 +90,8 @@ describe('EditTaskFilterCloudComponent', () => { taskService = TestBed.inject(TaskCloudService); alfrescoApiService = TestBed.inject(AlfrescoApiService); dialog = TestBed.inject(MatDialog); - const dialogRefMock: any = { - afterClosed: () => afterClosedSubject - }; + const dialogRefMock = jasmine.createSpyObj('MatDialogRef', ['afterClosed']); + dialogRefMock.afterClosed.and.returnValue(afterClosedSubject); spyOn(dialog, 'open').and.returnValue(dialogRefMock); spyOn(alfrescoApiService, 'getInstance').and.returnValue(mockAlfrescoApi); getTaskFilterSpy = spyOn(service, 'getTaskFilterById').and.returnValue(of(fakeFilter)); diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.spec.ts index 9a158551f7e..37973cdaf79 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters-cloud.component.spec.ts @@ -16,7 +16,7 @@ */ import { AppConfigService } from '@alfresco/adf-core'; -import { SimpleChange } from '@angular/core'; +import { DebugElement, SimpleChange } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { first, of, throwError } from 'rxjs'; @@ -32,6 +32,7 @@ import { HarnessLoader } from '@angular/cdk/testing'; import { MatActionListItemHarness } from '@angular/material/list/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TaskFilterCloudAdapter } from '../../../models/filter-cloud-model'; +import { provideMockFeatureFlags } from '@alfresco/adf-core/feature-flags'; describe('TaskFiltersCloudComponent', () => { let loader: HarnessLoader; @@ -45,10 +46,14 @@ describe('TaskFiltersCloudComponent', () => { let getTaskListFiltersSpy: jasmine.Spy; let getTaskListCounterSpy: jasmine.Spy; - const configureTestingModule = (providers: any[]) => { + const configureTestingModule = (providers: unknown[]) => { TestBed.configureTestingModule({ imports: [ProcessServiceCloudTestingModule, TaskFiltersCloudModule], - providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, ...providers] + providers: [ + { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, + provideMockFeatureFlags({ ['studio-ws-graphql-subprotocol']: false }), + ...providers + ] }); taskFilterService = TestBed.inject(TaskFilterCloudService); taskListService = TestBed.inject(TaskListCloudService); @@ -102,7 +107,7 @@ describe('TaskFiltersCloudComponent', () => { fixture.detectChanges(); await fixture.whenStable(); - const filters: any = fixture.debugElement.queryAll(By.css('.adf-icon')); + const filters: DebugElement[] = fixture.debugElement.queryAll(By.css('.adf-icon')); expect(filters.length).toBe(0); }); @@ -265,7 +270,7 @@ describe('TaskFiltersCloudComponent', () => { component.showIcons = false; fixture.detectChanges(); - const filters: any = fixture.debugElement.queryAll(By.css('.adf-icon')); + const filters: DebugElement[] = fixture.debugElement.queryAll(By.css('.adf-icon')); expect(filters.length).toBe(0); }); diff --git a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts index 09e48f0e087..e69dc1764d3 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/services/task-filter-cloud.service.spec.ts @@ -37,6 +37,7 @@ import { IdentityUserService } from '../../../people/services/identity-user.serv import { ApolloModule } from 'apollo-angular'; import { StorageService } from '@alfresco/adf-core'; import { TaskStatusFilter } from '../public-api'; +import { provideMockFeatureFlags } from '@alfresco/adf-core/feature-flags'; describe('TaskFilterCloudService', () => { let service: TaskFilterCloudService; @@ -57,7 +58,10 @@ describe('TaskFilterCloudService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule, ProcessServiceCloudTestingModule, ApolloModule], - providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: UserPreferenceCloudService }] + providers: [ + { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: UserPreferenceCloudService }, + provideMockFeatureFlags({ ['studio-ws-graphql-subprotocol']: false }) + ] }); service = TestBed.inject(TaskFilterCloudService); notificationCloudService = TestBed.inject(NotificationCloudService); @@ -266,7 +270,10 @@ describe('Inject [LocalPreferenceCloudService] into the TaskFilterCloudService', beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule, ProcessServiceCloudTestingModule, ApolloModule], - providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] + providers: [ + { provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, + provideMockFeatureFlags({ ['studio-ws-graphql-subprotocol']: false }) + ] }); service = TestBed.inject(TaskFilterCloudService); preferenceCloudService = service.preferenceService; diff --git a/package-lock.json b/package-lock.json index 8a7a941e796..55bb4554632 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "date-fns": "^2.30.0", "dotenv-expand": "^5.1.0", "event-emitter": "^0.3.5", + "graphql-ws": "^5.16.0", "material-icons": "^1.13.12", "minimatch-browser": "1.0.0", "ng2-charts": "^4.1.1", @@ -115,7 +116,7 @@ "eslint-plugin-rxjs": "^5.0.3", "eslint-plugin-storybook": "^0.11.1", "eslint-plugin-unicorn": "^49.0.0", - "graphql": "^16.8.1", + "graphql": "^16.9.0", "husky": "^7.0.4", "jasmine-ajax": "4.0.0", "jasmine-core": "5.4.0", @@ -19156,7 +19157,8 @@ }, "node_modules/graphql": { "version": "16.9.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -19174,6 +19176,20 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/graphql-ws": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.0.tgz", + "integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==", + "workspaces": [ + "website" + ], + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, "node_modules/guess-parser": { "version": "0.4.22", "dev": true, diff --git a/package.json b/package.json index c69ccf8a9c4..173d9586182 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "date-fns": "^2.30.0", "dotenv-expand": "^5.1.0", "event-emitter": "^0.3.5", + "graphql-ws": "^5.16.0", "material-icons": "^1.13.12", "minimatch-browser": "1.0.0", "ng2-charts": "^4.1.1", @@ -135,7 +136,7 @@ "eslint-plugin-rxjs": "^5.0.3", "eslint-plugin-storybook": "^0.11.1", "eslint-plugin-unicorn": "^49.0.0", - "graphql": "^16.8.1", + "graphql": "^16.9.0", "husky": "^7.0.4", "jasmine-ajax": "4.0.0", "jasmine-core": "5.4.0", From a5bfb5663be20a9511a12d2b79d9b11b76893ac8 Mon Sep 17 00:00:00 2001 From: Ehsan Rezaei Date: Mon, 2 Dec 2024 17:46:04 +0100 Subject: [PATCH 2/2] AAE-20808 Moved websocket service to core --- lib/core/src/lib/services/index.ts | 18 ++++++++++++++++++ lib/core/src/lib/services/public-api.ts | 18 ++++++++++++++++++ .../lib/services/web-socket.service.spec.ts | 0 .../src/lib/services/web-socket.service.ts | 3 ++- lib/core/src/public-api.ts | 1 + .../lib/services/notification-cloud.service.ts | 3 +-- 6 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 lib/core/src/lib/services/index.ts create mode 100644 lib/core/src/lib/services/public-api.ts rename lib/{process-services-cloud => core}/src/lib/services/web-socket.service.spec.ts (100%) rename lib/{process-services-cloud => core}/src/lib/services/web-socket.service.ts (98%) diff --git a/lib/core/src/lib/services/index.ts b/lib/core/src/lib/services/index.ts new file mode 100644 index 00000000000..54beb1a252f --- /dev/null +++ b/lib/core/src/lib/services/index.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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. + */ + +export * from './public-api'; diff --git a/lib/core/src/lib/services/public-api.ts b/lib/core/src/lib/services/public-api.ts new file mode 100644 index 00000000000..0db4fa65e8a --- /dev/null +++ b/lib/core/src/lib/services/public-api.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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. + */ + +export * from './web-socket.service'; diff --git a/lib/process-services-cloud/src/lib/services/web-socket.service.spec.ts b/lib/core/src/lib/services/web-socket.service.spec.ts similarity index 100% rename from lib/process-services-cloud/src/lib/services/web-socket.service.spec.ts rename to lib/core/src/lib/services/web-socket.service.spec.ts diff --git a/lib/process-services-cloud/src/lib/services/web-socket.service.ts b/lib/core/src/lib/services/web-socket.service.ts similarity index 98% rename from lib/process-services-cloud/src/lib/services/web-socket.service.ts rename to lib/core/src/lib/services/web-socket.service.ts index 0b10f549255..bb5f3aecf5b 100644 --- a/lib/process-services-cloud/src/lib/services/web-socket.service.ts +++ b/lib/core/src/lib/services/web-socket.service.ts @@ -17,7 +17,8 @@ import { createClient } from 'graphql-ws'; import { Inject, Injectable } from '@angular/core'; -import { AppConfigService, AuthenticationService } from '@alfresco/adf-core'; +import { AuthenticationService } from '../auth'; +import { AppConfigService } from '../app-config'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { WebSocketLink } from '@apollo/client/link/ws'; import { diff --git a/lib/core/src/public-api.ts b/lib/core/src/public-api.ts index 63bb4d692ad..2087b3aeedb 100644 --- a/lib/core/src/public-api.ts +++ b/lib/core/src/public-api.ts @@ -56,6 +56,7 @@ export * from './lib/mock/index'; export * from './lib/testing'; export * from './lib/auth'; +export * from './lib/services'; export * from './lib/common'; export * from './lib/core.module'; diff --git a/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts b/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts index 1646b3c97d9..d781fe3f140 100644 --- a/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts @@ -17,8 +17,7 @@ import { gql } from '@apollo/client/core'; import { Injectable } from '@angular/core'; -import { WebSocketService } from './web-socket.service'; - +import { WebSocketService } from '@alfresco/adf-core'; @Injectable({ providedIn: 'root' })