From 4a773fa53796e17ce5df325ed7d40ba2cb686476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Tue, 20 Sep 2022 13:20:52 +0200 Subject: [PATCH] feat(api): add websocket api definitions --- packages/api/jest.config.js | 8 +-- packages/api/package.json | 3 +- .../WebSocket/WebSocketApiOperations.ts | 3 + .../WebSocket/WebSocketApiService.spec.ts | 61 +++++++++++++++++++ .../Client/WebSocket/WebSocketApiService.ts | 33 ++++++++++ .../WebSocket/WebSocketApiServiceInterface.ts | 5 ++ packages/api/src/Domain/Client/index.ts | 2 + .../WebSocketConnectionTokenRequestParams.ts | 3 + packages/api/src/Domain/Request/index.ts | 1 + .../WebSocketConnectionTokenResponse.ts | 9 +++ .../WebSocketConnectionTokenResponseBody.ts | 3 + packages/api/src/Domain/Response/index.ts | 2 + .../api/src/Domain/Server/WebSocket/Paths.ts | 9 +++ .../Server/WebSocket/WebSocketServer.spec.ts | 27 ++++++++ .../Server/WebSocket/WebSocketServer.ts | 17 ++++++ .../WebSocket/WebSocketServerInterface.ts | 6 ++ packages/api/src/Domain/Server/index.ts | 2 + yarn.lock | 1 + 18 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 packages/api/src/Domain/Client/WebSocket/WebSocketApiOperations.ts create mode 100644 packages/api/src/Domain/Client/WebSocket/WebSocketApiService.spec.ts create mode 100644 packages/api/src/Domain/Client/WebSocket/WebSocketApiService.ts create mode 100644 packages/api/src/Domain/Client/WebSocket/WebSocketApiServiceInterface.ts create mode 100644 packages/api/src/Domain/Request/WebSocket/WebSocketConnectionTokenRequestParams.ts create mode 100644 packages/api/src/Domain/Response/WebSocket/WebSocketConnectionTokenResponse.ts create mode 100644 packages/api/src/Domain/Response/WebSocket/WebSocketConnectionTokenResponseBody.ts create mode 100644 packages/api/src/Domain/Server/WebSocket/Paths.ts create mode 100644 packages/api/src/Domain/Server/WebSocket/WebSocketServer.spec.ts create mode 100644 packages/api/src/Domain/Server/WebSocket/WebSocketServer.ts create mode 100644 packages/api/src/Domain/Server/WebSocket/WebSocketServerInterface.ts diff --git a/packages/api/jest.config.js b/packages/api/jest.config.js index aabae3703..4faa56c6c 100644 --- a/packages/api/jest.config.js +++ b/packages/api/jest.config.js @@ -10,10 +10,10 @@ module.exports = { }, coverageThreshold: { global: { - branches: 17, - functions: 43, - lines: 46, - statements: 46 + branches: 20, + functions: 66, + lines: 63, + statements: 63 } } }; diff --git a/packages/api/package.json b/packages/api/package.json index 3270eb5b1..6eb974974 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -32,7 +32,8 @@ "eslint": "^8.23.0", "eslint-plugin-prettier": "*", "jest": "^28.1.2", - "ts-jest": "^28.0.5" + "ts-jest": "^28.0.5", + "typescript": "*" }, "dependencies": { "@standardnotes/common": "^1.32.0", diff --git a/packages/api/src/Domain/Client/WebSocket/WebSocketApiOperations.ts b/packages/api/src/Domain/Client/WebSocket/WebSocketApiOperations.ts new file mode 100644 index 000000000..df1ac0b6c --- /dev/null +++ b/packages/api/src/Domain/Client/WebSocket/WebSocketApiOperations.ts @@ -0,0 +1,3 @@ +export enum WebSocketApiOperations { + CreatingConnectionToken, +} diff --git a/packages/api/src/Domain/Client/WebSocket/WebSocketApiService.spec.ts b/packages/api/src/Domain/Client/WebSocket/WebSocketApiService.spec.ts new file mode 100644 index 000000000..911e26966 --- /dev/null +++ b/packages/api/src/Domain/Client/WebSocket/WebSocketApiService.spec.ts @@ -0,0 +1,61 @@ +import { WebSocketConnectionTokenResponse } from '../../Response' + +import { WebSocketServerInterface } from '../../Server/WebSocket/WebSocketServerInterface' +import { WebSocketApiOperations } from './WebSocketApiOperations' + +import { WebSocketApiService } from './WebSocketApiService' + +describe('WebSocketApiService', () => { + let webSocketServer: WebSocketServerInterface + + const createService = () => new WebSocketApiService(webSocketServer) + + beforeEach(() => { + webSocketServer = {} as jest.Mocked + webSocketServer.createConnectionToken = jest.fn().mockReturnValue({ + data: { token: 'foobar' }, + } as jest.Mocked) + }) + + it('should create a websocket connection token', async () => { + const response = await createService().createConnectionToken() + + expect(response).toEqual({ + data: { + token: 'foobar', + }, + }) + expect(webSocketServer.createConnectionToken).toHaveBeenCalledWith({}) + }) + + it('should not create a token if it is already creating', async () => { + const service = createService() + Object.defineProperty(service, 'operationsInProgress', { + get: () => new Map([[WebSocketApiOperations.CreatingConnectionToken, true]]), + }) + + let error = null + try { + await service.createConnectionToken() + } catch (caughtError) { + error = caughtError + } + + expect(error).not.toBeNull() + }) + + it('should not create a token if the server fails', async () => { + webSocketServer.createConnectionToken = jest.fn().mockImplementation(() => { + throw new Error('Oops') + }) + + let error = null + try { + await createService().createConnectionToken() + } catch (caughtError) { + error = caughtError + } + + expect(error).not.toBeNull() + }) +}) diff --git a/packages/api/src/Domain/Client/WebSocket/WebSocketApiService.ts b/packages/api/src/Domain/Client/WebSocket/WebSocketApiService.ts new file mode 100644 index 000000000..6a6817161 --- /dev/null +++ b/packages/api/src/Domain/Client/WebSocket/WebSocketApiService.ts @@ -0,0 +1,33 @@ +import { ErrorMessage } from '../../Error/ErrorMessage' +import { ApiCallError } from '../../Error/ApiCallError' + +import { WebSocketApiServiceInterface } from './WebSocketApiServiceInterface' +import { WebSocketApiOperations } from './WebSocketApiOperations' +import { WebSocketServerInterface } from '../../Server' +import { WebSocketConnectionTokenResponse } from '../../Response' + +export class WebSocketApiService implements WebSocketApiServiceInterface { + private operationsInProgress: Map + + constructor(private webSocketServer: WebSocketServerInterface) { + this.operationsInProgress = new Map() + } + + async createConnectionToken(): Promise { + if (this.operationsInProgress.get(WebSocketApiOperations.CreatingConnectionToken)) { + throw new ApiCallError(ErrorMessage.GenericInProgress) + } + + this.operationsInProgress.set(WebSocketApiOperations.CreatingConnectionToken, true) + + try { + const response = await this.webSocketServer.createConnectionToken({}) + + this.operationsInProgress.set(WebSocketApiOperations.CreatingConnectionToken, false) + + return response + } catch (error) { + throw new ApiCallError(ErrorMessage.GenericFail) + } + } +} diff --git a/packages/api/src/Domain/Client/WebSocket/WebSocketApiServiceInterface.ts b/packages/api/src/Domain/Client/WebSocket/WebSocketApiServiceInterface.ts new file mode 100644 index 000000000..6770e2e4d --- /dev/null +++ b/packages/api/src/Domain/Client/WebSocket/WebSocketApiServiceInterface.ts @@ -0,0 +1,5 @@ +import { WebSocketConnectionTokenResponse } from '../../Response' + +export interface WebSocketApiServiceInterface { + createConnectionToken(): Promise +} diff --git a/packages/api/src/Domain/Client/index.ts b/packages/api/src/Domain/Client/index.ts index beb912853..0146d7484 100644 --- a/packages/api/src/Domain/Client/index.ts +++ b/packages/api/src/Domain/Client/index.ts @@ -3,3 +3,5 @@ export * from './Subscription/SubscriptionApiService' export * from './Subscription/SubscriptionApiServiceInterface' export * from './User/UserApiService' export * from './User/UserApiServiceInterface' +export * from './WebSocket/WebSocketApiService' +export * from './WebSocket/WebSocketApiServiceInterface' diff --git a/packages/api/src/Domain/Request/WebSocket/WebSocketConnectionTokenRequestParams.ts b/packages/api/src/Domain/Request/WebSocket/WebSocketConnectionTokenRequestParams.ts new file mode 100644 index 000000000..757495756 --- /dev/null +++ b/packages/api/src/Domain/Request/WebSocket/WebSocketConnectionTokenRequestParams.ts @@ -0,0 +1,3 @@ +export type WebSocketConnectionTokenRequestParams = { + [additionalParam: string]: unknown +} diff --git a/packages/api/src/Domain/Request/index.ts b/packages/api/src/Domain/Request/index.ts index f1f75adf3..daed7d22b 100644 --- a/packages/api/src/Domain/Request/index.ts +++ b/packages/api/src/Domain/Request/index.ts @@ -5,3 +5,4 @@ export * from './Subscription/SubscriptionInviteDeclineRequestParams' export * from './Subscription/SubscriptionInviteListRequestParams' export * from './Subscription/SubscriptionInviteRequestParams' export * from './User/UserRegistrationRequestParams' +export * from './WebSocket/WebSocketConnectionTokenRequestParams' diff --git a/packages/api/src/Domain/Response/WebSocket/WebSocketConnectionTokenResponse.ts b/packages/api/src/Domain/Response/WebSocket/WebSocketConnectionTokenResponse.ts new file mode 100644 index 000000000..a2629c0a4 --- /dev/null +++ b/packages/api/src/Domain/Response/WebSocket/WebSocketConnectionTokenResponse.ts @@ -0,0 +1,9 @@ +import { Either } from '@standardnotes/common' + +import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody' +import { HttpResponse } from '../../Http/HttpResponse' +import { WebSocketConnectionTokenResponseBody } from './WebSocketConnectionTokenResponseBody' + +export interface WebSocketConnectionTokenResponse extends HttpResponse { + data: Either +} diff --git a/packages/api/src/Domain/Response/WebSocket/WebSocketConnectionTokenResponseBody.ts b/packages/api/src/Domain/Response/WebSocket/WebSocketConnectionTokenResponseBody.ts new file mode 100644 index 000000000..1e367b227 --- /dev/null +++ b/packages/api/src/Domain/Response/WebSocket/WebSocketConnectionTokenResponseBody.ts @@ -0,0 +1,3 @@ +export type WebSocketConnectionTokenResponseBody = { + token: string +} diff --git a/packages/api/src/Domain/Response/index.ts b/packages/api/src/Domain/Response/index.ts index b441a9a1d..6b2890979 100644 --- a/packages/api/src/Domain/Response/index.ts +++ b/packages/api/src/Domain/Response/index.ts @@ -10,3 +10,5 @@ export * from './Subscription/SubscriptionInviteResponse' export * from './Subscription/SubscriptionInviteResponseBody' export * from './User/UserRegistrationResponse' export * from './User/UserRegistrationResponseBody' +export * from './WebSocket/WebSocketConnectionTokenResponse' +export * from './WebSocket/WebSocketConnectionTokenResponseBody' diff --git a/packages/api/src/Domain/Server/WebSocket/Paths.ts b/packages/api/src/Domain/Server/WebSocket/Paths.ts new file mode 100644 index 000000000..b0c1d8f95 --- /dev/null +++ b/packages/api/src/Domain/Server/WebSocket/Paths.ts @@ -0,0 +1,9 @@ +const TokenPaths = { + createConnectionToken: '/v1/sockets/tokens', +} + +export const Paths = { + v1: { + ...TokenPaths, + }, +} diff --git a/packages/api/src/Domain/Server/WebSocket/WebSocketServer.spec.ts b/packages/api/src/Domain/Server/WebSocket/WebSocketServer.spec.ts new file mode 100644 index 000000000..9469b135e --- /dev/null +++ b/packages/api/src/Domain/Server/WebSocket/WebSocketServer.spec.ts @@ -0,0 +1,27 @@ +import { HttpServiceInterface } from '../../Http' +import { WebSocketConnectionTokenResponse } from '../../Response' + +import { WebSocketServer } from './WebSocketServer' + +describe('WebSocketServer', () => { + let httpService: HttpServiceInterface + + const createServer = () => new WebSocketServer(httpService) + + beforeEach(() => { + httpService = {} as jest.Mocked + httpService.post = jest.fn().mockReturnValue({ + data: { token: 'foobar' }, + } as jest.Mocked) + }) + + it('should create a websocket connection token', async () => { + const response = await createServer().createConnectionToken({}) + + expect(response).toEqual({ + data: { + token: 'foobar', + }, + }) + }) +}) diff --git a/packages/api/src/Domain/Server/WebSocket/WebSocketServer.ts b/packages/api/src/Domain/Server/WebSocket/WebSocketServer.ts new file mode 100644 index 000000000..19213b2d6 --- /dev/null +++ b/packages/api/src/Domain/Server/WebSocket/WebSocketServer.ts @@ -0,0 +1,17 @@ +import { HttpServiceInterface } from '../../Http/HttpServiceInterface' +import { WebSocketConnectionTokenRequestParams } from '../../Request/WebSocket/WebSocketConnectionTokenRequestParams' +import { WebSocketConnectionTokenResponse } from '../../Response/WebSocket/WebSocketConnectionTokenResponse' +import { Paths } from './Paths' +import { WebSocketServerInterface } from './WebSocketServerInterface' + +export class WebSocketServer implements WebSocketServerInterface { + constructor(private httpService: HttpServiceInterface) {} + + async createConnectionToken( + params: WebSocketConnectionTokenRequestParams, + ): Promise { + const response = await this.httpService.post(Paths.v1.createConnectionToken, params) + + return response as WebSocketConnectionTokenResponse + } +} diff --git a/packages/api/src/Domain/Server/WebSocket/WebSocketServerInterface.ts b/packages/api/src/Domain/Server/WebSocket/WebSocketServerInterface.ts new file mode 100644 index 000000000..3faec05a5 --- /dev/null +++ b/packages/api/src/Domain/Server/WebSocket/WebSocketServerInterface.ts @@ -0,0 +1,6 @@ +import { WebSocketConnectionTokenRequestParams } from '../../Request/WebSocket/WebSocketConnectionTokenRequestParams' +import { WebSocketConnectionTokenResponse } from '../../Response/WebSocket/WebSocketConnectionTokenResponse' + +export interface WebSocketServerInterface { + createConnectionToken(params: WebSocketConnectionTokenRequestParams): Promise +} diff --git a/packages/api/src/Domain/Server/index.ts b/packages/api/src/Domain/Server/index.ts index ee655010a..013c4d9a8 100644 --- a/packages/api/src/Domain/Server/index.ts +++ b/packages/api/src/Domain/Server/index.ts @@ -2,3 +2,5 @@ export * from './Subscription/SubscriptionServer' export * from './Subscription/SubscriptionServerInterface' export * from './User/UserServer' export * from './User/UserServerInterface' +export * from './WebSocket/WebSocketServer' +export * from './WebSocket/WebSocketServerInterface' diff --git a/yarn.lock b/yarn.lock index 76541af24..48687897d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6397,6 +6397,7 @@ __metadata: jest: ^28.1.2 reflect-metadata: ^0.1.13 ts-jest: ^28.0.5 + typescript: "*" languageName: unknown linkType: soft