From c70040fe5dfd35663b9811fbbaa9370bd0298482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Thu, 25 May 2023 12:57:05 +0200 Subject: [PATCH] feat: add revisions service to home server (#613) * feat: add revisions service to home server * fix: make e2e test suite on home server non-optional * fix(auth): specs --- .github/workflows/common-e2e.yml | 4 +- .pnp.cjs | 11 +- .../Controller/v2/RevisionsControllerV2.ts | 8 +- .../Service/Proxy/DirectCallServiceProxy.ts | 7 +- .../src/Service/Resolver/EndpointResolver.ts | 4 + .../InversifyExpressSessionController.spec.ts | 26 +- .../InversifyExpressSessionController.ts | 17 +- ...ifyExpressSubscriptionInvitesController.ts | 5 +- .../InversifyExpressUsersController.spec.ts | 10 +- .../InversifyExpressUsersController.ts | 10 +- packages/home-server/bin/server.ts | 3 + packages/home-server/package.json | 1 + packages/revisions/bin/server.ts | 10 +- packages/revisions/bin/worker.ts | 10 +- .../Bootstrap/CommonContainerConfigLoader.ts | 82 ---- packages/revisions/src/Bootstrap/Container.ts | 351 ++++++++++++++++++ .../revisions/src/Bootstrap/DataSource.ts | 95 ++--- .../Bootstrap/ServerContainerConfigLoader.ts | 112 ------ packages/revisions/src/Bootstrap/Service.ts | 42 +++ packages/revisions/src/Bootstrap/Types.ts | 71 ++-- .../Bootstrap/WorkerContainerConfigLoader.ts | 155 -------- packages/revisions/src/Bootstrap/index.ts | 1 + .../InversifyExpressRevisionsController.ts | 12 +- .../Middleware}/ApiGatewayAuthMiddleware.ts | 0 .../RevisionMetadataPersistenceMapper.ts | 5 +- packages/revisions/src/index.ts | 1 + yarn.lock | 3 +- 27 files changed, 573 insertions(+), 483 deletions(-) delete mode 100644 packages/revisions/src/Bootstrap/CommonContainerConfigLoader.ts create mode 100644 packages/revisions/src/Bootstrap/Container.ts delete mode 100644 packages/revisions/src/Bootstrap/ServerContainerConfigLoader.ts create mode 100644 packages/revisions/src/Bootstrap/Service.ts delete mode 100644 packages/revisions/src/Bootstrap/WorkerContainerConfigLoader.ts create mode 100644 packages/revisions/src/Bootstrap/index.ts rename packages/revisions/src/{Controller => Infra/InversifyExpress/Middleware}/ApiGatewayAuthMiddleware.ts (100%) create mode 100644 packages/revisions/src/index.ts diff --git a/.github/workflows/common-e2e.yml b/.github/workflows/common-e2e.yml index 1b682879f..974eff1cd 100644 --- a/.github/workflows/common-e2e.yml +++ b/.github/workflows/common-e2e.yml @@ -50,7 +50,7 @@ jobs: run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html e2e-home-server: - name: (WIP - Home Server) E2E Test Suite + name: (Home Server) E2E Test Suite runs-on: ubuntu-latest services: @@ -85,6 +85,7 @@ jobs: sed -i "s/PSEUDO_KEY_PARAMS_KEY=/PSEUDO_KEY_PARAMS_KEY=$(openssl rand -hex 32)/g" packages/home-server/.env echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env echo "REFRESH_TOKEN_AGE=7" >> packages/home-server/.env + echo "REVISIONS_FREQUENCY=5" >> packages/home-server/.env - name: Run Server run: nohup yarn workspace @standardnotes/home-server start & @@ -95,5 +96,4 @@ jobs: run: docker/is-available.sh http://localhost:3123 $(pwd)/logs - name: Run E2E Test Suite - continue-on-error: true run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html?skip_paid_features=true diff --git a/.pnp.cjs b/.pnp.cjs index 65dc0fcbd..19060b4ec 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -4626,6 +4626,7 @@ const RAW_RUNTIME_STATE = ["@standardnotes/auth-server", "workspace:packages/auth"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\ + ["@standardnotes/revisions-server", "workspace:packages/revisions"],\ ["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\ ["@types/cors", "npm:2.8.13"],\ ["@types/express", "npm:4.17.17"],\ @@ -4733,7 +4734,7 @@ const RAW_RUNTIME_STATE = ["reflect-metadata", "npm:0.1.13"],\ ["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\ ["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\ - ["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15"],\ + ["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15"],\ ["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin::version=5.0.4&hash=b5f058"],\ ["winston", "npm:3.8.2"]\ ],\ @@ -4925,7 +4926,7 @@ const RAW_RUNTIME_STATE = ["reflect-metadata", "npm:0.1.13"],\ ["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\ ["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\ - ["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15"],\ + ["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15"],\ ["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin::version=5.0.4&hash=b5f058"],\ ["ua-parser-js", "npm:1.0.35"],\ ["uuid", "npm:9.0.0"],\ @@ -15166,10 +15167,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15", {\ - "packageLocation": "./.yarn/__virtual__/typeorm-virtual-91f15b21d5/0/cache/typeorm-npm-0.3.15-20a6c4f754-db890f14cb.zip/node_modules/typeorm/",\ + ["virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15", {\ + "packageLocation": "./.yarn/__virtual__/typeorm-virtual-7fe891193c/0/cache/typeorm-npm-0.3.15-20a6c4f754-db890f14cb.zip/node_modules/typeorm/",\ "packageDependencies": [\ - ["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15"],\ + ["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15"],\ ["@google-cloud/spanner", null],\ ["@sap/hana-client", null],\ ["@sqltools/formatter", "npm:1.2.5"],\ diff --git a/packages/api-gateway/src/Controller/v2/RevisionsControllerV2.ts b/packages/api-gateway/src/Controller/v2/RevisionsControllerV2.ts index e8a883997..7b46fe072 100644 --- a/packages/api-gateway/src/Controller/v2/RevisionsControllerV2.ts +++ b/packages/api-gateway/src/Controller/v2/RevisionsControllerV2.ts @@ -28,7 +28,7 @@ export class RevisionsControllerV2 extends BaseHttpController { ) } - @httpGet('/:id') + @httpGet('/:uuid') async getRevision(request: Request, response: Response): Promise { await this.httpService.callRevisionsServer( request, @@ -37,12 +37,12 @@ export class RevisionsControllerV2 extends BaseHttpController { 'GET', 'items/:itemUuid/revisions/:id', request.params.itemUuid, - request.params.id, + request.params.uuid, ), ) } - @httpDelete('/:id') + @httpDelete('/:uuid') async deleteRevision(request: Request, response: Response): Promise { await this.httpService.callRevisionsServer( request, @@ -51,7 +51,7 @@ export class RevisionsControllerV2 extends BaseHttpController { 'DELETE', 'items/:itemUuid/revisions/:id', request.params.itemUuid, - request.params.id, + request.params.uuid, ), ) } diff --git a/packages/api-gateway/src/Service/Proxy/DirectCallServiceProxy.ts b/packages/api-gateway/src/Service/Proxy/DirectCallServiceProxy.ts index d6185a01a..38f762585 100644 --- a/packages/api-gateway/src/Service/Proxy/DirectCallServiceProxy.ts +++ b/packages/api-gateway/src/Service/Proxy/DirectCallServiceProxy.ts @@ -66,7 +66,12 @@ export class DirectCallServiceProxy implements ServiceProxyInterface { throw new Error('Revisions service not found') } - await service.handleRequest(request, response, endpointOrMethodIdentifier) + const serviceResponse = (await service.handleRequest(request, response, endpointOrMethodIdentifier)) as { + statusCode: number + json: Record + } + + this.sendDecoratedResponse(response, serviceResponse) } async callSyncingServer(request: never, response: never, endpointOrMethodIdentifier: string): Promise { diff --git a/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts b/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts index d3657584c..d558970d8 100644 --- a/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts +++ b/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts @@ -58,6 +58,10 @@ export class EndpointResolver implements EndpointResolverInterface { ['[POST]:items/sync', 'sync.items.sync'], ['[POST]:items/check-integrity', 'sync.items.check_integrity'], ['[GET]:items/:uuid', 'sync.items.get_item'], + // Revisions Controller V2 + ['[GET]:items/:itemUuid/revisions', 'revisions.revisions.getRevisions'], + ['[GET]:items/:itemUuid/revisions/:id', 'revisions.revisions.getRevision'], + ['[DELETE]:items/:itemUuid/revisions/:id', 'revisions.revisions.deleteRevision'], ]) resolveEndpointOrMethodIdentifier(method: string, endpoint: string, ...params: string[]): string { diff --git a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.spec.ts index cf7ea7fb5..2fb9a142d 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.spec.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.spec.ts @@ -64,16 +64,12 @@ describe('InversifyExpressSessionController', () => { }, }) - await createController().refresh(request, response) + const httpResult = await createController().refresh(request, response) + const result = await httpResult.executeAsync() - expect(response.send).toHaveBeenCalledWith({ - session: { - access_token: '1231', - refresh_token: '2341', - access_expiration: 123123, - refresh_expiration: 123123, - }, - }) + expect(await result.content.readAsStringAsync()).toEqual( + '{"session":{"access_token":"1231","refresh_token":"2341","access_expiration":123123,"refresh_expiration":123123}}', + ) }) it('should return bad request if tokens are missing from refresh token request', async () => { @@ -113,14 +109,15 @@ describe('InversifyExpressSessionController', () => { } request.body.uuid = '123' - await createController().deleteSession(request, response) + const httpResult = await createController().deleteSession(request, response) + const result = await httpResult.executeAsync() expect(deleteSessionForUser.execute).toBeCalledWith({ userUuid: '123', sessionUuid: '123', }) - expect(response.status).toHaveBeenCalledWith(204) + expect(result.statusCode).toEqual(204) }) it('should not delete a specific session is current session has read only access', async () => { @@ -205,15 +202,16 @@ describe('InversifyExpressSessionController', () => { uuid: '234', }, } - await createController().deleteAllSessions(request, response) + + const httpResult = await createController().deleteAllSessions(request, response) + const result = await httpResult.executeAsync() expect(deletePreviousSessionsForUser.execute).toHaveBeenCalledWith({ userUuid: '123', currentSessionUuid: '234', }) - expect(response.status).toHaveBeenCalledWith(204) - expect(response.send).toHaveBeenCalled() + expect(result.statusCode).toEqual(204) }) it('should not delete all sessions if current sessions has read only access', async () => { diff --git a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.ts b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.ts index d2356c6bc..7b1fdf967 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.ts @@ -32,7 +32,7 @@ export class InversifyExpressSessionController extends BaseHttpController { } @httpDelete('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware) - async deleteSession(request: Request, response: Response): Promise { + async deleteSession(request: Request, response: Response): Promise { if (response.locals.readOnlyAccess) { return this.json( { @@ -84,11 +84,15 @@ export class InversifyExpressSessionController extends BaseHttpController { } response.setHeader('x-invalidate-cache', response.locals.user.uuid) - response.status(204).send() + + return this.statusCode(204) } @httpDelete('/all', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware) - async deleteAllSessions(_request: Request, response: Response): Promise { + async deleteAllSessions( + _request: Request, + response: Response, + ): Promise { if (response.locals.readOnlyAccess) { return this.json( { @@ -118,11 +122,12 @@ export class InversifyExpressSessionController extends BaseHttpController { }) response.setHeader('x-invalidate-cache', response.locals.user.uuid) - response.status(204).send() + + return this.statusCode(204) } @httpPost('/refresh') - async refresh(request: Request, response: Response): Promise { + async refresh(request: Request, response: Response): Promise { if (!request.body.access_token || !request.body.refresh_token) { return this.json( { @@ -152,7 +157,7 @@ export class InversifyExpressSessionController extends BaseHttpController { } response.setHeader('x-invalidate-cache', result.userUuid as string) - response.send({ + return this.json({ session: result.sessionPayload, }) } diff --git a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController.ts b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController.ts index 284de7bb6..564d38bf4 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController.ts @@ -32,14 +32,15 @@ export class InversifyExpressSubscriptionInvitesController extends BaseHttpContr } @httpPost('/:inviteUuid/accept', TYPES.Auth_RequiredCrossServiceTokenMiddleware) - async acceptInvite(request: Request, response: Response): Promise { + async acceptInvite(request: Request, response: Response): Promise { const result = await this.subscriptionInvitesController.acceptInvite({ api: request.query.api as ApiVersion, inviteUuid: request.params.inviteUuid, }) response.setHeader('x-invalidate-cache', response.locals.user.uuid) - response.status(result.status).send(result.data) + + return this.json(result.data, result.status) } @httpGet('/:inviteUuid/decline') diff --git a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.spec.ts index 7cb985758..b1093e450 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.spec.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.spec.ts @@ -99,7 +99,8 @@ describe('InversifyExpressUsersController', () => { updateUser.execute = jest.fn().mockReturnValue({ success: true, authResponse: { foo: 'bar' } }) - await createController().update(request, response) + const httpResponse = await createController().update(request, response) + const result = await httpResponse.executeAsync() expect(updateUser.execute).toHaveBeenCalledWith({ apiVersion: '20190520', @@ -112,7 +113,7 @@ describe('InversifyExpressUsersController', () => { }, }) - expect(response.send).toHaveBeenCalledWith({ foo: 'bar' }) + expect(await result.content.readAsStringAsync()).toEqual('{"foo":"bar"}') }) it('should not update user if session has read only access', async () => { @@ -310,7 +311,8 @@ describe('InversifyExpressUsersController', () => { changeCredentials.execute = jest.fn().mockReturnValue({ success: true, authResponse: { foo: 'bar' } }) - await createController().changeCredentials(request, response) + const httpResponse = await createController().changeCredentials(request, response) + const result = await httpResponse.executeAsync() expect(changeCredentials.execute).toHaveBeenCalledWith({ apiVersion: '20190520', @@ -326,7 +328,7 @@ describe('InversifyExpressUsersController', () => { expect(clearLoginAttempts.execute).toHaveBeenCalled() - expect(response.send).toHaveBeenCalledWith({ foo: 'bar' }) + expect(await result.content.readAsStringAsync()).toEqual('{"foo":"bar"}') }) it('should not change a password if session has read only access', async () => { diff --git a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.ts b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.ts index 9134a6331..8f741767f 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.ts @@ -42,7 +42,7 @@ export class InversifyExpressUsersController extends BaseHttpController { } @httpPatch('/:userId', TYPES.Auth_RequiredCrossServiceTokenMiddleware) - async update(request: Request, response: Response): Promise { + async update(request: Request, response: Response): Promise { if (response.locals.readOnlyAccess) { return this.json( { @@ -83,9 +83,8 @@ export class InversifyExpressUsersController extends BaseHttpController { if (updateResult.success) { response.setHeader('x-invalidate-cache', response.locals.user.uuid) - response.send(updateResult.authResponse) - return + return this.json(updateResult.authResponse) } return this.json( @@ -157,7 +156,7 @@ export class InversifyExpressUsersController extends BaseHttpController { } @httpPut('/:userId/attributes/credentials', TYPES.Auth_RequiredCrossServiceTokenMiddleware) - async changeCredentials(request: Request, response: Response): Promise { + async changeCredentials(request: Request, response: Response): Promise { if (response.locals.readOnlyAccess) { return this.json( { @@ -245,6 +244,7 @@ export class InversifyExpressUsersController extends BaseHttpController { await this.clearLoginAttempts.execute({ email: response.locals.user.email }) response.setHeader('x-invalidate-cache', response.locals.user.uuid) - response.send(changeCredentialsResult.authResponse) + + return this.json(changeCredentialsResult.authResponse) } } diff --git a/packages/home-server/bin/server.ts b/packages/home-server/bin/server.ts index 7393dea5e..2080f40a2 100644 --- a/packages/home-server/bin/server.ts +++ b/packages/home-server/bin/server.ts @@ -5,6 +5,7 @@ import { Service as ApiGatewayService, TYPES as ApiGatewayTYPES } from '@standar import { DirectCallDomainEventPublisher } from '@standardnotes/domain-events-infra' import { Service as AuthService } from '@standardnotes/auth-server' import { Service as SyncingService } from '@standardnotes/syncing-server' +import { Service as RevisionsService } from '@standardnotes/revisions-server' import { Container } from 'inversify' import { InversifyExpressServer } from 'inversify-express-utils' import helmet from 'helmet' @@ -24,11 +25,13 @@ const startServer = async (): Promise => { const apiGatewayService = new ApiGatewayService(serviceContainer) const authService = new AuthService(serviceContainer, controllerContainer, directCallDomainEventPublisher) const syncingService = new SyncingService(serviceContainer, controllerContainer, directCallDomainEventPublisher) + const revisionsService = new RevisionsService(serviceContainer, controllerContainer, directCallDomainEventPublisher) const container = Container.merge( (await apiGatewayService.getContainer()) as Container, (await authService.getContainer()) as Container, (await syncingService.getContainer()) as Container, + (await revisionsService.getContainer()) as Container, ) const env: Env = new Env() diff --git a/packages/home-server/package.json b/packages/home-server/package.json index 6e083f9d2..a17742f5c 100644 --- a/packages/home-server/package.json +++ b/packages/home-server/package.json @@ -22,6 +22,7 @@ "@standardnotes/auth-server": "workspace:^", "@standardnotes/domain-core": "workspace:^", "@standardnotes/domain-events-infra": "workspace:^", + "@standardnotes/revisions-server": "workspace:^", "@standardnotes/syncing-server": "workspace:^", "cors": "2.8.5", "dotenv": "^16.0.1", diff --git a/packages/revisions/bin/server.ts b/packages/revisions/bin/server.ts index d7b1c9c73..e02c9cb15 100644 --- a/packages/revisions/bin/server.ts +++ b/packages/revisions/bin/server.ts @@ -9,20 +9,20 @@ import * as winston from 'winston' import { InversifyExpressServer } from 'inversify-express-utils' import TYPES from '../src/Bootstrap/Types' import { Env } from '../src/Bootstrap/Env' -import { ServerContainerConfigLoader } from '../src/Bootstrap/ServerContainerConfigLoader' +import { ContainerConfigLoader } from '../src/Bootstrap/Container' import '../src/Infra/InversifyExpress/InversifyExpressRevisionsController' import '../src/Infra/InversifyExpress/InversifyExpressHealthCheckController' -const container = new ServerContainerConfigLoader() +const container = new ContainerConfigLoader() void container.load().then((container) => { - const env: Env = container.get(TYPES.Env) + const env: Env = container.get(TYPES.Revisions_Env) const server = new InversifyExpressServer(container) server.setConfig((app) => { app.use((_request: Request, response: Response, next: NextFunction) => { - response.setHeader('X-Revisions-Version', container.get(TYPES.VERSION)) + response.setHeader('X-Revisions-Version', container.get(TYPES.Revisions_VERSION)) next() }) app.use(json()) @@ -30,7 +30,7 @@ void container.load().then((container) => { app.use(cors()) }) - const logger: winston.Logger = container.get(TYPES.Logger) + const logger: winston.Logger = container.get(TYPES.Revisions_Logger) server.setErrorConfig((app) => { app.use((error: Record, _request: Request, response: Response, _next: NextFunction) => { diff --git a/packages/revisions/bin/worker.ts b/packages/revisions/bin/worker.ts index 6899b06d7..863670b0e 100644 --- a/packages/revisions/bin/worker.ts +++ b/packages/revisions/bin/worker.ts @@ -7,17 +7,19 @@ import { Logger } from 'winston' import TYPES from '../src/Bootstrap/Types' import { Env } from '../src/Bootstrap/Env' import { DomainEventSubscriberFactoryInterface } from '@standardnotes/domain-events' -import { WorkerContainerConfigLoader } from '../src/Bootstrap/WorkerContainerConfigLoader' +import { ContainerConfigLoader } from '../src/Bootstrap/Container' -const container = new WorkerContainerConfigLoader() +const container = new ContainerConfigLoader() void container.load().then((container) => { const env: Env = new Env() env.load() - const logger: Logger = container.get(TYPES.Logger) + const logger: Logger = container.get(TYPES.Revisions_Logger) logger.info('Starting worker...') - const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory) + const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get( + TYPES.Revisions_DomainEventSubscriberFactory, + ) subscriberFactory.create().start() }) diff --git a/packages/revisions/src/Bootstrap/CommonContainerConfigLoader.ts b/packages/revisions/src/Bootstrap/CommonContainerConfigLoader.ts deleted file mode 100644 index 66a18c654..000000000 --- a/packages/revisions/src/Bootstrap/CommonContainerConfigLoader.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { MapperInterface } from '@standardnotes/domain-core' -import { Container, interfaces } from 'inversify' -import { Repository } from 'typeorm' -import * as winston from 'winston' - -import { Revision } from '../Domain/Revision/Revision' -import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata' -import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface' -import { TypeORMRevisionRepository } from '../Infra/TypeORM/TypeORMRevisionRepository' -import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision' -import { RevisionMetadataPersistenceMapper } from '../Mapping/RevisionMetadataPersistenceMapper' -import { RevisionPersistenceMapper } from '../Mapping/RevisionPersistenceMapper' -import { AppDataSource } from './DataSource' -import { Env } from './Env' -import TYPES from './Types' - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const newrelicFormatter = require('@newrelic/winston-enricher') - -export class CommonContainerConfigLoader { - async load(): Promise { - const env: Env = new Env() - env.load() - - const container = new Container({ - defaultScope: 'Singleton', - }) - - await AppDataSource.initialize() - - container.bind(TYPES.Env).toConstantValue(env) - - container.bind(TYPES.Logger).toDynamicValue((context: interfaces.Context) => { - const env: Env = context.container.get(TYPES.Env) - - const newrelicWinstonFormatter = newrelicFormatter(winston) - const winstonFormatters = [winston.format.splat(), winston.format.json()] - if (env.get('NEW_RELIC_ENABLED', true) === 'true') { - winstonFormatters.push(newrelicWinstonFormatter()) - } - - const logger = winston.createLogger({ - level: env.get('LOG_LEVEL') || 'info', - format: winston.format.combine(...winstonFormatters), - transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })], - defaultMeta: { service: 'revisions' }, - }) - - return logger - }) - - container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true)) - container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION')) - - // Map - container - .bind>(TYPES.RevisionMetadataPersistenceMapper) - .toDynamicValue(() => new RevisionMetadataPersistenceMapper()) - container - .bind>(TYPES.RevisionPersistenceMapper) - .toDynamicValue(() => new RevisionPersistenceMapper()) - - // ORM - container - .bind>(TYPES.ORMRevisionRepository) - .toDynamicValue(() => AppDataSource.getRepository(TypeORMRevision)) - - // Repositories - container - .bind(TYPES.RevisionRepository) - .toDynamicValue((context: interfaces.Context) => { - return new TypeORMRevisionRepository( - context.container.get(TYPES.ORMRevisionRepository), - context.container.get(TYPES.RevisionMetadataPersistenceMapper), - context.container.get(TYPES.RevisionPersistenceMapper), - context.container.get(TYPES.Logger), - ) - }) - - return container - } -} diff --git a/packages/revisions/src/Bootstrap/Container.ts b/packages/revisions/src/Bootstrap/Container.ts new file mode 100644 index 000000000..235da02c1 --- /dev/null +++ b/packages/revisions/src/Bootstrap/Container.ts @@ -0,0 +1,351 @@ +import { ControllerContainer, ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core' +import { Container, interfaces } from 'inversify' +import { Repository } from 'typeorm' +import * as winston from 'winston' + +import { Revision } from '../Domain/Revision/Revision' +import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata' +import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface' +import { TypeORMRevisionRepository } from '../Infra/TypeORM/TypeORMRevisionRepository' +import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision' +import { RevisionMetadataPersistenceMapper } from '../Mapping/RevisionMetadataPersistenceMapper' +import { RevisionPersistenceMapper } from '../Mapping/RevisionPersistenceMapper' +import { AppDataSource } from './DataSource' +import { Env } from './Env' +import TYPES from './Types' +import { TokenDecoderInterface, CrossServiceTokenData, TokenDecoder } from '@standardnotes/security' +import { TimerInterface, Timer } from '@standardnotes/time' +import { ApiGatewayAuthMiddleware } from '../Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware' +import { RevisionsController } from '../Controller/RevisionsController' +import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision' +import { GetRequiredRoleToViewRevision } from '../Domain/UseCase/GetRequiredRoleToViewRevision/GetRequiredRoleToViewRevision' +import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision' +import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada' +import { RevisionHttpMapper } from '../Mapping/RevisionHttpMapper' +import { RevisionMetadataHttpMapper } from '../Mapping/RevisionMetadataHttpMapper' +import { S3Client } from '@aws-sdk/client-s3' +import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs' +import { + DomainEventMessageHandlerInterface, + DomainEventHandlerInterface, + DomainEventSubscriberFactoryInterface, +} from '@standardnotes/domain-events' +import { + SQSNewRelicEventMessageHandler, + SQSEventMessageHandler, + SQSDomainEventSubscriberFactory, + DirectCallEventMessageHandler, + DirectCallDomainEventPublisher, +} from '@standardnotes/domain-events-infra' +import { DumpRepositoryInterface } from '../Domain/Dump/DumpRepositoryInterface' +import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler' +import { ItemDumpedEventHandler } from '../Domain/Handler/ItemDumpedEventHandler' +import { RevisionsCopyRequestedEventHandler } from '../Domain/Handler/RevisionsCopyRequestedEventHandler' +import { CopyRevisions } from '../Domain/UseCase/CopyRevisions/CopyRevisions' +import { FSDumpRepository } from '../Infra/FS/FSDumpRepository' +import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository' +import { RevisionItemStringMapper } from '../Mapping/RevisionItemStringMapper' +import { InversifyExpressRevisionsController } from '../Infra/InversifyExpress/InversifyExpressRevisionsController' + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const newrelicFormatter = require('@newrelic/winston-enricher') + +export class ContainerConfigLoader { + async load(configuration?: { + controllerConatiner?: ControllerContainerInterface + directCallDomainEventPublisher?: DirectCallDomainEventPublisher + }): Promise { + const directCallDomainEventPublisher = + configuration?.directCallDomainEventPublisher ?? new DirectCallDomainEventPublisher() + + const env: Env = new Env() + env.load() + + const isConfiguredForHomeServer = env.get('DB_TYPE') === 'sqlite' + + const container = new Container({ + defaultScope: 'Singleton', + }) + + await AppDataSource.initialize() + + container.bind(TYPES.Revisions_Env).toConstantValue(env) + + container.bind(TYPES.Revisions_Logger).toDynamicValue((context: interfaces.Context) => { + const env: Env = context.container.get(TYPES.Revisions_Env) + + const newrelicWinstonFormatter = newrelicFormatter(winston) + const winstonFormatters = [winston.format.splat(), winston.format.json()] + if (env.get('NEW_RELIC_ENABLED', true) === 'true') { + winstonFormatters.push(newrelicWinstonFormatter()) + } + + const logger = winston.createLogger({ + level: env.get('LOG_LEVEL') || 'info', + format: winston.format.combine(...winstonFormatters), + transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })], + defaultMeta: { service: 'revisions' }, + }) + + return logger + }) + + container.bind(TYPES.Revisions_NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true)) + container.bind(TYPES.Revisions_VERSION).toConstantValue(env.get('VERSION')) + + // Map + container + .bind>(TYPES.Revisions_RevisionMetadataPersistenceMapper) + .toDynamicValue(() => new RevisionMetadataPersistenceMapper()) + container + .bind>(TYPES.Revisions_RevisionPersistenceMapper) + .toDynamicValue(() => new RevisionPersistenceMapper()) + + // ORM + container + .bind>(TYPES.Revisions_ORMRevisionRepository) + .toDynamicValue(() => AppDataSource.getRepository(TypeORMRevision)) + + // Repositories + container + .bind(TYPES.Revisions_RevisionRepository) + .toDynamicValue((context: interfaces.Context) => { + return new TypeORMRevisionRepository( + context.container.get(TYPES.Revisions_ORMRevisionRepository), + context.container.get(TYPES.Revisions_RevisionMetadataPersistenceMapper), + context.container.get(TYPES.Revisions_RevisionPersistenceMapper), + context.container.get(TYPES.Revisions_Logger), + ) + }) + + container.bind(TYPES.Revisions_Timer).toDynamicValue(() => new Timer()) + + container + .bind(TYPES.Revisions_GetRequiredRoleToViewRevision) + .toDynamicValue((context: interfaces.Context) => { + return new GetRequiredRoleToViewRevision(context.container.get(TYPES.Revisions_Timer)) + }) + + // Map + container + .bind< + MapperInterface< + Revision, + { + uuid: string + item_uuid: string + content: string | null + content_type: string + items_key_id: string | null + enc_item_key: string | null + auth_hash: string | null + created_at: string + updated_at: string + } + > + >(TYPES.Revisions_RevisionHttpMapper) + .toDynamicValue(() => new RevisionHttpMapper()) + container + .bind< + MapperInterface< + RevisionMetadata, + { + uuid: string + content_type: string + created_at: string + updated_at: string + } + > + >(TYPES.Revisions_RevisionMetadataHttpMapper) + .toDynamicValue((context: interfaces.Context) => { + return new RevisionMetadataHttpMapper(context.container.get(TYPES.Revisions_GetRequiredRoleToViewRevision)) + }) + + // use cases + container + .bind(TYPES.Revisions_GetRevisionsMetada) + .toDynamicValue((context: interfaces.Context) => { + return new GetRevisionsMetada(context.container.get(TYPES.Revisions_RevisionRepository)) + }) + container.bind(TYPES.Revisions_GetRevision).toDynamicValue((context: interfaces.Context) => { + return new GetRevision(context.container.get(TYPES.Revisions_RevisionRepository)) + }) + container.bind(TYPES.Revisions_DeleteRevision).toDynamicValue((context: interfaces.Context) => { + return new DeleteRevision(context.container.get(TYPES.Revisions_RevisionRepository)) + }) + + // env vars + container.bind(TYPES.Revisions_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET')) + + // Controller + container + .bind(TYPES.Revisions_ControllerContainer) + .toConstantValue(configuration?.controllerConatiner ?? new ControllerContainer()) + + container + .bind(TYPES.Revisions_RevisionsController) + .toDynamicValue((context: interfaces.Context) => { + return new RevisionsController( + context.container.get(TYPES.Revisions_GetRevisionsMetada), + context.container.get(TYPES.Revisions_GetRevision), + context.container.get(TYPES.Revisions_DeleteRevision), + context.container.get(TYPES.Revisions_RevisionHttpMapper), + context.container.get(TYPES.Revisions_RevisionMetadataHttpMapper), + context.container.get(TYPES.Revisions_Logger), + ) + }) + + container + .bind>(TYPES.Revisions_CrossServiceTokenDecoder) + .toDynamicValue((context: interfaces.Context) => { + return new TokenDecoder(context.container.get(TYPES.Revisions_AUTH_JWT_SECRET)) + }) + + container + .bind(TYPES.Revisions_ApiGatewayAuthMiddleware) + .toDynamicValue((context: interfaces.Context) => { + return new ApiGatewayAuthMiddleware( + context.container.get(TYPES.Revisions_CrossServiceTokenDecoder), + context.container.get(TYPES.Revisions_Logger), + ) + }) + + // Map + container + .bind>(TYPES.Revisions_RevisionItemStringMapper) + .toDynamicValue(() => new RevisionItemStringMapper()) + + container + .bind(TYPES.Revisions_DumpRepository) + .toConstantValue( + env.get('S3_AWS_REGION', true) + ? new S3DumpRepository( + container.get(TYPES.Revisions_S3_BACKUP_BUCKET_NAME), + container.get(TYPES.Revisions_S3), + container.get(TYPES.Revisions_RevisionItemStringMapper), + container.get(TYPES.Revisions_Logger), + ) + : new FSDumpRepository(container.get(TYPES.Revisions_RevisionItemStringMapper)), + ) + + if (!isConfiguredForHomeServer) { + // env vars + container.bind(TYPES.Revisions_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL')) + container.bind(TYPES.Revisions_S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true)) + container.bind(TYPES.Revisions_S3_BACKUP_BUCKET_NAME).toConstantValue(env.get('S3_BACKUP_BUCKET_NAME', true)) + + container.bind(TYPES.Revisions_SQS).toDynamicValue((context: interfaces.Context) => { + const env: Env = context.container.get(TYPES.Revisions_Env) + + const sqsConfig: SQSClientConfig = { + region: env.get('SQS_AWS_REGION'), + } + if (env.get('SQS_ENDPOINT', true)) { + sqsConfig.endpoint = env.get('SQS_ENDPOINT', true) + } + if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) { + sqsConfig.credentials = { + accessKeyId: env.get('SQS_ACCESS_KEY_ID', true), + secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true), + } + } + + return new SQSClient(sqsConfig) + }) + + container.bind(TYPES.Revisions_S3).toDynamicValue((context: interfaces.Context) => { + const env: Env = context.container.get(TYPES.Revisions_Env) + + let s3Client = undefined + if (env.get('S3_AWS_REGION', true)) { + s3Client = new S3Client({ + apiVersion: 'latest', + region: env.get('S3_AWS_REGION', true), + }) + } + + return s3Client + }) + } + + // use cases + container.bind(TYPES.Revisions_CopyRevisions).toDynamicValue((context: interfaces.Context) => { + return new CopyRevisions(context.container.get(TYPES.Revisions_RevisionRepository)) + }) + + // Handlers + container + .bind(TYPES.Revisions_ItemDumpedEventHandler) + .toDynamicValue((context: interfaces.Context) => { + return new ItemDumpedEventHandler( + context.container.get(TYPES.Revisions_DumpRepository), + context.container.get(TYPES.Revisions_RevisionRepository), + ) + }) + container + .bind(TYPES.Revisions_AccountDeletionRequestedEventHandler) + .toDynamicValue((context: interfaces.Context) => { + return new AccountDeletionRequestedEventHandler( + context.container.get(TYPES.Revisions_RevisionRepository), + context.container.get(TYPES.Revisions_Logger), + ) + }) + container + .bind(TYPES.Revisions_RevisionsCopyRequestedEventHandler) + .toDynamicValue((context: interfaces.Context) => { + return new RevisionsCopyRequestedEventHandler( + context.container.get(TYPES.Revisions_CopyRevisions), + context.container.get(TYPES.Revisions_Logger), + ) + }) + + const eventHandlers: Map = new Map([ + ['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)], + ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Revisions_AccountDeletionRequestedEventHandler)], + ['REVISIONS_COPY_REQUESTED', container.get(TYPES.Revisions_RevisionsCopyRequestedEventHandler)], + ]) + + if (isConfiguredForHomeServer) { + const directCallEventMessageHandler = new DirectCallEventMessageHandler( + eventHandlers, + container.get(TYPES.Revisions_Logger), + ) + directCallDomainEventPublisher.register(directCallEventMessageHandler) + container + .bind(TYPES.Revisions_DomainEventMessageHandler) + .toConstantValue(directCallEventMessageHandler) + } else { + container + .bind(TYPES.Revisions_DomainEventMessageHandler) + .toConstantValue( + env.get('NEW_RELIC_ENABLED', true) === 'true' + ? new SQSNewRelicEventMessageHandler(eventHandlers, container.get(TYPES.Revisions_Logger)) + : new SQSEventMessageHandler(eventHandlers, container.get(TYPES.Revisions_Logger)), + ) + + container + .bind(TYPES.Revisions_DomainEventSubscriberFactory) + .toDynamicValue((context: interfaces.Context) => { + return new SQSDomainEventSubscriberFactory( + context.container.get(TYPES.Revisions_SQS), + context.container.get(TYPES.Revisions_SQS_QUEUE_URL), + context.container.get(TYPES.Revisions_DomainEventMessageHandler), + ) + }) + } + + // Inversify Controllers + if (isConfiguredForHomeServer) { + container + .bind(TYPES.Revisions_InversifyExpressRevisionsController) + .toConstantValue( + new InversifyExpressRevisionsController( + container.get(TYPES.Revisions_RevisionsController), + container.get(TYPES.Revisions_ControllerContainer), + ), + ) + } + + return container + } +} diff --git a/packages/revisions/src/Bootstrap/DataSource.ts b/packages/revisions/src/Bootstrap/DataSource.ts index dfe00f07e..b7bc6a77d 100644 --- a/packages/revisions/src/Bootstrap/DataSource.ts +++ b/packages/revisions/src/Bootstrap/DataSource.ts @@ -15,55 +15,62 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true) ? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true) : 45_000 -const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false +const commonDataSourceOptions = { + maxQueryExecutionTime, + entities: [TypeORMRevision], + migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`], + migrationsRun: true, + logging: env.get('DB_DEBUG_LEVEL', true) ?? 'info', +} -const replicationConfig = { - master: { - host: env.get('DB_HOST'), - port: parseInt(env.get('DB_PORT')), - username: env.get('DB_USERNAME'), - password: env.get('DB_PASSWORD'), - database: env.get('DB_DATABASE'), - }, - slaves: [ - { - host: env.get('DB_REPLICA_HOST', true), +let dataSource: DataSource +if (isConfiguredForMySQL) { + const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false + + const replicationConfig = { + master: { + host: env.get('DB_HOST'), port: parseInt(env.get('DB_PORT')), username: env.get('DB_USERNAME'), password: env.get('DB_PASSWORD'), database: env.get('DB_DATABASE'), }, - ], - removeNodeErrorCount: 10, - restoreNodeTimeout: 5, + slaves: [ + { + host: env.get('DB_REPLICA_HOST', true), + port: parseInt(env.get('DB_PORT')), + username: env.get('DB_USERNAME'), + password: env.get('DB_PASSWORD'), + database: env.get('DB_DATABASE'), + }, + ], + removeNodeErrorCount: 10, + restoreNodeTimeout: 5, + } + + const mySQLDataSourceOptions: MysqlConnectionOptions = { + ...commonDataSourceOptions, + type: 'mysql', + charset: 'utf8mb4', + supportBigNumbers: true, + bigNumberStrings: false, + replication: inReplicaMode ? replicationConfig : undefined, + host: inReplicaMode ? undefined : env.get('DB_HOST'), + port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')), + username: inReplicaMode ? undefined : env.get('DB_USERNAME'), + password: inReplicaMode ? undefined : env.get('DB_PASSWORD'), + database: inReplicaMode ? undefined : env.get('DB_DATABASE'), + } + + dataSource = new DataSource(mySQLDataSourceOptions) +} else { + const sqliteDataSourceOptions: SqliteConnectionOptions = { + ...commonDataSourceOptions, + type: 'sqlite', + database: `data/${env.get('DB_DATABASE')}.sqlite`, + } + + dataSource = new DataSource(sqliteDataSourceOptions) } -const commonDataSourceOptions = { - maxQueryExecutionTime, - entities: [TypeORMRevision], - migrations: [`dist/migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`], - migrationsRun: true, - logging: env.get('DB_DEBUG_LEVEL'), -} - -const mySQLDataSourceOptions: MysqlConnectionOptions = { - ...commonDataSourceOptions, - type: 'mysql', - charset: 'utf8mb4', - supportBigNumbers: true, - bigNumberStrings: false, - replication: inReplicaMode ? replicationConfig : undefined, - host: inReplicaMode ? undefined : env.get('DB_HOST'), - port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')), - username: inReplicaMode ? undefined : env.get('DB_USERNAME'), - password: inReplicaMode ? undefined : env.get('DB_PASSWORD'), - database: inReplicaMode ? undefined : env.get('DB_DATABASE'), -} - -const sqliteDataSourceOptions: SqliteConnectionOptions = { - ...commonDataSourceOptions, - type: 'sqlite', - database: `data/${env.get('DB_DATABASE')}.sqlite`, -} - -export const AppDataSource = new DataSource(isConfiguredForMySQL ? mySQLDataSourceOptions : sqliteDataSourceOptions) +export const AppDataSource = dataSource diff --git a/packages/revisions/src/Bootstrap/ServerContainerConfigLoader.ts b/packages/revisions/src/Bootstrap/ServerContainerConfigLoader.ts deleted file mode 100644 index 8d9f36f28..000000000 --- a/packages/revisions/src/Bootstrap/ServerContainerConfigLoader.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Timer, TimerInterface } from '@standardnotes/time' -import { Container, interfaces } from 'inversify' -import { MapperInterface } from '@standardnotes/domain-core' - -import TYPES from './Types' -import { RevisionsController } from '../Controller/RevisionsController' -import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada' -import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata' -import { Revision } from '../Domain/Revision/Revision' -import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision' -import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision' -import { RevisionHttpMapper } from '../Mapping/RevisionHttpMapper' -import { RevisionMetadataHttpMapper } from '../Mapping/RevisionMetadataHttpMapper' -import { GetRequiredRoleToViewRevision } from '../Domain/UseCase/GetRequiredRoleToViewRevision/GetRequiredRoleToViewRevision' -import { CommonContainerConfigLoader } from './CommonContainerConfigLoader' -import { ApiGatewayAuthMiddleware } from '../Controller/ApiGatewayAuthMiddleware' -import { CrossServiceTokenData, TokenDecoder, TokenDecoderInterface } from '@standardnotes/security' -import { Env } from './Env' - -export class ServerContainerConfigLoader extends CommonContainerConfigLoader { - override async load(): Promise { - const container = await super.load() - - const env: Env = container.get(TYPES.Env) - - container.bind(TYPES.Timer).toDynamicValue(() => new Timer()) - - container - .bind(TYPES.GetRequiredRoleToViewRevision) - .toDynamicValue((context: interfaces.Context) => { - return new GetRequiredRoleToViewRevision(context.container.get(TYPES.Timer)) - }) - - // Map - container - .bind< - MapperInterface< - Revision, - { - uuid: string - item_uuid: string - content: string | null - content_type: string - items_key_id: string | null - enc_item_key: string | null - auth_hash: string | null - created_at: string - updated_at: string - } - > - >(TYPES.RevisionHttpMapper) - .toDynamicValue(() => new RevisionHttpMapper()) - container - .bind< - MapperInterface< - RevisionMetadata, - { - uuid: string - content_type: string - created_at: string - updated_at: string - } - > - >(TYPES.RevisionMetadataHttpMapper) - .toDynamicValue((context: interfaces.Context) => { - return new RevisionMetadataHttpMapper(context.container.get(TYPES.GetRequiredRoleToViewRevision)) - }) - - // use cases - container.bind(TYPES.GetRevisionsMetada).toDynamicValue((context: interfaces.Context) => { - return new GetRevisionsMetada(context.container.get(TYPES.RevisionRepository)) - }) - container.bind(TYPES.GetRevision).toDynamicValue((context: interfaces.Context) => { - return new GetRevision(context.container.get(TYPES.RevisionRepository)) - }) - container.bind(TYPES.DeleteRevision).toDynamicValue((context: interfaces.Context) => { - return new DeleteRevision(context.container.get(TYPES.RevisionRepository)) - }) - - // env vars - container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET')) - - // Controller - container.bind(TYPES.RevisionsController).toDynamicValue((context: interfaces.Context) => { - return new RevisionsController( - context.container.get(TYPES.GetRevisionsMetada), - context.container.get(TYPES.GetRevision), - context.container.get(TYPES.DeleteRevision), - context.container.get(TYPES.RevisionHttpMapper), - context.container.get(TYPES.RevisionMetadataHttpMapper), - context.container.get(TYPES.Logger), - ) - }) - - container - .bind>(TYPES.CrossServiceTokenDecoder) - .toDynamicValue((context: interfaces.Context) => { - return new TokenDecoder(context.container.get(TYPES.AUTH_JWT_SECRET)) - }) - - container - .bind(TYPES.ApiGatewayAuthMiddleware) - .toDynamicValue((context: interfaces.Context) => { - return new ApiGatewayAuthMiddleware( - context.container.get(TYPES.CrossServiceTokenDecoder), - context.container.get(TYPES.Logger), - ) - }) - - return container - } -} diff --git a/packages/revisions/src/Bootstrap/Service.ts b/packages/revisions/src/Bootstrap/Service.ts new file mode 100644 index 000000000..5b1a728dd --- /dev/null +++ b/packages/revisions/src/Bootstrap/Service.ts @@ -0,0 +1,42 @@ +import { + ControllerContainerInterface, + ServiceContainerInterface, + ServiceIdentifier, + ServiceInterface, +} from '@standardnotes/domain-core' + +import { ContainerConfigLoader } from './Container' +import { DirectCallDomainEventPublisher } from '@standardnotes/domain-events-infra' + +export class Service implements ServiceInterface { + constructor( + private serviceContainer: ServiceContainerInterface, + private controllerContainer: ControllerContainerInterface, + private directCallDomainEventPublisher: DirectCallDomainEventPublisher, + ) { + this.serviceContainer.register(this.getId(), this) + } + + async handleRequest(request: never, response: never, endpointOrMethodIdentifier: string): Promise { + const method = this.controllerContainer.get(endpointOrMethodIdentifier) + + if (!method) { + throw new Error(`Method ${endpointOrMethodIdentifier} not found`) + } + + return method(request, response) + } + + async getContainer(): Promise { + const config = new ContainerConfigLoader() + + return config.load({ + controllerConatiner: this.controllerContainer, + directCallDomainEventPublisher: this.directCallDomainEventPublisher, + }) + } + + getId(): ServiceIdentifier { + return ServiceIdentifier.create(ServiceIdentifier.NAMES.Revisions).getValue() + } +} diff --git a/packages/revisions/src/Bootstrap/Types.ts b/packages/revisions/src/Bootstrap/Types.ts index 73b5ac5a8..6669d157c 100644 --- a/packages/revisions/src/Bootstrap/Types.ts +++ b/packages/revisions/src/Bootstrap/Types.ts @@ -1,46 +1,49 @@ const TYPES = { - DBConnection: Symbol.for('DBConnection'), - Logger: Symbol.for('Logger'), - SQS: Symbol.for('SQS'), - S3: Symbol.for('S3'), - Env: Symbol.for('Env'), + Revisions_DBConnection: Symbol.for('Revisions_DBConnection'), + Revisions_Logger: Symbol.for('Revisions_Logger'), + Revisions_SQS: Symbol.for('Revisions_SQS'), + Revisions_S3: Symbol.for('Revisions_S3'), + Revisions_Env: Symbol.for('Revisions_Env'), // Map - RevisionMetadataPersistenceMapper: Symbol.for('RevisionMetadataPersistenceMapper'), - RevisionPersistenceMapper: Symbol.for('RevisionPersistenceMapper'), - RevisionItemStringMapper: Symbol.for('RevisionItemStringMapper'), - RevisionHttpMapper: Symbol.for('RevisionHttpMapper'), - RevisionMetadataHttpMapper: Symbol.for('RevisionMetadataHttpMapper'), + Revisions_RevisionMetadataPersistenceMapper: Symbol.for('Revisions_RevisionMetadataPersistenceMapper'), + Revisions_RevisionPersistenceMapper: Symbol.for('Revisions_RevisionPersistenceMapper'), + Revisions_RevisionItemStringMapper: Symbol.for('Revisions_RevisionItemStringMapper'), + Revisions_RevisionHttpMapper: Symbol.for('Revisions_RevisionHttpMapper'), + Revisions_RevisionMetadataHttpMapper: Symbol.for('Revisions_RevisionMetadataHttpMapper'), // ORM - ORMRevisionRepository: Symbol.for('ORMRevisionRepository'), + Revisions_ORMRevisionRepository: Symbol.for('Revisions_ORMRevisionRepository'), // Repositories - RevisionRepository: Symbol.for('RevisionRepository'), - DumpRepository: Symbol.for('DumpRepository'), + Revisions_RevisionRepository: Symbol.for('Revisions_RevisionRepository'), + Revisions_DumpRepository: Symbol.for('Revisions_DumpRepository'), // env vars - AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'), - SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'), - SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'), - S3_AWS_REGION: Symbol.for('S3_AWS_REGION'), - S3_BACKUP_BUCKET_NAME: Symbol.for('S3_BACKUP_BUCKET_NAME'), - NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'), - VERSION: Symbol.for('VERSION'), + Revisions_AUTH_JWT_SECRET: Symbol.for('Revisions_AUTH_JWT_SECRET'), + Revisions_SQS_QUEUE_URL: Symbol.for('Revisions_SQS_QUEUE_URL'), + Revisions_SQS_AWS_REGION: Symbol.for('Revisions_SQS_AWS_REGION'), + Revisions_S3_AWS_REGION: Symbol.for('Revisions_S3_AWS_REGION'), + Revisions_S3_BACKUP_BUCKET_NAME: Symbol.for('Revisions_S3_BACKUP_BUCKET_NAME'), + Revisions_NEW_RELIC_ENABLED: Symbol.for('Revisions_NEW_RELIC_ENABLED'), + Revisions_VERSION: Symbol.for('Revisions_VERSION'), // use cases - GetRevisionsMetada: Symbol.for('GetRevisionsMetada'), - GetRevision: Symbol.for('GetRevision'), - DeleteRevision: Symbol.for('DeleteRevision'), - CopyRevisions: Symbol.for('CopyRevisions'), - GetRequiredRoleToViewRevision: Symbol.for('GetRequiredRoleToViewRevision'), + Revisions_GetRevisionsMetada: Symbol.for('Revisions_GetRevisionsMetada'), + Revisions_GetRevision: Symbol.for('Revisions_GetRevision'), + Revisions_DeleteRevision: Symbol.for('Revisions_DeleteRevision'), + Revisions_CopyRevisions: Symbol.for('Revisions_CopyRevisions'), + Revisions_GetRequiredRoleToViewRevision: Symbol.for('Revisions_GetRequiredRoleToViewRevision'), // Controller - RevisionsController: Symbol.for('RevisionsController'), - ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'), + Revisions_ControllerContainer: Symbol.for('Revisions_ControllerContainer'), + Revisions_RevisionsController: Symbol.for('Revisions_RevisionsController'), + Revisions_ApiGatewayAuthMiddleware: Symbol.for('Revisions_ApiGatewayAuthMiddleware'), // Handlers - ItemDumpedEventHandler: Symbol.for('ItemDumpedEventHandler'), - AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'), - RevisionsCopyRequestedEventHandler: Symbol.for('RevisionsCopyRequestedEventHandler'), + Revisions_ItemDumpedEventHandler: Symbol.for('Revisions_ItemDumpedEventHandler'), + Revisions_AccountDeletionRequestedEventHandler: Symbol.for('Revisions_AccountDeletionRequestedEventHandler'), + Revisions_RevisionsCopyRequestedEventHandler: Symbol.for('Revisions_RevisionsCopyRequestedEventHandler'), // Services - CrossServiceTokenDecoder: Symbol.for('CrossServiceTokenDecoder'), - DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'), - DomainEventMessageHandler: Symbol.for('DomainEventMessageHandler'), - Timer: Symbol.for('Timer'), + Revisions_CrossServiceTokenDecoder: Symbol.for('Revisions_CrossServiceTokenDecoder'), + Revisions_DomainEventSubscriberFactory: Symbol.for('Revisions_DomainEventSubscriberFactory'), + Revisions_DomainEventMessageHandler: Symbol.for('Revisions_DomainEventMessageHandler'), + Revisions_Timer: Symbol.for('Revisions_Timer'), + // Inversify Express Controllers + Revisions_InversifyExpressRevisionsController: Symbol.for('Revisions_InversifyExpressRevisionsController'), } export default TYPES diff --git a/packages/revisions/src/Bootstrap/WorkerContainerConfigLoader.ts b/packages/revisions/src/Bootstrap/WorkerContainerConfigLoader.ts deleted file mode 100644 index ff15cfa58..000000000 --- a/packages/revisions/src/Bootstrap/WorkerContainerConfigLoader.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs' -import { S3Client } from '@aws-sdk/client-s3' -import { Container, interfaces } from 'inversify' -import { - DomainEventHandlerInterface, - DomainEventMessageHandlerInterface, - DomainEventSubscriberFactoryInterface, -} from '@standardnotes/domain-events' -import { - SQSDomainEventSubscriberFactory, - SQSEventMessageHandler, - SQSNewRelicEventMessageHandler, -} from '@standardnotes/domain-events-infra' -import { MapperInterface } from '@standardnotes/domain-core' - -import TYPES from './Types' -import { Revision } from '../Domain/Revision/Revision' -import { RevisionItemStringMapper } from '../Mapping/RevisionItemStringMapper' -import { ItemDumpedEventHandler } from '../Domain/Handler/ItemDumpedEventHandler' -import { DumpRepositoryInterface } from '../Domain/Dump/DumpRepositoryInterface' -import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository' -import { FSDumpRepository } from '../Infra/FS/FSDumpRepository' -import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler' -import { RevisionsCopyRequestedEventHandler } from '../Domain/Handler/RevisionsCopyRequestedEventHandler' -import { CopyRevisions } from '../Domain/UseCase/CopyRevisions/CopyRevisions' -import { CommonContainerConfigLoader } from './CommonContainerConfigLoader' -import { Env } from './Env' - -export class WorkerContainerConfigLoader extends CommonContainerConfigLoader { - override async load(): Promise { - const container = await super.load() - - const env: Env = container.get(TYPES.Env) - - container.bind(TYPES.SQS).toDynamicValue((context: interfaces.Context) => { - const env: Env = context.container.get(TYPES.Env) - - const sqsConfig: SQSClientConfig = { - region: env.get('SQS_AWS_REGION'), - } - if (env.get('SQS_ENDPOINT', true)) { - sqsConfig.endpoint = env.get('SQS_ENDPOINT', true) - } - if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) { - sqsConfig.credentials = { - accessKeyId: env.get('SQS_ACCESS_KEY_ID', true), - secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true), - } - } - - return new SQSClient(sqsConfig) - }) - - container.bind(TYPES.S3).toDynamicValue((context: interfaces.Context) => { - const env: Env = context.container.get(TYPES.Env) - - let s3Client = undefined - if (env.get('S3_AWS_REGION', true)) { - s3Client = new S3Client({ - apiVersion: 'latest', - region: env.get('S3_AWS_REGION', true), - }) - } - - return s3Client - }) - - // Map - container - .bind>(TYPES.RevisionItemStringMapper) - .toDynamicValue(() => new RevisionItemStringMapper()) - - // env vars - container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL')) - container.bind(TYPES.S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true)) - container.bind(TYPES.S3_BACKUP_BUCKET_NAME).toConstantValue(env.get('S3_BACKUP_BUCKET_NAME', true)) - - container.bind(TYPES.DumpRepository).toDynamicValue((context: interfaces.Context) => { - const env: Env = context.container.get(TYPES.Env) - - if (env.get('S3_AWS_REGION', true)) { - return new S3DumpRepository( - context.container.get(TYPES.S3_BACKUP_BUCKET_NAME), - context.container.get(TYPES.S3), - context.container.get(TYPES.RevisionItemStringMapper), - context.container.get(TYPES.Logger), - ) - } else { - return new FSDumpRepository(context.container.get(TYPES.RevisionItemStringMapper)) - } - }) - - // use cases - container.bind(TYPES.CopyRevisions).toDynamicValue((context: interfaces.Context) => { - return new CopyRevisions(context.container.get(TYPES.RevisionRepository)) - }) - - // Handlers - container - .bind(TYPES.ItemDumpedEventHandler) - .toDynamicValue((context: interfaces.Context) => { - return new ItemDumpedEventHandler( - context.container.get(TYPES.DumpRepository), - context.container.get(TYPES.RevisionRepository), - ) - }) - container - .bind(TYPES.AccountDeletionRequestedEventHandler) - .toDynamicValue((context: interfaces.Context) => { - return new AccountDeletionRequestedEventHandler( - context.container.get(TYPES.RevisionRepository), - context.container.get(TYPES.Logger), - ) - }) - container - .bind(TYPES.RevisionsCopyRequestedEventHandler) - .toDynamicValue((context: interfaces.Context) => { - return new RevisionsCopyRequestedEventHandler( - context.container.get(TYPES.CopyRevisions), - context.container.get(TYPES.Logger), - ) - }) - - container - .bind(TYPES.DomainEventMessageHandler) - .toDynamicValue((context: interfaces.Context) => { - const env: Env = context.container.get(TYPES.Env) - - const eventHandlers: Map = new Map([ - ['ITEM_DUMPED', context.container.get(TYPES.ItemDumpedEventHandler)], - ['ACCOUNT_DELETION_REQUESTED', context.container.get(TYPES.AccountDeletionRequestedEventHandler)], - ['REVISIONS_COPY_REQUESTED', context.container.get(TYPES.RevisionsCopyRequestedEventHandler)], - ]) - - const handler = - env.get('NEW_RELIC_ENABLED', true) === 'true' - ? new SQSNewRelicEventMessageHandler(eventHandlers, context.container.get(TYPES.Logger)) - : new SQSEventMessageHandler(eventHandlers, context.container.get(TYPES.Logger)) - - return handler - }) - - container - .bind(TYPES.DomainEventSubscriberFactory) - .toDynamicValue((context: interfaces.Context) => { - return new SQSDomainEventSubscriberFactory( - context.container.get(TYPES.SQS), - context.container.get(TYPES.SQS_QUEUE_URL), - context.container.get(TYPES.DomainEventMessageHandler), - ) - }) - - return container - } -} diff --git a/packages/revisions/src/Bootstrap/index.ts b/packages/revisions/src/Bootstrap/index.ts new file mode 100644 index 000000000..22cddecef --- /dev/null +++ b/packages/revisions/src/Bootstrap/index.ts @@ -0,0 +1 @@ +export * from './Service' diff --git a/packages/revisions/src/Infra/InversifyExpress/InversifyExpressRevisionsController.ts b/packages/revisions/src/Infra/InversifyExpress/InversifyExpressRevisionsController.ts index 52091a16e..4370ed993 100644 --- a/packages/revisions/src/Infra/InversifyExpress/InversifyExpressRevisionsController.ts +++ b/packages/revisions/src/Infra/InversifyExpress/InversifyExpressRevisionsController.ts @@ -1,14 +1,22 @@ import { Request, Response } from 'express' import { BaseHttpController, controller, httpDelete, httpGet, results } from 'inversify-express-utils' import { inject } from 'inversify' +import { ControllerContainerInterface } from '@standardnotes/domain-core' import TYPES from '../../Bootstrap/Types' import { RevisionsController } from '../../Controller/RevisionsController' -@controller('/items/:itemUuid/revisions', TYPES.ApiGatewayAuthMiddleware) +@controller('/items/:itemUuid/revisions', TYPES.Revisions_ApiGatewayAuthMiddleware) export class InversifyExpressRevisionsController extends BaseHttpController { - constructor(@inject(TYPES.RevisionsController) private revisionsController: RevisionsController) { + constructor( + @inject(TYPES.Revisions_RevisionsController) private revisionsController: RevisionsController, + @inject(TYPES.Revisions_ControllerContainer) private controllerContainer: ControllerContainerInterface, + ) { super() + + this.controllerContainer.register('revisions.revisions.getRevisions', this.getRevisions.bind(this)) + this.controllerContainer.register('revisions.revisions.getRevision', this.getRevision.bind(this)) + this.controllerContainer.register('revisions.revisions.deleteRevision', this.deleteRevision.bind(this)) } @httpGet('/') diff --git a/packages/revisions/src/Controller/ApiGatewayAuthMiddleware.ts b/packages/revisions/src/Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware.ts similarity index 100% rename from packages/revisions/src/Controller/ApiGatewayAuthMiddleware.ts rename to packages/revisions/src/Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware.ts diff --git a/packages/revisions/src/Mapping/RevisionMetadataPersistenceMapper.ts b/packages/revisions/src/Mapping/RevisionMetadataPersistenceMapper.ts index 840144a0c..76c8765fd 100644 --- a/packages/revisions/src/Mapping/RevisionMetadataPersistenceMapper.ts +++ b/packages/revisions/src/Mapping/RevisionMetadataPersistenceMapper.ts @@ -12,7 +12,10 @@ export class RevisionMetadataPersistenceMapper implements MapperInterface