diff --git a/.pnp.cjs b/.pnp.cjs index b518e2168..10e4381ac 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -2521,6 +2521,20 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@standardnotes/api", [\ + ["npm:1.11.0", {\ + "packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.11.0-ce72fb3e14-f1134efb44.zip/node_modules/@standardnotes/api/",\ + "packageDependencies": [\ + ["@standardnotes/api", "npm:1.11.0"],\ + ["@standardnotes/common", "workspace:packages/common"],\ + ["@standardnotes/encryption", "npm:1.16.0"],\ + ["@standardnotes/models", "npm:1.24.0"],\ + ["@standardnotes/responses", "npm:1.10.4"],\ + ["@standardnotes/security", "workspace:packages/security"],\ + ["@standardnotes/utils", "npm:1.9.0"],\ + ["reflect-metadata", "npm:0.1.13"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:1.9.0", {\ "packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.9.0-507434ff00-cc3feac393.zip/node_modules/@standardnotes/api/",\ "packageDependencies": [\ @@ -2737,6 +2751,19 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["reflect-metadata", "npm:0.1.13"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.16.0", {\ + "packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.16.0-df46ea19bc-9971b9afcc.zip/node_modules/@standardnotes/encryption/",\ + "packageDependencies": [\ + ["@standardnotes/encryption", "npm:1.16.0"],\ + ["@standardnotes/common", "workspace:packages/common"],\ + ["@standardnotes/models", "npm:1.24.0"],\ + ["@standardnotes/responses", "npm:1.10.4"],\ + ["@standardnotes/sncrypto-common", "npm:1.13.0"],\ + ["@standardnotes/utils", "npm:1.9.0"],\ + ["reflect-metadata", "npm:0.1.13"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@standardnotes/event-store", [\ @@ -2790,6 +2817,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["reflect-metadata", "npm:0.1.13"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.52.2", {\ + "packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.52.2-076ab9f511-ab345f8dc1.zip/node_modules/@standardnotes/features/",\ + "packageDependencies": [\ + ["@standardnotes/features", "npm:1.52.2"],\ + ["@standardnotes/auth", "npm:3.19.4"],\ + ["@standardnotes/common", "workspace:packages/common"],\ + ["@standardnotes/security", "workspace:packages/security"],\ + ["reflect-metadata", "npm:0.1.13"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@standardnotes/files-server", [\ @@ -2857,6 +2895,19 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["reflect-metadata", "npm:0.1.13"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.24.0", {\ + "packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.24.0-bf039594ac-2acbbbc062.zip/node_modules/@standardnotes/models/",\ + "packageDependencies": [\ + ["@standardnotes/models", "npm:1.24.0"],\ + ["@standardnotes/common", "workspace:packages/common"],\ + ["@standardnotes/features", "npm:1.52.2"],\ + ["@standardnotes/responses", "npm:1.10.4"],\ + ["@standardnotes/utils", "npm:1.9.0"],\ + ["lodash", "npm:4.17.21"],\ + ["reflect-metadata", "npm:0.1.13"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@standardnotes/payloads", [\ @@ -2899,6 +2950,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ + ["npm:1.10.4", {\ + "packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.10.4-3af0d3ab54-41e4971144.zip/node_modules/@standardnotes/responses/",\ + "packageDependencies": [\ + ["@standardnotes/responses", "npm:1.10.4"],\ + ["@standardnotes/common", "workspace:packages/common"],\ + ["@standardnotes/features", "npm:1.52.2"],\ + ["@standardnotes/security", "workspace:packages/security"],\ + ["reflect-metadata", "npm:0.1.13"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:1.6.39", {\ "packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.6.39-395f4c2d65-0ea1d4d5b8.zip/node_modules/@standardnotes/responses/",\ "packageDependencies": [\ @@ -3012,6 +3074,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ + ["npm:1.13.0", {\ + "packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.0-18cb5f8eb9-e58258f525.zip/node_modules/@standardnotes/sncrypto-common/",\ + "packageDependencies": [\ + ["@standardnotes/sncrypto-common", "npm:1.13.0"],\ + ["reflect-metadata", "npm:0.1.13"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:1.9.0", {\ "packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.9.0-48773f745a-42252d7198.zip/node_modules/@standardnotes/sncrypto-common/",\ "packageDependencies": [\ @@ -3143,6 +3213,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@standardnotes/workspace-server", "workspace:packages/workspace"],\ ["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\ ["@sentry/node", "npm:7.5.0"],\ + ["@standardnotes/api", "npm:1.11.0"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\ diff --git a/.yarn/cache/@standardnotes-api-npm-1.11.0-ce72fb3e14-f1134efb44.zip b/.yarn/cache/@standardnotes-api-npm-1.11.0-ce72fb3e14-f1134efb44.zip new file mode 100644 index 000000000..ed8175e36 Binary files /dev/null and b/.yarn/cache/@standardnotes-api-npm-1.11.0-ce72fb3e14-f1134efb44.zip differ diff --git a/.yarn/cache/@standardnotes-encryption-npm-1.16.0-df46ea19bc-9971b9afcc.zip b/.yarn/cache/@standardnotes-encryption-npm-1.16.0-df46ea19bc-9971b9afcc.zip new file mode 100644 index 000000000..2bd02a0b4 Binary files /dev/null and b/.yarn/cache/@standardnotes-encryption-npm-1.16.0-df46ea19bc-9971b9afcc.zip differ diff --git a/.yarn/cache/@standardnotes-features-npm-1.52.2-076ab9f511-ab345f8dc1.zip b/.yarn/cache/@standardnotes-features-npm-1.52.2-076ab9f511-ab345f8dc1.zip new file mode 100644 index 000000000..7329ef156 Binary files /dev/null and b/.yarn/cache/@standardnotes-features-npm-1.52.2-076ab9f511-ab345f8dc1.zip differ diff --git a/.yarn/cache/@standardnotes-models-npm-1.24.0-bf039594ac-2acbbbc062.zip b/.yarn/cache/@standardnotes-models-npm-1.24.0-bf039594ac-2acbbbc062.zip new file mode 100644 index 000000000..c2cc3533f Binary files /dev/null and b/.yarn/cache/@standardnotes-models-npm-1.24.0-bf039594ac-2acbbbc062.zip differ diff --git a/.yarn/cache/@standardnotes-responses-npm-1.10.4-3af0d3ab54-41e4971144.zip b/.yarn/cache/@standardnotes-responses-npm-1.10.4-3af0d3ab54-41e4971144.zip new file mode 100644 index 000000000..89ad6bc21 Binary files /dev/null and b/.yarn/cache/@standardnotes-responses-npm-1.10.4-3af0d3ab54-41e4971144.zip differ diff --git a/.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.0-18cb5f8eb9-e58258f525.zip b/.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.0-18cb5f8eb9-e58258f525.zip new file mode 100644 index 000000000..23214414f Binary files /dev/null and b/.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.0-18cb5f8eb9-e58258f525.zip differ diff --git a/packages/api-gateway/.env.sample b/packages/api-gateway/.env.sample index 0bf49cbfe..5ec94e82d 100644 --- a/packages/api-gateway/.env.sample +++ b/packages/api-gateway/.env.sample @@ -6,6 +6,7 @@ PORT=3000 SYNCING_SERVER_JS_URL=http://syncing_server_js:3000 AUTH_SERVER_URL=http://auth:3000 +WORKSPACE_SERVER_URL=http://workspace:3000 PAYMENTS_SERVER_URL=http://payments:3000 FILES_SERVER_URL=http://files:3000 diff --git a/packages/api-gateway/bin/server.ts b/packages/api-gateway/bin/server.ts index 48dc3bd58..dc87004cf 100644 --- a/packages/api-gateway/bin/server.ts +++ b/packages/api-gateway/bin/server.ts @@ -19,6 +19,7 @@ import '../src/Controller/v1/TokensController' import '../src/Controller/v1/OfflineController' import '../src/Controller/v1/FilesController' import '../src/Controller/v1/SubscriptionInvitesController' +import '../src/Controller/v1/WorkspacesController' import '../src/Controller/v2/PaymentsControllerV2' import '../src/Controller/v2/ActionsControllerV2' diff --git a/packages/api-gateway/src/Bootstrap/Container.ts b/packages/api-gateway/src/Bootstrap/Container.ts index 07660dbf9..4e46f7885 100644 --- a/packages/api-gateway/src/Bootstrap/Container.ts +++ b/packages/api-gateway/src/Bootstrap/Container.ts @@ -75,6 +75,7 @@ export class ContainerConfigLoader { container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true)) container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true)) container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET')) + container.bind(TYPES.WORKSPACE_SERVER_URL).toConstantValue(env.get('WORKSPACE_SERVER_URL')) container .bind(TYPES.HTTP_CALL_TIMEOUT) .toConstantValue(env.get('HTTP_CALL_TIMEOUT', true) ? +env.get('HTTP_CALL_TIMEOUT', true) : 60_000) diff --git a/packages/api-gateway/src/Bootstrap/Types.ts b/packages/api-gateway/src/Bootstrap/Types.ts index 29ee0c638..0d4d4a7cd 100644 --- a/packages/api-gateway/src/Bootstrap/Types.ts +++ b/packages/api-gateway/src/Bootstrap/Types.ts @@ -8,6 +8,7 @@ const TYPES = { AUTH_SERVER_URL: Symbol.for('AUTH_SERVER_URL'), PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'), FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'), + WORKSPACE_SERVER_URL: Symbol.for('WORKSPACE_SERVER_URL'), AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'), HTTP_CALL_TIMEOUT: Symbol.for('HTTP_CALL_TIMEOUT'), VERSION: Symbol.for('VERSION'), diff --git a/packages/api-gateway/src/Controller/v1/WorkspacesController.ts b/packages/api-gateway/src/Controller/v1/WorkspacesController.ts new file mode 100644 index 000000000..c01c22b59 --- /dev/null +++ b/packages/api-gateway/src/Controller/v1/WorkspacesController.ts @@ -0,0 +1,18 @@ +import { inject } from 'inversify' +import { Request, Response } from 'express' +import { controller, BaseHttpController, httpPost } from 'inversify-express-utils' + +import TYPES from '../../Bootstrap/Types' +import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface' + +@controller('/v1/workspaces') +export class WorkspacesController extends BaseHttpController { + constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) { + super() + } + + @httpPost('/', TYPES.AuthMiddleware) + async create(request: Request, response: Response): Promise { + await this.httpService.callWorkspaceServer(request, response, 'workspaces', request.body) + } +} diff --git a/packages/api-gateway/src/Service/Http/HttpService.ts b/packages/api-gateway/src/Service/Http/HttpService.ts index 378a147d9..7da503a89 100644 --- a/packages/api-gateway/src/Service/Http/HttpService.ts +++ b/packages/api-gateway/src/Service/Http/HttpService.ts @@ -16,6 +16,7 @@ export class HttpService implements HttpServiceInterface { @inject(TYPES.SYNCING_SERVER_JS_URL) private syncingServerJsUrl: string, @inject(TYPES.PAYMENTS_SERVER_URL) private paymentsServerUrl: string, @inject(TYPES.FILES_SERVER_URL) private filesServerUrl: string, + @inject(TYPES.WORKSPACE_SERVER_URL) private workspaceServerUrl: string, @inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number, @inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface, @inject(TYPES.Logger) private logger: Logger, @@ -48,6 +49,15 @@ export class HttpService implements HttpServiceInterface { await this.callServer(this.authServerUrl, request, response, endpoint, payload) } + async callWorkspaceServer( + request: Request, + response: Response, + endpoint: string, + payload?: Record | string, + ): Promise { + await this.callServer(this.workspaceServerUrl, request, response, endpoint, payload) + } + async callPaymentsServer( request: Request, response: Response, diff --git a/packages/api-gateway/src/Service/Http/HttpServiceInterface.ts b/packages/api-gateway/src/Service/Http/HttpServiceInterface.ts index cb2360b93..75c0e8c87 100644 --- a/packages/api-gateway/src/Service/Http/HttpServiceInterface.ts +++ b/packages/api-gateway/src/Service/Http/HttpServiceInterface.ts @@ -31,4 +31,10 @@ export interface HttpServiceInterface { endpoint: string, payload?: Record | string, ): Promise + callWorkspaceServer( + request: Request, + response: Response, + endpoint: string, + payload?: Record | string, + ): Promise } diff --git a/packages/workspace/bin/server.ts b/packages/workspace/bin/server.ts index cb905c816..91b421471 100644 --- a/packages/workspace/bin/server.ts +++ b/packages/workspace/bin/server.ts @@ -5,6 +5,7 @@ import 'newrelic' import * as Sentry from '@sentry/node' import '../src/Infra/InversifyExpressUtils/InversifyExpressHealthCheckController' +import '../src/Infra/InversifyExpressUtils/InversifyExpressWorkspacesController' import * as cors from 'cors' import { urlencoded, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express' diff --git a/packages/workspace/package.json b/packages/workspace/package.json index cecf63d2e..78d5d06e8 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -25,6 +25,7 @@ "dependencies": { "@newrelic/winston-enricher": "^4.0.0", "@sentry/node": "^7.3.0", + "@standardnotes/api": "^1.11.0", "@standardnotes/common": "workspace:*", "@standardnotes/domain-events": "workspace:*", "@standardnotes/domain-events-infra": "workspace:*", diff --git a/packages/workspace/src/Bootstrap/Container.ts b/packages/workspace/src/Bootstrap/Container.ts index 55cdd8b26..0e6b1efdc 100644 --- a/packages/workspace/src/Bootstrap/Container.ts +++ b/packages/workspace/src/Bootstrap/Container.ts @@ -21,6 +21,15 @@ import { } from '@standardnotes/domain-events-infra' import { ApiGatewayAuthMiddleware } from '../Controller/ApiGatewayAuthMiddleware' import { CrossServiceTokenData, TokenDecoder, TokenDecoderInterface } from '@standardnotes/security' +import { WorkspaceRepositoryInterface } from '../Domain/Workspace/WorkspaceRepositoryInterface' +import { MySQLWorkspaceRepository } from '../Infra/MySQL/MySQLWorkspaceRepository' +import { WorkspaceUserRepositoryInterface } from '../Domain/Workspace/WorkspaceUserRepositoryInterface' +import { MySQLWorkspaceUserRepository } from '../Infra/MySQL/MySQLWorkspaceUserRepository' +import { Repository } from 'typeorm' +import { Workspace } from '../Domain/Workspace/Workspace' +import { WorkspaceUser } from '../Domain/Workspace/WorkspaceUser' +import { CreateWorkspace } from '../Domain/UseCase/CreateWorkspace/CreateWorkspace' +import { WorkspacesController } from '../Controller/WorkspacesController' // eslint-disable-next-line @typescript-eslint/no-var-requires const newrelicFormatter = require('@newrelic/winston-enricher') @@ -82,8 +91,17 @@ export class ContainerConfigLoader { } // Controller + container.bind(TYPES.WorkspacesController).to(WorkspacesController) // Repositories + container.bind(TYPES.WorkspaceRepository).to(MySQLWorkspaceRepository) + container.bind(TYPES.WorkspaceUserRepository).to(MySQLWorkspaceUserRepository) // ORM + container + .bind>(TYPES.ORMWorkspaceRepository) + .toConstantValue(AppDataSource.getRepository(Workspace)) + container + .bind>(TYPES.ORMWorkspaceUserRepository) + .toConstantValue(AppDataSource.getRepository(WorkspaceUser)) // Middleware container.bind(TYPES.ApiGatewayAuthMiddleware).to(ApiGatewayAuthMiddleware) // env vars @@ -97,6 +115,7 @@ export class ContainerConfigLoader { container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION')) // use cases + container.bind(TYPES.CreateWorkspace).to(CreateWorkspace) // Handlers // Services container diff --git a/packages/workspace/src/Bootstrap/Types.ts b/packages/workspace/src/Bootstrap/Types.ts index 0c82dbe3e..54ccf5f30 100644 --- a/packages/workspace/src/Bootstrap/Types.ts +++ b/packages/workspace/src/Bootstrap/Types.ts @@ -4,8 +4,13 @@ const TYPES = { SNS: Symbol.for('SNS'), SQS: Symbol.for('SQS'), // Controller + WorkspacesController: Symbol.for('WorkspacesController'), // Repositories + WorkspaceRepository: Symbol.for('WorkspaceRepository'), + WorkspaceUserRepository: Symbol.for('WorkspaceUserRepository'), // ORM + ORMWorkspaceRepository: Symbol.for('ORMWorkspaceRepository'), + ORMWorkspaceUserRepository: Symbol.for('ORMWorkspaceUserRepository'), // Middleware ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'), // env vars @@ -19,6 +24,7 @@ const TYPES = { NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'), VERSION: Symbol.for('VERSION'), // use cases + CreateWorkspace: Symbol.for('CreateWorkspace'), // Handlers // Services CrossServiceTokenDecoder: Symbol.for('CrossServiceTokenDecoder'), diff --git a/packages/workspace/src/Controller/WorkspacesController.spec.ts b/packages/workspace/src/Controller/WorkspacesController.spec.ts new file mode 100644 index 000000000..0acaf7075 --- /dev/null +++ b/packages/workspace/src/Controller/WorkspacesController.spec.ts @@ -0,0 +1,33 @@ +import 'reflect-metadata' + +import { CreateWorkspace } from '../Domain/UseCase/CreateWorkspace/CreateWorkspace' + +import { WorkspacesController } from './WorkspacesController' + +describe('WorkspacesController', () => { + let doCreateWorkspace: CreateWorkspace + + const createController = () => new WorkspacesController(doCreateWorkspace) + + beforeEach(() => { + doCreateWorkspace = {} as jest.Mocked + doCreateWorkspace.execute = jest.fn().mockReturnValue({ workspace: { uuid: 'w-1-2-3' } }) + }) + + it('should create a workspace', async () => { + const result = await createController().createWorkspace({ + encryptedPrivateKey: 'foo', + encryptedWorkspaceKey: 'bar', + publicKey: 'buzz', + workspaceName: 'A Team', + ownerUuid: 'u-1-2-3', + }) + + expect(result).toEqual({ + data: { + uuid: 'w-1-2-3', + }, + status: 200, + }) + }) +}) diff --git a/packages/workspace/src/Controller/WorkspacesController.ts b/packages/workspace/src/Controller/WorkspacesController.ts new file mode 100644 index 000000000..e7d589f6c --- /dev/null +++ b/packages/workspace/src/Controller/WorkspacesController.ts @@ -0,0 +1,32 @@ +import { inject, injectable } from 'inversify' +import { + HttpStatusCode, + WorkspaceCreationRequestParams, + WorkspaceCreationResponse, + WorkspaceServerInterface, +} from '@standardnotes/api' + +import TYPES from '../Bootstrap/Types' +import { CreateWorkspace } from '../Domain/UseCase/CreateWorkspace/CreateWorkspace' +import { WorkspaceType } from '../Domain/Workspace/WorkspaceType' + +@injectable() +export class WorkspacesController implements WorkspaceServerInterface { + constructor(@inject(TYPES.CreateWorkspace) private doCreateWorkspace: CreateWorkspace) {} + + async createWorkspace(params: WorkspaceCreationRequestParams): Promise { + const { workspace } = await this.doCreateWorkspace.execute({ + encryptedPrivateKey: params.encryptedPrivateKey, + encryptedWorkspaceKey: params.encryptedWorkspaceKey, + publicKey: params.publicKey, + name: params.workspaceName, + type: WorkspaceType.Root, + ownerUuid: params.ownerUuid as string, + }) + + return { + status: HttpStatusCode.Success, + data: { uuid: workspace.uuid }, + } + } +} diff --git a/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspace.spec.ts b/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspace.spec.ts new file mode 100644 index 000000000..2719c314e --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspace.spec.ts @@ -0,0 +1,74 @@ +import 'reflect-metadata' +import { WorkspaceRepositoryInterface } from '../../Workspace/WorkspaceRepositoryInterface' +import { WorkspaceType } from '../../Workspace/WorkspaceType' +import { WorkspaceUserRepositoryInterface } from '../../Workspace/WorkspaceUserRepositoryInterface' + +import { CreateWorkspace } from './CreateWorkspace' + +describe('CreateWorkspace', () => { + let workspaceRepository: WorkspaceRepositoryInterface + let workspaceUserRepository: WorkspaceUserRepositoryInterface + + const createUseCase = () => new CreateWorkspace(workspaceRepository, workspaceUserRepository) + + beforeEach(() => { + workspaceRepository = {} as jest.Mocked + workspaceRepository.save = jest.fn().mockImplementation((workspace) => { + return { + ...workspace, + uuid: 'w-1-2-3', + } + }) + + workspaceUserRepository = {} as jest.Mocked + workspaceUserRepository.save = jest.fn() + }) + + it('should create a workspace and owner association with it', async () => { + await createUseCase().execute({ + encryptedPrivateKey: 'foo', + encryptedWorkspaceKey: 'bar', + publicKey: 'buzz', + name: 'A Team', + ownerUuid: '1-2-3', + type: WorkspaceType.Root, + }) + + expect(workspaceRepository.save).toHaveBeenCalledWith({ + name: 'A Team', + type: 'root', + }) + expect(workspaceUserRepository.save).toHaveBeenCalledWith({ + accessLevel: 'owner', + encryptedWorkspaceKey: 'bar', + privateKey: 'foo', + publicKey: 'buzz', + status: 'active', + userUuid: '1-2-3', + workspaceUuid: 'w-1-2-3', + }) + }) + + it('should create a workspace without a name and owner association with it', async () => { + await createUseCase().execute({ + encryptedPrivateKey: 'foo', + encryptedWorkspaceKey: 'bar', + publicKey: 'buzz', + ownerUuid: '1-2-3', + type: WorkspaceType.Private, + }) + + expect(workspaceRepository.save).toHaveBeenCalledWith({ + type: 'private', + }) + expect(workspaceUserRepository.save).toHaveBeenCalledWith({ + accessLevel: 'owner', + encryptedWorkspaceKey: 'bar', + privateKey: 'foo', + publicKey: 'buzz', + status: 'active', + userUuid: '1-2-3', + workspaceUuid: 'w-1-2-3', + }) + }) +}) diff --git a/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspace.ts b/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspace.ts new file mode 100644 index 000000000..a9f5176bf --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspace.ts @@ -0,0 +1,44 @@ +import { inject, injectable } from 'inversify' + +import TYPES from '../../../Bootstrap/Types' +import { Workspace } from '../../Workspace/Workspace' +import { WorkspaceAccessLevel } from '../../Workspace/WorkspaceAccessLevel' +import { WorkspaceRepositoryInterface } from '../../Workspace/WorkspaceRepositoryInterface' +import { WorkspaceUser } from '../../Workspace/WorkspaceUser' +import { WorkspaceUserRepositoryInterface } from '../../Workspace/WorkspaceUserRepositoryInterface' +import { WorkspaceUserStatus } from '../../Workspace/WorkspaceUserStatus' +import { UseCaseInterface } from '../UseCaseInterface' + +import { CreateWorkspaceDTO } from './CreateWorkspaceDTO' +import { CreateWorkspaceResponse } from './CreateWorkspaceResponse' + +@injectable() +export class CreateWorkspace implements UseCaseInterface { + constructor( + @inject(TYPES.WorkspaceRepository) private workspaceRepository: WorkspaceRepositoryInterface, + @inject(TYPES.WorkspaceUserRepository) private workspaceUserRepository: WorkspaceUserRepositoryInterface, + ) {} + + async execute(dto: CreateWorkspaceDTO): Promise { + let workspace = new Workspace() + if (dto.name !== undefined) { + workspace.name = dto.name + } + workspace.type = dto.type + + workspace = await this.workspaceRepository.save(workspace) + + const ownerAssociation = new WorkspaceUser() + ownerAssociation.accessLevel = WorkspaceAccessLevel.Owner + ownerAssociation.encryptedWorkspaceKey = dto.encryptedWorkspaceKey + ownerAssociation.privateKey = dto.encryptedPrivateKey + ownerAssociation.publicKey = dto.publicKey + ownerAssociation.status = WorkspaceUserStatus.Active + ownerAssociation.userUuid = dto.ownerUuid + ownerAssociation.workspaceUuid = workspace.uuid + + await this.workspaceUserRepository.save(ownerAssociation) + + return { workspace } + } +} diff --git a/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspaceDTO.ts b/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspaceDTO.ts new file mode 100644 index 000000000..756a2e99d --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspaceDTO.ts @@ -0,0 +1,12 @@ +import { Uuid } from '@standardnotes/common' + +import { WorkspaceType } from '../../Workspace/WorkspaceType' + +export type CreateWorkspaceDTO = { + ownerUuid: Uuid + encryptedWorkspaceKey: string + encryptedPrivateKey: string + publicKey: string + name?: string + type: WorkspaceType +} diff --git a/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspaceResponse.ts b/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspaceResponse.ts new file mode 100644 index 000000000..08f2bc5bd --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/CreateWorkspace/CreateWorkspaceResponse.ts @@ -0,0 +1,5 @@ +import { Workspace } from '../../Workspace/Workspace' + +export type CreateWorkspaceResponse = { + workspace: Workspace +} diff --git a/packages/workspace/src/Domain/UseCase/UseCaseInterface.ts b/packages/workspace/src/Domain/UseCase/UseCaseInterface.ts new file mode 100644 index 000000000..7c8405a9a --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/UseCaseInterface.ts @@ -0,0 +1,3 @@ +export interface UseCaseInterface { + execute(...args: any[]): Promise> +} diff --git a/packages/workspace/src/Domain/Workspace/WorkspaceRepositoryInterface.ts b/packages/workspace/src/Domain/Workspace/WorkspaceRepositoryInterface.ts new file mode 100644 index 000000000..5ded5fee6 --- /dev/null +++ b/packages/workspace/src/Domain/Workspace/WorkspaceRepositoryInterface.ts @@ -0,0 +1,5 @@ +import { Workspace } from './Workspace' + +export interface WorkspaceRepositoryInterface { + save(workspace: Workspace): Promise +} diff --git a/packages/workspace/src/Domain/Workspace/WorkspaceUserRepositoryInterface.ts b/packages/workspace/src/Domain/Workspace/WorkspaceUserRepositoryInterface.ts new file mode 100644 index 000000000..bad097e18 --- /dev/null +++ b/packages/workspace/src/Domain/Workspace/WorkspaceUserRepositoryInterface.ts @@ -0,0 +1,5 @@ +import { WorkspaceUser } from './WorkspaceUser' + +export interface WorkspaceUserRepositoryInterface { + save(workspace: WorkspaceUser): Promise +} diff --git a/packages/workspace/src/Infra/InversifyExpressUtils/InversifyExpressWorkspacesController.ts b/packages/workspace/src/Infra/InversifyExpressUtils/InversifyExpressWorkspacesController.ts new file mode 100644 index 000000000..0a63c9bb1 --- /dev/null +++ b/packages/workspace/src/Infra/InversifyExpressUtils/InversifyExpressWorkspacesController.ts @@ -0,0 +1,22 @@ +import { Request, Response } from 'express' +import { inject } from 'inversify' +import { BaseHttpController, controller, httpPost, results } from 'inversify-express-utils' +import TYPES from '../../Bootstrap/Types' +import { WorkspacesController } from '../../Controller/WorkspacesController' + +@controller('/workspaces') +export class InversifyExpressWorkspacesController extends BaseHttpController { + constructor(@inject(TYPES.WorkspacesController) private workspacesController: WorkspacesController) { + super() + } + + @httpPost('/', TYPES.ApiGatewayAuthMiddleware) + async create(request: Request, response: Response): Promise { + const result = await this.workspacesController.createWorkspace({ + ...request.body, + ownerUuid: response.locals.user.uuid, + }) + + return this.json(result.data, result.status) + } +} diff --git a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.spec.ts b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.spec.ts new file mode 100644 index 000000000..cd5ec4ae2 --- /dev/null +++ b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.spec.ts @@ -0,0 +1,31 @@ +import 'reflect-metadata' + +import { Repository, SelectQueryBuilder } from 'typeorm' + +import { Workspace } from '../../Domain/Workspace/Workspace' + +import { MySQLWorkspaceRepository } from './MySQLWorkspaceRepository' + +describe('MySQLWorkspaceRepository', () => { + let ormRepository: Repository + let workspace: Workspace + let queryBuilder: SelectQueryBuilder + + const createRepository = () => new MySQLWorkspaceRepository(ormRepository) + + beforeEach(() => { + workspace = {} as jest.Mocked + + queryBuilder = {} as jest.Mocked> + + ormRepository = {} as jest.Mocked> + ormRepository.save = jest.fn() + ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder) + }) + + it('should save', async () => { + await createRepository().save(workspace) + + expect(ormRepository.save).toHaveBeenCalledWith(workspace) + }) +}) diff --git a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.ts b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.ts new file mode 100644 index 000000000..17547ce33 --- /dev/null +++ b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.ts @@ -0,0 +1,17 @@ +import { inject, injectable } from 'inversify' +import { Repository } from 'typeorm' +import TYPES from '../../Bootstrap/Types' +import { Workspace } from '../../Domain/Workspace/Workspace' +import { WorkspaceRepositoryInterface } from '../../Domain/Workspace/WorkspaceRepositoryInterface' + +@injectable() +export class MySQLWorkspaceRepository implements WorkspaceRepositoryInterface { + constructor( + @inject(TYPES.ORMWorkspaceRepository) + private ormRepository: Repository, + ) {} + + async save(workspace: Workspace): Promise { + return this.ormRepository.save(workspace) + } +} diff --git a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.spec.ts b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.spec.ts new file mode 100644 index 000000000..44dbcf215 --- /dev/null +++ b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.spec.ts @@ -0,0 +1,30 @@ +import 'reflect-metadata' + +import { Repository, SelectQueryBuilder } from 'typeorm' + +import { WorkspaceUser } from '../../Domain/Workspace/WorkspaceUser' +import { MySQLWorkspaceUserRepository } from './MySQLWorkspaceUserRepository' + +describe('MySQLWorkspaceUserRepository', () => { + let ormRepository: Repository + let workspace: WorkspaceUser + let queryBuilder: SelectQueryBuilder + + const createRepository = () => new MySQLWorkspaceUserRepository(ormRepository) + + beforeEach(() => { + workspace = {} as jest.Mocked + + queryBuilder = {} as jest.Mocked> + + ormRepository = {} as jest.Mocked> + ormRepository.save = jest.fn() + ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder) + }) + + it('should save', async () => { + await createRepository().save(workspace) + + expect(ormRepository.save).toHaveBeenCalledWith(workspace) + }) +}) diff --git a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.ts b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.ts new file mode 100644 index 000000000..1ccfcf4f7 --- /dev/null +++ b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.ts @@ -0,0 +1,18 @@ +import { inject, injectable } from 'inversify' +import { Repository } from 'typeorm' + +import TYPES from '../../Bootstrap/Types' +import { WorkspaceUser } from '../../Domain/Workspace/WorkspaceUser' +import { WorkspaceUserRepositoryInterface } from '../../Domain/Workspace/WorkspaceUserRepositoryInterface' + +@injectable() +export class MySQLWorkspaceUserRepository implements WorkspaceUserRepositoryInterface { + constructor( + @inject(TYPES.ORMWorkspaceUserRepository) + private ormRepository: Repository, + ) {} + + async save(workspaceUser: WorkspaceUser): Promise { + return this.ormRepository.save(workspaceUser) + } +} diff --git a/yarn.lock b/yarn.lock index a6d8f1bcf..8bcbd171a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1824,6 +1824,21 @@ __metadata: languageName: unknown linkType: soft +"@standardnotes/api@npm:^1.11.0": + version: 1.11.0 + resolution: "@standardnotes/api@npm:1.11.0" + dependencies: + "@standardnotes/common": ^1.32.0 + "@standardnotes/encryption": 1.16.0 + "@standardnotes/models": 1.24.0 + "@standardnotes/responses": 1.10.4 + "@standardnotes/security": ^1.1.0 + "@standardnotes/utils": 1.9.0 + reflect-metadata: ^0.1.13 + checksum: f1134efb44a7a2cc8f117bc6a7826d89e11f7780d7ed6f16f654f1a96987cbe6f445ec5f5a91abb11fc79a1e621d9bab30f46fb7a746d44bb5e8b81904d9370f + languageName: node + linkType: hard + "@standardnotes/api@npm:^1.9.0": version: 1.9.0 resolution: "@standardnotes/api@npm:1.9.0" @@ -1986,6 +2001,20 @@ __metadata: languageName: node linkType: hard +"@standardnotes/encryption@npm:1.16.0": + version: 1.16.0 + resolution: "@standardnotes/encryption@npm:1.16.0" + dependencies: + "@standardnotes/common": ^1.32.0 + "@standardnotes/models": 1.24.0 + "@standardnotes/responses": 1.10.4 + "@standardnotes/sncrypto-common": 1.13.0 + "@standardnotes/utils": 1.9.0 + reflect-metadata: ^0.1.13 + checksum: 9971b9afcc8d32c7a6d720b43b7e098659d40212abfe1c4387f6b8aee4f410198a0e28db61bfdfdd8defe8cd336a9823e4cc565bf8f72145da2d349cb4be12a6 + languageName: node + linkType: hard + "@standardnotes/event-store@workspace:packages/event-store": version: 0.0.0-use.local resolution: "@standardnotes/event-store@workspace:packages/event-store" @@ -2026,6 +2055,18 @@ __metadata: languageName: node linkType: hard +"@standardnotes/features@npm:1.52.2": + version: 1.52.2 + resolution: "@standardnotes/features@npm:1.52.2" + dependencies: + "@standardnotes/auth": ^3.19.4 + "@standardnotes/common": ^1.32.0 + "@standardnotes/security": ^1.2.0 + reflect-metadata: ^0.1.13 + checksum: ab345f8dc11be32967e439366de0507707a95590b898d6c4332d5d42adbba7bb3fe32bca2ee1fd9948b9106d8186402439916402e8df50cf9e440996420251df + languageName: node + linkType: hard + "@standardnotes/features@npm:^1.36.3, @standardnotes/features@npm:^1.47.0": version: 1.50.0 resolution: "@standardnotes/features@npm:1.50.0" @@ -2101,6 +2142,20 @@ __metadata: languageName: node linkType: hard +"@standardnotes/models@npm:1.24.0": + version: 1.24.0 + resolution: "@standardnotes/models@npm:1.24.0" + dependencies: + "@standardnotes/common": ^1.32.0 + "@standardnotes/features": 1.52.2 + "@standardnotes/responses": 1.10.4 + "@standardnotes/utils": 1.9.0 + lodash: ^4.17.21 + reflect-metadata: ^0.1.13 + checksum: 2acbbbc0629011bc9c901f2e61df936b13f349ff90409072dcfecf4899872f6d0e80d22bd45af9fc0928adc33097fd573983d0f194c4c84e7bf204a28de9943d + languageName: node + linkType: hard + "@standardnotes/payloads@npm:^1.5.1": version: 1.5.1 resolution: "@standardnotes/payloads@npm:1.5.1" @@ -2138,6 +2193,18 @@ __metadata: languageName: node linkType: hard +"@standardnotes/responses@npm:1.10.4": + version: 1.10.4 + resolution: "@standardnotes/responses@npm:1.10.4" + dependencies: + "@standardnotes/common": ^1.32.0 + "@standardnotes/features": 1.52.2 + "@standardnotes/security": ^1.1.0 + reflect-metadata: ^0.1.13 + checksum: 41e4971144950e168c0de3d05a2871857d9a416ad8061849b646992f04d6590dce43e8161af0cb05c3faed1856fd4bf93fceea3fcabeebf62b5761e5376ce8c6 + languageName: node + linkType: hard + "@standardnotes/responses@npm:^1.6.39": version: 1.6.39 resolution: "@standardnotes/responses@npm:1.6.39" @@ -2243,6 +2310,15 @@ __metadata: languageName: node linkType: hard +"@standardnotes/sncrypto-common@npm:1.13.0": + version: 1.13.0 + resolution: "@standardnotes/sncrypto-common@npm:1.13.0" + dependencies: + reflect-metadata: ^0.1.13 + checksum: e58258f52546a3f0ce2d8f76d4be79a7d23e15dfd81e687ac66cd3fc00dc91e563556da2e600dbd2f270f659b3e1f3e4301dd6e6e7e6eb4faf51c4343fc65c96 + languageName: node + linkType: hard + "@standardnotes/sncrypto-common@npm:^1.9.0": version: 1.9.0 resolution: "@standardnotes/sncrypto-common@npm:1.9.0" @@ -2366,6 +2442,7 @@ __metadata: dependencies: "@newrelic/winston-enricher": ^4.0.0 "@sentry/node": ^7.3.0 + "@standardnotes/api": ^1.11.0 "@standardnotes/common": "workspace:*" "@standardnotes/domain-events": "workspace:*" "@standardnotes/domain-events-infra": "workspace:*"