From 7bb4c6115ba0ddb3ea90e7ff9611d185efd5e46a Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 18 Oct 2023 15:58:40 +0200 Subject: [PATCH] fix: continued implementation co-authored by ianshade --- apps/app/src/electron/EverythingService.ts | 9 +- apps/app/src/electron/api/PartService.ts | 45 ++++- apps/app/src/electron/api/ProjectService.ts | 5 + apps/app/src/electron/api/RundownService.ts | 190 -------------------- apps/app/src/ipc/IPCAPI.ts | 31 +++- apps/app/src/ipc/__tests__/IPCAPI.test.ts | 56 ++++++ apps/app/src/react/api/ApiClient.ts | 14 +- 7 files changed, 143 insertions(+), 207 deletions(-) create mode 100644 apps/app/src/ipc/__tests__/IPCAPI.test.ts diff --git a/apps/app/src/electron/EverythingService.ts b/apps/app/src/electron/EverythingService.ts index f2d1b030..c97e40a6 100644 --- a/apps/app/src/electron/EverythingService.ts +++ b/apps/app/src/electron/EverythingService.ts @@ -122,7 +122,10 @@ type ConvertToServerSide = { : T[K] } -/** This class is used server-side, to handle requests from the client */ +/** + * This class is used server-side, to handle requests from the client + * The methods in here will later be moved away to other Services + */ export class EverythingService extends (EventEmitter as new () => TypedEmitter) implements ConvertToServerSide @@ -2201,8 +2204,10 @@ export class EverythingService this._saveUpdates({ appData }) } - async updateProject(arg: { id: string; project: Project }): Promise { + async updateProject(arg: { id: string; project: Project }): Promise { this._saveUpdates({ project: arg.project }) + + return this.storage.getProject() } async newRundown(arg: { name: string }): Promise> { const rundown = this.storage.newRundown(arg.name) diff --git a/apps/app/src/electron/api/PartService.ts b/apps/app/src/electron/api/PartService.ts index e82c175a..51d5e4fb 100644 --- a/apps/app/src/electron/api/PartService.ts +++ b/apps/app/src/electron/api/PartService.ts @@ -4,10 +4,11 @@ import EventEmitter from 'node:events' import { GeneralError } from '@feathersjs/errors' import { RundownTrigger } from '../../models/rundown/Trigger' import { ClientEventBus } from '../ClientEventBus' -import { ResourceAny } from '@shared/models' +import { ResourceAny, ResourceId } from '@shared/models' import { MoveTarget } from '../../lib/util' import { Part } from '../../models/rundown/Part' import { ServiceTypes } from '../../ipc/IPCAPI' +import { TimelineObj } from '../../models/rundown/TimelineObj' export class PartService extends EventEmitter { constructor( @@ -101,4 +102,46 @@ export class PartService extends EventEmitter { const result = await this.everythingService.duplicatePart(data) if (!result) throw new GeneralError() } + + async deleteTimelineObj(data: { + rundownId: string + groupId: string + partId: string + timelineObjId: string + }): Promise { + // TODO: access control + const result = await this.everythingService.deleteTimelineObj(data) + if (!result) throw new GeneralError() + } + + async insertTimelineObjs(data: { + rundownId: string + groupId: string + partId: string + timelineObjs: TimelineObj[] + target: MoveTarget | null + }): Promise< + { + groupId: string + partId: string + timelineObjId: string + }[] + > { + // TODO: access control + const result = await this.everythingService.insertTimelineObjs(data) + if (!result?.result) throw new GeneralError() + return result.result + } + + async addResourcesToTimeline(data: { + rundownId: string + groupId: string + partId: string + + layerId: string | null + resourceIds: (ResourceId | ResourceAny)[] + }): Promise { + // TODO: access control + await this.everythingService.addResourcesToTimeline(data) + } } diff --git a/apps/app/src/electron/api/ProjectService.ts b/apps/app/src/electron/api/ProjectService.ts index 6499f9c8..1a64f6c3 100644 --- a/apps/app/src/electron/api/ProjectService.ts +++ b/apps/app/src/electron/api/ProjectService.ts @@ -44,6 +44,11 @@ export class ProjectService extends EventEmitter { return await this.everythingService.newProject() } + async update(id: string, project: Project): Promise { + // TODO: access control + return await this.everythingService.updateProject({ id, project }) + } + async open(data: { projectId: string }): Promise { // TODO: access control return await this.everythingService.openProject(data) diff --git a/apps/app/src/electron/api/RundownService.ts b/apps/app/src/electron/api/RundownService.ts index c67b9f26..e5ef9ad7 100644 --- a/apps/app/src/electron/api/RundownService.ts +++ b/apps/app/src/electron/api/RundownService.ts @@ -4,12 +4,9 @@ import { Rundown } from '../../models/rundown/Rundown' import EventEmitter from 'node:events' import { GeneralError, NotFound } from '@feathersjs/errors' import { ServiceTypes } from '../../ipc/IPCAPI' -import { RundownTrigger } from '../../models/rundown/Trigger' import { PartialDeep } from 'type-fest/source/partial-deep' import { TimelineObj } from '../../models/rundown/TimelineObj' import { ClientEventBus } from '../ClientEventBus' -import { ResourceAny, ResourceId } from '@shared/models' -import { MoveTarget } from '../../lib/util' export const RUNDOWN_CHANNEL_PREFIX = 'rundowns/' interface TimelineObjectUpdate { @@ -93,84 +90,6 @@ export class RundownService extends EventEmitter { return await this.everythingService.isRundownPlaying(data) } - async setPartTrigger(data: { - rundownId: string - groupId: string - partId: string - trigger: RundownTrigger | null - triggerIndex: number | null - }): Promise { - // TODO: access control - await this.everythingService.setPartTrigger(data) - } - - // async stopGroup(data: GroupData): Promise { - // // TODO: access control - // await this.everythingService.stopGroup(data) - // } - - // async playGroup(data: GroupData): Promise { - // // TODO: access control - // await this.everythingService.playGroup(data) - // } - - // async pauseGroup(data: GroupData): Promise { - // // TODO: access control - // await this.everythingService.pauseGroup(data) - // } - - // async playNext(data: GroupData): Promise { - // // TODO: access control - // await this.everythingService.playNext(data) - // } - - // async playPrev(data: GroupData): Promise { - // // TODO: access control - // await this.everythingService.playPrev(data) - // } - - async deleteTimelineObj(data: { - rundownId: string - groupId: string - partId: string - timelineObjId: string - }): Promise { - // TODO: access control - const result = await this.everythingService.deleteTimelineObj(data) - if (!result) throw new GeneralError() - } - - async insertTimelineObjs(data: { - rundownId: string - groupId: string - partId: string - timelineObjs: TimelineObj[] - target: MoveTarget | null - }): Promise< - { - groupId: string - partId: string - timelineObjId: string - }[] - > { - // TODO: access control - const result = await this.everythingService.insertTimelineObjs(data) - if (!result?.result) throw new GeneralError() - return result.result - } - - async addResourcesToTimeline(data: { - rundownId: string - groupId: string - partId: string - - layerId: string | null - resourceIds: (ResourceId | ResourceAny)[] - }): Promise { - // TODO: access control - await this.everythingService.addResourcesToTimeline(data) - } - async updateTimelineObj(data: TimelineObjectUpdate): Promise { // TODO: access control await this.everythingService.updateTimelineObj(data) @@ -180,113 +99,4 @@ export class RundownService extends EventEmitter { // TODO: access control await this.everythingService.moveTimelineObjToNewLayer(data) } - - // async newPart(data: { - // rundownId: string - // /** The group to create the part into. If null; will create a "transparent group" */ - // groupId: string | null - - // name: string - // }): Promise<{ partId: string; groupId?: string }> { - // // TODO: access control - // const result = await this.everythingService.newPart(data) - // if (!result?.result) throw new GeneralError() - // return result.result - // } - - // async insertParts(data: { - // rundownId: string - // groupId: string | null - // parts: { part: Part; resources: ResourceAny[] }[] - // target: MoveTarget - // }): Promise< - // { - // groupId: string - // partId: string - // }[] - // > { - // // TODO: access control - // const result = await this.everythingService.insertParts(data) - // if (!result?.result) throw new GeneralError() - // return result.result - // } - - // async updatePart(data: { rundownId: string; groupId: string; partId: string; part: Partial }): Promise { - // // TODO: access control - // const result = await this.everythingService.updatePart(data) - // if (!result) throw new GeneralError() - // } - - // async newGroup(data: { rundownId: string; name: string }): Promise { - // // TODO: access control - // const result = await this.everythingService.newGroup(data) - // if (!result?.result) throw new GeneralError() - // return result.result - // } - - // async insertGroups(data: { - // rundownId: string - // groups: { - // group: Group - // resources: { - // [partId: string]: ResourceAny[] - // } - // }[] - // target: MoveTarget - // }): Promise< - // { - // groupId: string - // }[] - // > { - // // TODO: access control - // const result = await this.everythingService.insertGroups(data) - // if (!result?.result) throw new GeneralError() - // return result.result - // } - - // async updateGroup(data: { rundownId: string; groupId: string; group: PartialDeep }): Promise { - // // TODO: access control - // const result = await this.everythingService.updateGroup(data) - // if (!result) throw new GeneralError() - // } - - // async deletePart(data: { rundownId: string; groupId: string; partId: string }): Promise { - // // TODO: access control - // const result = await this.everythingService.deletePart(data) - // if (!result) throw new GeneralError() - // } - - // async deleteGroup(data: { rundownId: string; groupId: string }): Promise { - // // TODO: access control - // const result = await this.everythingService.deleteGroup(data) - // if (!result) throw new GeneralError() - // } - - // async moveParts(data: { - // parts: { rundownId: string; partId: string }[] - // to: { rundownId: string; groupId: string | null; target: MoveTarget } - // }): Promise<{ partId: string; groupId: string; rundownId: string }[]> { - // // TODO: access control - // const result = await this.everythingService.moveParts(data) - // if (!result?.result) throw new GeneralError() - // return result.result - // } - - // async duplicatePart(data: { rundownId: string; groupId: string; partId: string }): Promise { - // // TODO: access control - // const result = await this.everythingService.duplicatePart(data) - // if (!result) throw new GeneralError() - // } - - // async moveGroups(data: { rundownId: string; groupIds: string[]; target: MoveTarget }): Promise { - // // TODO: access control - // const result = await this.everythingService.moveGroups(data) - // if (!result) throw new GeneralError() - // } - - // async duplicateGroup(data: { rundownId: string; groupId: string }): Promise { - // // TODO: access control - // const result = await this.everythingService.duplicateGroup(data) - // if (!result) throw new GeneralError() - // } } diff --git a/apps/app/src/ipc/IPCAPI.ts b/apps/app/src/ipc/IPCAPI.ts index 385987db..c11a2a0d 100644 --- a/apps/app/src/ipc/IPCAPI.ts +++ b/apps/app/src/ipc/IPCAPI.ts @@ -20,9 +20,9 @@ import { DefiningArea } from '../lib/triggers/keyDisplay/keyDisplay' import { type EverythingService } from '../electron/EverythingService' import { type PartService } from '../electron/api/PartService' import { type ProjectService } from '../electron/api/ProjectService' -import { ReportingService } from '../electron/api/ReportingService' -import { RundownService } from '../electron/api/RundownService' -import { GroupService } from '../electron/api/GroupService' +import { type ReportingService } from '../electron/api/ReportingService' +import { type RundownService } from '../electron/api/RundownService' +import { type GroupService } from '../electron/api/GroupService' export const MAX_UNDO_LEDGER_LENGTH = 100 @@ -66,8 +66,23 @@ export const ClientMethods: ServiceKeyArrays = { 'update', ], [ServiceName.LEGACY]: [], - [ServiceName.PARTS]: ['play', 'stop', 'pause', 'move', 'duplicate', 'create', 'update', 'remove', 'insert'], - [ServiceName.PROJECTS]: ['create', 'getAll', 'open', 'import', 'export'], + [ServiceName.PARTS]: [ + 'play', + 'stop', + 'pause', + 'move', + 'duplicate', + 'create', + 'update', + 'remove', + 'insert', + 'setPartTrigger', + + 'addResourcesToTimeline', + 'deleteTimelineObj', + 'insertTimelineObjs', + ], + [ServiceName.PROJECTS]: ['get', 'create', 'update', 'getAll', 'open', 'import', 'export', 'unsubscribe'], [ServiceName.REPORTING]: [ 'log', 'handleClientError', @@ -78,11 +93,12 @@ export const ClientMethods: ServiceKeyArrays = { [ServiceName.RUNDOWNS]: [ 'get', 'create', + 'remove', + 'rename', 'unsubscribe', 'isPlaying', 'close', 'open', - 'setPartTrigger', // 'pauseGroup', // 'playGroup', // 'playNext', @@ -90,6 +106,7 @@ export const ClientMethods: ServiceKeyArrays = { // 'stopGroup', 'updateTimelineObj', 'moveTimelineObjToNewLayer', + // 'newPart', // 'insertParts', // 'updatePart', @@ -291,7 +308,7 @@ export interface IPCServerMethods { triggerHandleAutoFill: () => void updateAppData: (arg: UpdateAppDataOptions) => void - updateProject: (arg: { id: string; project: Project }) => void + updateProject: (arg: { id: string; project: Project }) => Project newRundown: (arg: { name: string }) => Rundown deleteRundown: (arg: { rundownId: string }) => void diff --git a/apps/app/src/ipc/__tests__/IPCAPI.test.ts b/apps/app/src/ipc/__tests__/IPCAPI.test.ts new file mode 100644 index 00000000..864346e6 --- /dev/null +++ b/apps/app/src/ipc/__tests__/IPCAPI.test.ts @@ -0,0 +1,56 @@ +import { ClientMethods, ServiceName, ServiceTypes } from '../IPCAPI' + +import { EverythingService } from '../../electron/EverythingService' +import { PartService } from '../../electron/api/PartService' +import { ProjectService } from '../../electron/api/ProjectService' +import { ReportingService } from '../../electron/api/ReportingService' +import { RundownService } from '../../electron/api/RundownService' +import { GroupService } from '../../electron/api/GroupService' + +describe('ClientMethods', () => { + const services: ServiceTypes = { + [ServiceName.GROUPS]: GroupService.prototype, + [ServiceName.LEGACY]: EverythingService.prototype, + [ServiceName.PARTS]: PartService.prototype, + [ServiceName.PROJECTS]: ProjectService.prototype, + [ServiceName.REPORTING]: ReportingService.prototype, + [ServiceName.RUNDOWNS]: RundownService.prototype, + } + test('ClientMethods has all methods', () => { + const missingMethods: string[] = [] + const IGNORE_METHODS = ['constructor', 'getMaxListeners'] + for (const [serviceType, service] of Object.entries(services)) { + if (serviceType === ServiceName.LEGACY) continue // skip legacy service + + const clientServiceMethods = (ClientMethods as any)[serviceType] + + for (const method of Object.getOwnPropertyNames(service)) { + if (typeof (service as any)[method] === 'function') { + if (IGNORE_METHODS.includes(method)) continue + if (!clientServiceMethods.includes(method)) { + missingMethods.push(`${serviceType}.${method}`) + } + } else { + throw new Error(`Method "${method}" on service "${serviceType}" is not a function`) + } + } + } + expect(missingMethods).toHaveLength(0) + }) + test('methods in ClientMethods exist in services', () => { + const nonexistantMethods: string[] = [] + + for (const [serviceType, clientMethods] of Object.entries(ClientMethods)) { + if (serviceType === ServiceName.LEGACY) continue // skip legacy service + + const serviceMethods = (services as any)[serviceType] + + for (const method of clientMethods) { + if (typeof serviceMethods[method] !== 'function') { + nonexistantMethods.push(`${serviceType}.${method}`) + } + } + } + expect(nonexistantMethods).toHaveLength(0) + }) +}) diff --git a/apps/app/src/react/api/ApiClient.ts b/apps/app/src/react/api/ApiClient.ts index 81565ee4..194e203b 100644 --- a/apps/app/src/react/api/ApiClient.ts +++ b/apps/app/src/react/api/ApiClient.ts @@ -226,14 +226,14 @@ export class ApiClient { async duplicateGroup(data: GroupArg<'duplicate'>): ServerReturn<'duplicateGroup'> { return await this.groupService.duplicate(data) } - async deleteTimelineObj(data: RundownArg<'deleteTimelineObj'>): ServerReturn<'deleteTimelineObj'> { - return await this.rundownService.deleteTimelineObj(data) + async deleteTimelineObj(data: PartArg<'deleteTimelineObj'>): ServerReturn<'deleteTimelineObj'> { + return await this.partService.deleteTimelineObj(data) } - async insertTimelineObjs(data: RundownArg<'insertTimelineObjs'>): ServerReturn<'insertTimelineObjs'> { - return await this.rundownService.insertTimelineObjs(data) + async insertTimelineObjs(data: PartArg<'insertTimelineObjs'>): ServerReturn<'insertTimelineObjs'> { + return await this.partService.insertTimelineObjs(data) } - async addResourcesToTimeline(data: RundownArg<'addResourcesToTimeline'>): ServerReturn<'addResourcesToTimeline'> { - return await this.rundownService.addResourcesToTimeline(data) + async addResourcesToTimeline(data: PartArg<'addResourcesToTimeline'>): ServerReturn<'addResourcesToTimeline'> { + return await this.partService.addResourcesToTimeline(data) } async toggleGroupLoop(...args: ServerArgs<'toggleGroupLoop'>): ServerReturn<'toggleGroupLoop'> { return this.invokeServerMethod('toggleGroupLoop', ...args) @@ -273,7 +273,7 @@ export class ApiClient { return this.invokeServerMethod('updateAppData', ...args) } async updateProject(data: { id: string; project: Project }): ServerReturn<'updateProject'> { - await this.projectService.update(data.id, data.project) + return await this.projectService.update(data.id, data.project) } async newRundown(...args: ServerArgs<'newRundown'>): ServerReturn<'newRundown'> { return await this.rundownService.create(...args)