feat(api): add workspaces api (#1765)
* feat(api): add workspaces api * fix(api): lint issues
This commit is contained in:
@@ -10,10 +10,10 @@ module.exports = {
|
||||
},
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 20,
|
||||
functions: 66,
|
||||
lines: 63,
|
||||
statements: 63
|
||||
branches: 22,
|
||||
functions: 69,
|
||||
lines: 67,
|
||||
statements: 67
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum WorkspaceApiOperations {
|
||||
Creating,
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { WorkspaceCreationResponse } from '../../Response'
|
||||
|
||||
export interface WorkspaceApiServiceInterface {
|
||||
createWorkspace(dto: {
|
||||
encryptedWorkspaceKey: string
|
||||
encryptedPrivateKey: string
|
||||
publicKey: string
|
||||
workspaceName?: string
|
||||
}): Promise<WorkspaceCreationResponse>
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export type WorkspaceCreationRequestParams = {
|
||||
encryptedWorkspaceKey: string
|
||||
encryptedPrivateKey: string
|
||||
publicKey: string
|
||||
workspaceName?: string
|
||||
[additionalParam: string]: unknown
|
||||
}
|
||||
@@ -6,3 +6,4 @@ export * from './Subscription/SubscriptionInviteListRequestParams'
|
||||
export * from './Subscription/SubscriptionInviteRequestParams'
|
||||
export * from './User/UserRegistrationRequestParams'
|
||||
export * from './WebSocket/WebSocketConnectionTokenRequestParams'
|
||||
export * from './Workspace/WorkspaceCreationRequestParams'
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type WorkspaceCreationResponseBody = {
|
||||
uuid: string
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
9
packages/api/src/Domain/Server/Workspace/Paths.ts
Normal file
9
packages/api/src/Domain/Server/Workspace/Paths.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
const WorkspacePaths = {
|
||||
createWorkspace: '/v1/workspaces',
|
||||
}
|
||||
|
||||
export const Paths = {
|
||||
v1: {
|
||||
...WorkspacePaths,
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
16
packages/api/src/Domain/Server/Workspace/WorkspaceServer.ts
Normal file
16
packages/api/src/Domain/Server/Workspace/WorkspaceServer.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { WorkspaceCreationRequestParams } from '../../Request/Workspace/WorkspaceCreationRequestParams'
|
||||
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
|
||||
|
||||
export interface WorkspaceServerInterface {
|
||||
createWorkspace(params: WorkspaceCreationRequestParams): Promise<WorkspaceCreationResponse>
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -12,7 +12,7 @@ module.exports = {
|
||||
global: {
|
||||
branches: 9,
|
||||
functions: 10,
|
||||
lines: 17,
|
||||
lines: 16,
|
||||
statements: 16
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface WorkspaceClientInterface {
|
||||
createWorkspace(dto: {
|
||||
encryptedWorkspaceKey: string
|
||||
encryptedPrivateKey: string
|
||||
publicKey: string
|
||||
workspaceName?: string
|
||||
}): Promise<{ uuid: string } | null>
|
||||
}
|
||||
32
packages/services/src/Domain/Workspace/WorkspaceManager.ts
Normal file
32
packages/services/src/Domain/Workspace/WorkspaceManager.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user