feat(api): add workspaces api (#1765)

* feat(api): add workspaces api

* fix(api): lint issues
This commit is contained in:
Karol Sójko
2022-10-07 10:36:30 +02:00
committed by GitHub
parent 3733707bf1
commit 01ba715eba
22 changed files with 303 additions and 8 deletions

View File

@@ -10,10 +10,10 @@ module.exports = {
},
coverageThreshold: {
global: {
branches: 20,
functions: 66,
lines: 63,
statements: 63
branches: 22,
functions: 69,
lines: 67,
statements: 67
}
}
};

View File

@@ -0,0 +1,3 @@
export enum WorkspaceApiOperations {
Creating,
}

View File

@@ -0,0 +1,76 @@
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
import { WorkspaceServerInterface } from '../../Server/Workspace/WorkspaceServerInterface'
import { WorkspaceApiOperations } from './WorkspaceApiOperations'
import { WorkspaceApiService } from './WorkspaceApiService'
describe('WorkspaceApiService', () => {
let workspaceServer: WorkspaceServerInterface
const createService = () => new WorkspaceApiService(workspaceServer)
beforeEach(() => {
workspaceServer = {} as jest.Mocked<WorkspaceServerInterface>
workspaceServer.createWorkspace = jest.fn().mockReturnValue({
data: { uuid: '1-2-3' },
} as jest.Mocked<WorkspaceCreationResponse>)
})
it('should create a workspace', async () => {
const response = await createService().createWorkspace({
encryptedPrivateKey: 'foo',
encryptedWorkspaceKey: 'bar',
publicKey: 'buzz',
})
expect(response).toEqual({
data: {
uuid: '1-2-3',
},
})
expect(workspaceServer.createWorkspace).toHaveBeenCalledWith({
encryptedPrivateKey: 'foo',
encryptedWorkspaceKey: 'bar',
publicKey: 'buzz',
})
})
it('should not create a workspace if it is already creating', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[WorkspaceApiOperations.Creating, true]]),
})
let error = null
try {
await service.createWorkspace({
encryptedPrivateKey: 'foo',
encryptedWorkspaceKey: 'bar',
publicKey: 'buzz',
})
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not create a workspace if the server fails', async () => {
workspaceServer.createWorkspace = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().createWorkspace({
encryptedPrivateKey: 'foo',
encryptedWorkspaceKey: 'bar',
publicKey: 'buzz',
})
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
})

View File

@@ -0,0 +1,43 @@
import { ErrorMessage } from '../../Error/ErrorMessage'
import { ApiCallError } from '../../Error/ApiCallError'
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
import { WorkspaceServerInterface } from '../../Server/Workspace/WorkspaceServerInterface'
import { WorkspaceApiServiceInterface } from './WorkspaceApiServiceInterface'
import { WorkspaceApiOperations } from './WorkspaceApiOperations'
export class WorkspaceApiService implements WorkspaceApiServiceInterface {
private operationsInProgress: Map<WorkspaceApiOperations, boolean>
constructor(private workspaceServer: WorkspaceServerInterface) {
this.operationsInProgress = new Map()
}
async createWorkspace(dto: {
encryptedWorkspaceKey: string
encryptedPrivateKey: string
publicKey: string
workspaceName?: string
}): Promise<WorkspaceCreationResponse> {
if (this.operationsInProgress.get(WorkspaceApiOperations.Creating)) {
throw new ApiCallError(ErrorMessage.GenericInProgress)
}
this.operationsInProgress.set(WorkspaceApiOperations.Creating, true)
try {
const response = await this.workspaceServer.createWorkspace({
encryptedPrivateKey: dto.encryptedPrivateKey,
encryptedWorkspaceKey: dto.encryptedWorkspaceKey,
publicKey: dto.publicKey,
workspaceName: dto.workspaceName,
})
this.operationsInProgress.set(WorkspaceApiOperations.Creating, false)
return response
} catch (error) {
throw new ApiCallError(ErrorMessage.GenericFail)
}
}
}

View File

@@ -0,0 +1,10 @@
import { WorkspaceCreationResponse } from '../../Response'
export interface WorkspaceApiServiceInterface {
createWorkspace(dto: {
encryptedWorkspaceKey: string
encryptedPrivateKey: string
publicKey: string
workspaceName?: string
}): Promise<WorkspaceCreationResponse>
}

View File

@@ -5,3 +5,5 @@ export * from './User/UserApiService'
export * from './User/UserApiServiceInterface'
export * from './WebSocket/WebSocketApiService'
export * from './WebSocket/WebSocketApiServiceInterface'
export * from './Workspace/WorkspaceApiService'
export * from './Workspace/WorkspaceApiServiceInterface'

View File

@@ -0,0 +1,7 @@
export type WorkspaceCreationRequestParams = {
encryptedWorkspaceKey: string
encryptedPrivateKey: string
publicKey: string
workspaceName?: string
[additionalParam: string]: unknown
}

View File

@@ -6,3 +6,4 @@ export * from './Subscription/SubscriptionInviteListRequestParams'
export * from './Subscription/SubscriptionInviteRequestParams'
export * from './User/UserRegistrationRequestParams'
export * from './WebSocket/WebSocketConnectionTokenRequestParams'
export * from './Workspace/WorkspaceCreationRequestParams'

View File

@@ -0,0 +1,9 @@
import { Either } from '@standardnotes/common'
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { WorkspaceCreationResponseBody } from './WorkspaceCreationResponseBody'
export interface WorkspaceCreationResponse extends HttpResponse {
data: Either<WorkspaceCreationResponseBody, HttpErrorResponseBody>
}

View File

@@ -0,0 +1,3 @@
export type WorkspaceCreationResponseBody = {
uuid: string
}

View File

@@ -12,3 +12,5 @@ export * from './User/UserRegistrationResponse'
export * from './User/UserRegistrationResponseBody'
export * from './WebSocket/WebSocketConnectionTokenResponse'
export * from './WebSocket/WebSocketConnectionTokenResponseBody'
export * from './Workspace/WorkspaceCreationResponse'
export * from './Workspace/WorkspaceCreationResponseBody'

View File

@@ -0,0 +1,9 @@
const WorkspacePaths = {
createWorkspace: '/v1/workspaces',
}
export const Paths = {
v1: {
...WorkspacePaths,
},
}

View File

@@ -0,0 +1,31 @@
import { HttpServiceInterface } from '../../Http'
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
import { WorkspaceServer } from './WorkspaceServer'
describe('WorkspaceServer', () => {
let httpService: HttpServiceInterface
const createServer = () => new WorkspaceServer(httpService)
beforeEach(() => {
httpService = {} as jest.Mocked<HttpServiceInterface>
httpService.post = jest.fn().mockReturnValue({
data: { uuid: '1-2-3' },
} as jest.Mocked<WorkspaceCreationResponse>)
})
it('should create a workspace', async () => {
const response = await createServer().createWorkspace({
encryptedPrivateKey: 'foo',
encryptedWorkspaceKey: 'bar',
publicKey: 'buzz',
})
expect(response).toEqual({
data: {
uuid: '1-2-3',
},
})
})
})

View File

@@ -0,0 +1,16 @@
import { HttpServiceInterface } from '../../Http/HttpServiceInterface'
import { WorkspaceCreationRequestParams } from '../../Request/Workspace/WorkspaceCreationRequestParams'
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
import { Paths } from './Paths'
import { WorkspaceServerInterface } from './WorkspaceServerInterface'
export class WorkspaceServer implements WorkspaceServerInterface {
constructor(private httpService: HttpServiceInterface) {}
async createWorkspace(params: WorkspaceCreationRequestParams): Promise<WorkspaceCreationResponse> {
const response = await this.httpService.post(Paths.v1.createWorkspace, params)
return response as WorkspaceCreationResponse
}
}

View File

@@ -0,0 +1,6 @@
import { WorkspaceCreationRequestParams } from '../../Request/Workspace/WorkspaceCreationRequestParams'
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
export interface WorkspaceServerInterface {
createWorkspace(params: WorkspaceCreationRequestParams): Promise<WorkspaceCreationResponse>
}

View File

@@ -4,3 +4,5 @@ export * from './User/UserServer'
export * from './User/UserServerInterface'
export * from './WebSocket/WebSocketServer'
export * from './WebSocket/WebSocketServerInterface'
export * from './Workspace/WorkspaceServer'
export * from './Workspace/WorkspaceServerInterface'

View File

@@ -12,7 +12,7 @@ module.exports = {
global: {
branches: 9,
functions: 10,
lines: 17,
lines: 16,
statements: 16
}
}

View File

@@ -1,13 +1,15 @@
import { ApplicationIdentifier, ContentType } from '@standardnotes/common'
import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models'
import { FilesClientInterface } from '@standardnotes/files'
import { AlertService } from '../Alert/AlertService'
import { AlertService } from '../Alert/AlertService'
import { ComponentManagerInterface } from '../Component/ComponentManagerInterface'
import { ApplicationEvent } from '../Event/ApplicationEvent'
import { ApplicationEventCallback } from '../Event/ApplicationEventCallback'
import { FeaturesClientInterface } from '../Feature/FeaturesClientInterface'
import { SubscriptionClientInterface } from '../Subscription/SubscriptionClientInterface'
import { DeviceInterface } from '../Device/DeviceInterface'
import { WorkspaceClientInterface } from '../Workspace/WorkspaceClientInterface'
import { ItemsClientInterface } from '../Item/ItemsClientInterface'
import { MutatorClientInterface } from '../Mutator/MutatorClientInterface'
import { StorageValueModes } from '../Storage/StorageTypes'
@@ -15,7 +17,6 @@ import { StorageValueModes } from '../Storage/StorageTypes'
import { DeinitMode } from './DeinitMode'
import { DeinitSource } from './DeinitSource'
import { UserClientInterface } from './UserClientInterface'
import { DeviceInterface } from '../Device/DeviceInterface'
export interface ApplicationInterface {
deinit(mode: DeinitMode, source: DeinitSource): void
@@ -49,6 +50,7 @@ export interface ApplicationInterface {
get user(): UserClientInterface
get files(): FilesClientInterface
get subscriptions(): SubscriptionClientInterface
get workspaces(): WorkspaceClientInterface
readonly identifier: ApplicationIdentifier
readonly platform: Platform
deviceInterface: DeviceInterface

View File

@@ -0,0 +1,8 @@
export interface WorkspaceClientInterface {
createWorkspace(dto: {
encryptedWorkspaceKey: string
encryptedPrivateKey: string
publicKey: string
workspaceName?: string
}): Promise<{ uuid: string } | null>
}

View File

@@ -0,0 +1,32 @@
import { WorkspaceApiServiceInterface } from '@standardnotes/api'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { AbstractService } from '../Service/AbstractService'
import { WorkspaceClientInterface } from './WorkspaceClientInterface'
export class WorkspaceManager extends AbstractService implements WorkspaceClientInterface {
constructor(
private workspaceApiService: WorkspaceApiServiceInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
}
async createWorkspace(dto: {
encryptedWorkspaceKey: string
encryptedPrivateKey: string
publicKey: string
workspaceName?: string
}): Promise<{ uuid: string } | null> {
try {
const result = await this.workspaceApiService.createWorkspace(dto)
if (result.data.error !== undefined) {
return null
}
return result.data
} catch (error) {
return null
}
}
}

View File

@@ -73,3 +73,5 @@ export * from './Sync/SyncOptions'
export * from './Sync/SyncQueueStrategy'
export * from './Sync/SyncServiceInterface'
export * from './Sync/SyncSource'
export * from './Workspace/WorkspaceClientInterface'
export * from './Workspace/WorkspaceManager'

View File

@@ -14,6 +14,10 @@ import {
WebSocketApiServiceInterface,
WebSocketServer,
WebSocketServerInterface,
WorkspaceApiService,
WorkspaceApiServiceInterface,
WorkspaceServer,
WorkspaceServerInterface,
} from '@standardnotes/api'
import * as Common from '@standardnotes/common'
import * as ExternalServices from '@standardnotes/services'
@@ -45,6 +49,8 @@ import {
FileService,
SubscriptionClientInterface,
SubscriptionManager,
WorkspaceClientInterface,
WorkspaceManager,
} from '@standardnotes/services'
import { FilesClientInterface } from '@standardnotes/files'
import { ComputePrivateWorkspaceIdentifier } from '@standardnotes/encryption'
@@ -110,6 +116,9 @@ export class SNApplication
private declare subscriptionApiService: SubscriptionApiServiceInterface
private declare subscriptionServer: SubscriptionServerInterface
private declare subscriptionManager: SubscriptionClientInterface
private declare workspaceApiService: WorkspaceApiServiceInterface
private declare workspaceServer: WorkspaceServerInterface
private declare workspaceManager: WorkspaceClientInterface
private declare webSocketApiService: WebSocketApiServiceInterface
private declare webSocketServer: WebSocketServerInterface
private sessionManager!: InternalServices.SNSessionManager
@@ -211,6 +220,10 @@ export class SNApplication
return this.subscriptionManager
}
get workspaces(): ExternalServices.WorkspaceClientInterface {
return this.workspaceManager
}
public get files(): FilesClientInterface {
return this.fileService
}
@@ -1047,6 +1060,9 @@ export class SNApplication
this.createWebSocketServer()
this.createWebSocketApiService()
this.createSubscriptionManager()
this.createWorkspaceServer()
this.createWorkspaceApiService()
this.createWorkspaceManager()
this.createWebSocketsService()
this.createSessionManager()
this.createHistoryManager()
@@ -1087,9 +1103,12 @@ export class SNApplication
;(this.userServer as unknown) = undefined
;(this.subscriptionApiService as unknown) = undefined
;(this.subscriptionServer as unknown) = undefined
;(this.subscriptionManager as unknown) = undefined
;(this.workspaceApiService as unknown) = undefined
;(this.workspaceServer as unknown) = undefined
;(this.workspaceManager as unknown) = undefined
;(this.webSocketApiService as unknown) = undefined
;(this.webSocketServer as unknown) = undefined
;(this.subscriptionManager as unknown) = undefined
;(this.sessionManager as unknown) = undefined
;(this.syncService as unknown) = undefined
;(this.challengeService as unknown) = undefined
@@ -1304,6 +1323,18 @@ export class SNApplication
this.subscriptionManager = new SubscriptionManager(this.subscriptionApiService, this.internalEventBus)
}
private createWorkspaceServer() {
this.workspaceServer = new WorkspaceServer(this.httpService)
}
private createWorkspaceApiService() {
this.workspaceApiService = new WorkspaceApiService(this.workspaceServer)
}
private createWorkspaceManager() {
this.workspaceManager = new WorkspaceManager(this.workspaceApiService, this.internalEventBus)
}
private createItemManager() {
this.itemManager = new InternalServices.ItemManager(this.payloadManager, this.options, this.internalEventBus)
this.services.push(this.itemManager)