diff --git a/libs/angular-integration-interface/src/index.ts b/libs/angular-integration-interface/src/index.ts index 8b01a8b5..ffd738f4 100644 --- a/libs/angular-integration-interface/src/index.ts +++ b/libs/angular-integration-interface/src/index.ts @@ -6,6 +6,7 @@ export * from './lib/services/portal-message.service' export * from './lib/services/theme.service' export * from './lib/services/remote-components.service' export * from './lib/services/initialize-module-guard.service' +export * from './lib/services/workspace.service' // models export * from './lib/model/config-key.model' diff --git a/libs/angular-integration-interface/src/lib/services/workspace.service.spec.ts b/libs/angular-integration-interface/src/lib/services/workspace.service.spec.ts new file mode 100644 index 00000000..f5e7d44a --- /dev/null +++ b/libs/angular-integration-interface/src/lib/services/workspace.service.spec.ts @@ -0,0 +1,321 @@ +import { TestBed } from '@angular/core/testing' +import { WorkspaceService } from './workspace.service' +import { AppStateServiceMock, provideAppStateServiceMock } from '@onecx/angular-integration-interface/mocks' + +describe('WorkspaceService', () => { + let service: WorkspaceService + let mockAppStateService: AppStateServiceMock + const params: Record = { + id: 5, + key: 'xy', + } + + const paramsWrong: Record = { + idx: 5, + } + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideAppStateServiceMock()], + }) + + service = TestBed.inject(WorkspaceService) + mockAppStateService = TestBed.inject(AppStateServiceMock) + + mockAppStateService.currentWorkspace$.publish({ + portalName: 'test-portal', + workspaceName: 'test-workspace', + microfrontendRegistrations: [], + baseUrl: 'http://example.com', + routes: [ + { + appId: 'onecx-workspace-ui', + productName: 'onecx-workspace', + baseUrl: 'http://example.com/workspace/baseurl', + endpoints: [ + { name: 'details', path: '/details/{id}' }, + { name: 'edit', path: '[[details]]' }, + { name: 'change', path: '[[edit]]' }, + ], + }, + ], + }) + }) + + it('should be created', () => { + expect(service).toBeTruthy() + }) + + describe('getUrl', () => { + it('should find endpoint and return correct url from route and endpoint ', (done) => { + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'details', params).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl/details/5') + done() + }) + }) + + it('should return empty string when workspace baseUrl is empty string"', (done) => { + mockAppStateService.currentWorkspace$.publish({ + portalName: 'test-portal', + workspaceName: 'test-workspace', + microfrontendRegistrations: [], + baseUrl: '', + routes: [], + }) + + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'detailswrong', params).subscribe((url) => { + expect(url).toBe('') + done() + }) + }) + + it('should return workspace baseUrl when workspace has no routes at all"', (done) => { + mockAppStateService.currentWorkspace$.publish({ + portalName: 'test-portal', + workspaceName: 'test-workspace', + microfrontendRegistrations: [], + baseUrl: 'http://example.com', + }) + + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'detailswrong', params).subscribe((url) => { + expect(url).toBe('http://example.com') + done() + }) + }) + + it('should return workspace baseUrl when workspace.routes is empty"', (done) => { + mockAppStateService.currentWorkspace$.publish({ + portalName: 'test-portal', + workspaceName: 'test-workspace', + microfrontendRegistrations: [], + baseUrl: 'http://example.com', + routes: [], + }) + + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'detailswrong', params).subscribe((url) => { + expect(url).toBe('http://example.com') + done() + }) + }) + + it('should return workspace baseUrl when route for appId and productName was not found"', (done) => { + service.getUrl('onecx-workspace-uix', 'onecx-workspace', 'details', {}).subscribe((url) => { + expect(url).toBe('http://example.com') + done() + }) + }) + + it('should return workspace baseUrl and endpoints when route has no baseUrl', (done) => { + mockAppStateService.currentWorkspace$.publish({ + portalName: 'test-portal', + workspaceName: 'test-workspace', + microfrontendRegistrations: [], + baseUrl: 'http://example.com', + routes: [ + { + appId: 'onecx-workspace-ui', + productName: 'onecx-workspace', + endpoints: [ + { name: 'details', path: '/details/{id}' }, + { name: 'edit', path: '[[details]]' }, + { name: 'change', path: '[[edit]]' }, + ], + }, + ], + }) + + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'details', params).subscribe((url) => { + expect(url).toBe('http://example.com/details/5') + done() + }) + }) + + it('should return workspace baseUrl with endpoints when route has empty baseUrl', (done) => { + mockAppStateService.currentWorkspace$.publish({ + portalName: 'test-portal', + workspaceName: 'test-workspace', + microfrontendRegistrations: [], + baseUrl: 'http://example.com', + routes: [ + { + appId: 'onecx-workspace-ui', + productName: 'onecx-workspace', + baseUrl: '', + endpoints: [ + { name: 'details', path: '/details/{id}' }, + { name: 'edit', path: '[[details]]' }, + { name: 'change', path: '[[edit]]' }, + ], + }, + ], + }) + + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'details', params).subscribe((url) => { + expect(url).toBe('http://example.com/details/5') + done() + }) + }) + + it('should return route.baseUrl when endpoints are empty"', (done) => { + mockAppStateService.currentWorkspace$.publish({ + portalName: 'test-portal', + workspaceName: 'test-workspace', + microfrontendRegistrations: [], + baseUrl: 'http://example.com', + routes: [ + { + appId: 'onecx-workspace-ui', + productName: 'onecx-workspace', + baseUrl: 'http://example.com/workspace/baseurl', + endpoints: [], + }, + ], + }) + + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'detailswrong', params).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl') + done() + }) + }) + + it('should return route.baseUrl when endpoint was not found', (done) => { + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'detailswrong', params).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl') + done() + }) + }) + + it('should return well formed url for endpoint with 1 alias', (done) => { + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'edit', params).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl/details/5') + done() + }) + }) + + it('should return well formed url for endpoint with 2 alias ', (done) => { + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'change', params).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl/details/5') + done() + }) + }) + + it('should return baseurl when endpoint was not found', (done) => { + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'changexy', params).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl') + done() + }) + }) + + it('should return baseurl when endpoint has wrong alias', (done) => { + mockAppStateService.currentWorkspace$.publish({ + portalName: 'test-portal', + workspaceName: 'test-workspace', + microfrontendRegistrations: [], + baseUrl: 'http://example.com', + routes: [ + { + appId: 'onecx-workspace-ui', + productName: 'onecx-workspace', + baseUrl: 'http://example.com/workspace/baseurl', + endpoints: [ + { name: 'details', path: '/details/{id}/{key}' }, + { name: 'change', path: '[[edit]]' }, + ], + }, + ], + }) + + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'change', params).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl') + done() + }) + }) + + it('should return baseurl when param was not found"', (done) => { + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'details', paramsWrong).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl') + done() + }) + }) + + it('should baseurl without endpoint when params are empty"', (done) => { + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'details', {}).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl') + done() + }) + }) + + it('should return well formed url with 2 params in endpoint', (done) => { + mockAppStateService.currentWorkspace$.publish({ + portalName: 'test-portal', + workspaceName: 'test-workspace', + microfrontendRegistrations: [], + baseUrl: 'http://example.com', + routes: [ + { + appId: 'onecx-workspace-ui', + productName: 'onecx-workspace', + baseUrl: 'http://example.com/workspace/baseurl/', + endpoints: [ + { name: 'details', path: '/details/{id}/{key}' }, + { name: 'edit', path: '[[details]]' }, + { name: 'change', path: '[[edit]]' }, + ], + }, + ], + }) + + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'details', params).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl/details/5/xy') + done() + }) + }) + + it('should return route.baseUrl when no endpoints are available"', (done) => { + mockAppStateService.currentWorkspace$.publish({ + portalName: 'test-portal', + workspaceName: 'test-workspace', + microfrontendRegistrations: [], + baseUrl: 'http://example.com', + routes: [ + { + appId: 'onecx-workspace-ui', + productName: 'onecx-workspace', + baseUrl: 'http://example.com/workspace/baseurl', + }, + ], + }) + + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'detailswrong', params).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl') + done() + }) + }) + + it('should return well formed url although double / are retrieved', (done) => { + mockAppStateService.currentWorkspace$.publish({ + portalName: 'test-portal', + workspaceName: 'test-workspace', + microfrontendRegistrations: [], + baseUrl: 'http://example.com', + routes: [ + { + appId: 'onecx-workspace-ui', + productName: 'onecx-workspace', + baseUrl: 'http://example.com/workspace/baseurl/', + endpoints: [ + { name: 'details', path: '/details/{id}' }, + { name: 'edit', path: '[[details]]' }, + { name: 'change', path: '[[edit]]' }, + ], + }, + ], + }) + + service.getUrl('onecx-workspace-ui', 'onecx-workspace', 'details', params).subscribe((url) => { + expect(url).toBe('http://example.com/workspace/baseurl/details/5') + done() + }) + }) + }) +}) diff --git a/libs/angular-integration-interface/src/lib/services/workspace.service.ts b/libs/angular-integration-interface/src/lib/services/workspace.service.ts new file mode 100644 index 00000000..a08da7a6 --- /dev/null +++ b/libs/angular-integration-interface/src/lib/services/workspace.service.ts @@ -0,0 +1,154 @@ +import { Injectable } from '@angular/core' +import { AppStateService } from './app-state.service' +import { Observable, map } from 'rxjs' +import { Route } from '@onecx/integration-interface' +import { Endpoint } from '@onecx/integration-interface' +import { Location } from '@angular/common' + +@Injectable({ + providedIn: 'root', +}) +export class WorkspaceService { + private aliasStart = '[[' + private aliasEnd = ']]' + private paramStart = '{' + private paramEnd = '}' + + constructor(protected appStateService: AppStateService) {} + + getUrl( + appId: string, + productName: string, + endpointName: string, + params: Record = {} + ): Observable { + return this.appStateService.currentWorkspace$.pipe( + map((workspace) => { + const finalUrl = this.constructRouteUrl(workspace, appId, productName, endpointName, params) + return finalUrl + }) + ) + } + + private constructBaseUrlFromWorkspace(workspace: any): string { + if (workspace.baseUrl === undefined) { + console.log('WARNING: There was no baseUrl for received workspace.') + return '' + } + return workspace.baseUrl + } + + private constructRouteUrl( + workspace: any, + appId: string, + productName: string, + endpointName: string, + params: Record + ): string { + const route = this.filterRouteFromList(workspace.routes, appId, productName) + let url = this.constructBaseUrlFromWorkspace(workspace) + if (!route) { + console.log( + `WARNING: No route.baseUrl could be found for given appId "${appId}" and productName "${productName}"` + ) + + return url + } + + if (route.baseUrl !== undefined && route.baseUrl.length > 0) { + url = route.baseUrl + } + + url = Location.joinWithSlash(url, this.constructEndpointUrl(route, endpointName, params)) + return url + } + + private constructEndpointUrl(route: any, endpointName: string, params: Record): string { + if (!route.endpoints) { + return '' + } + const finalEndpoint = this.dissolveEndpoint(endpointName, route.endpoints) + if (!finalEndpoint || finalEndpoint.path === undefined) { + console.log('WARNING: No endpoint or endpoint.path could be found') + return '' + } + + const paramsFilled = this.fillParamsForPath(finalEndpoint.path, params) + if (paramsFilled === undefined) { + console.log('WARNING: Params could not be filled correctly') + return '' + } + + return paramsFilled + } + + private filterRouteFromList(routes: Array, appId: string, productName: string): Route | undefined { + if (!routes) { + return undefined + } + + const productRoutes = routes.filter((route) => route.appId === appId && route.productName === productName) + + if (productRoutes.length === 0) { + return undefined + } + + if (productRoutes.length > 1) { + console.log('WARNING: There were more than one route. First route has been used.') + } + + return productRoutes[0] + } + + private dissolveEndpoint(endpointName: string, endpoints: Array): Endpoint | undefined { + let endpoint = endpoints.find((ep) => ep.name === endpointName) + + if (!endpoint) { + return undefined + } + + while (endpoint.path?.includes(this.aliasStart)) { + const path: string = endpoint.path + const startIdx = path.indexOf(this.aliasStart) + this.aliasStart.length + const endIdx = path.lastIndexOf(this.aliasEnd) + if (endIdx <= startIdx) { + return undefined + } + const aliasName = path.substring(startIdx, endIdx) + endpoint = endpoints.find((ep) => ep.name === aliasName) + + if (!endpoint) { + return undefined + } + } + + return endpoint + } + + private fillParamsForPath(path: string, params: Record): string { + while (path.includes(this.paramStart)) { + const paramName = path.substring( + path.indexOf(this.paramStart) + this.paramStart.length, + path.indexOf(this.paramEnd) + ) + const paramValue = this.getStringFromUnknown(params[paramName]) + if (paramValue != undefined && paramValue.length > 0) { + path = path.replace(this.paramStart.concat(paramName).concat(this.paramEnd), paramValue) + } else { + console.log(`WARNING: Searched param "${paramName}" was not found in given param list `) + return '' + } + } + return path + } + + private getStringFromUnknown(value: unknown): string { + if (value === null || value === undefined) { + return '' + } else if (typeof value === 'string') { + return value + } else { + return String(value) + } + } +} diff --git a/libs/integration-interface/src/index.ts b/libs/integration-interface/src/index.ts index 340c3260..205cb73e 100644 --- a/libs/integration-interface/src/index.ts +++ b/libs/integration-interface/src/index.ts @@ -18,6 +18,8 @@ export * from './lib/topics/configuration/v1/configuration.topic' export * from './lib/topics/current-workspace/v1/current-workspace.topic' export * from './lib/topics/current-workspace/v1/mfe-portal-registration.model' export * from './lib/topics/current-workspace/v1/workspace.model' +export * from './lib/topics/current-workspace/v1/route.model' +export * from './lib/topics/current-workspace/v1/endpoint.model' export * from './lib/topics/is-authenticated/v1/isAuthenticated.topic' diff --git a/libs/integration-interface/src/lib/topics/current-workspace/v1/endpoint.model.ts b/libs/integration-interface/src/lib/topics/current-workspace/v1/endpoint.model.ts new file mode 100644 index 00000000..19677b9f --- /dev/null +++ b/libs/integration-interface/src/lib/topics/current-workspace/v1/endpoint.model.ts @@ -0,0 +1,4 @@ +export interface Endpoint { + name?: string + path?: string + } \ No newline at end of file diff --git a/libs/integration-interface/src/lib/topics/current-workspace/v1/route.model.ts b/libs/integration-interface/src/lib/topics/current-workspace/v1/route.model.ts new file mode 100644 index 00000000..54de444b --- /dev/null +++ b/libs/integration-interface/src/lib/topics/current-workspace/v1/route.model.ts @@ -0,0 +1,16 @@ +import {Endpoint} from './endpoint.model' + +export interface Route { + url?: string + baseUrl?: string + remoteEntryUrl?: string + appId?: string + productName?: string + technology?: string + exposedModule?: string + pathMatch?: string + remoteName?: string + elementName?: string + displayName?: string + endpoints?: Array + } \ No newline at end of file diff --git a/libs/integration-interface/src/lib/topics/current-workspace/v1/workspace.model.ts b/libs/integration-interface/src/lib/topics/current-workspace/v1/workspace.model.ts index 60ee78ac..b7ed22b5 100644 --- a/libs/integration-interface/src/lib/topics/current-workspace/v1/workspace.model.ts +++ b/libs/integration-interface/src/lib/topics/current-workspace/v1/workspace.model.ts @@ -1,4 +1,5 @@ import { MicrofrontendRegistration } from './mfe-portal-registration.model' +import {Route} from './route.model' export interface Workspace { id?: string @@ -58,5 +59,7 @@ export interface Workspace { * @deprecated will be removed */ userUploaded?: boolean - logoSmallImageUrl?: string + logoSmallImageUrl?: string, + + routes?: Array }