Skip to content

Commit

Permalink
Merge branch 'feat/upsert' into develop
Browse files Browse the repository at this point in the history
# Conflicts:
#	apps/app/src/electron/IPCServer.ts
#	apps/app/src/models/rundown/Group.ts
  • Loading branch information
nytamin committed Jun 17, 2023
2 parents 3f1ab90 + 537cdc7 commit 0d8177f
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 28 deletions.
2 changes: 2 additions & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"devDependencies": {
"@types/deep-extend": "0.4.32",
"@types/koa": "^2.13.5",
"@types/koa-bodyparser": "^4.3.10",
"@types/koa__router": "^12.0.0",
"@types/lodash": "^4.14.189",
"@types/moment": "^2.13.0",
Expand Down Expand Up @@ -91,6 +92,7 @@
"got": "^11.8.2",
"graphics-data-definition": "0.1.1-nightly-fix-npm-repo-20230214-163658-dbb8098.0",
"koa": "^2.13.4",
"koa-bodyparser": "^4.4.0",
"lodash": "^4.17.21",
"mobx": "^6.7.0",
"mobx-react-lite": "^3.4.0",
Expand Down
38 changes: 11 additions & 27 deletions apps/app/src/electron/HTTPAPI.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Koa from 'koa'
import Router from '@koa/router'
import bodyParser from 'koa-bodyparser'
import { IPCServer, isUndoable } from './IPCServer'
import { stringifyError } from '@shared/lib'
import { LoggerLike } from '@shared/api'
Expand All @@ -16,6 +17,7 @@ export class HTTPAPI {
} = {}

constructor(port: number, ipcServer: IPCServer, log: LoggerLike) {
this.app.use(bodyParser())
this.router.get(`/`, async (ctx) => {
ctx.response.body = `<html><body>
<a href="/api/internal">Internal API (unstable)</a>
Expand All @@ -25,13 +27,15 @@ export class HTTPAPI {
this.router.get(`/api/internal`, async (ctx) => {
const methods = Object.entries<{ endpoint: string; type: string }>(this.methodSignatures)
.map(([_fullEndpoint, e]) => {
const url = `/api/internal/${e.endpoint}/?`

const url = `/api/internal/${e.endpoint}`
return `<a href="${url}">${e.type} ${url}</a>`
})
.join('<br>\n')

ctx.response.body = `<html><body>${methods}</body></html>`
ctx.response.body = `<html><body>
<p>Send request parameters as a JSON body.</p>
${methods}
</body></html>`
ctx.response.status = 200
})

Expand All @@ -49,33 +53,14 @@ export class HTTPAPI {
let endpoint: string
let endpointType: 'GET' | 'POST' | 'DELETE'

if (methodName.startsWith('get')) {
// Handle GET requests

endpoint = methodName.charAt(3).toLocaleLowerCase() + methodName.substring(4)
endpointType = 'GET'
this.router.get(`/api/internal/${endpoint}`, async (ctx) => {
try {
const result = await fcn(ctx.request.query)
ctx.response.body = result
ctx.response.status = 200
} catch (error) {
const stringifiedError = stringifyError(error)
if (stringifiedError.match(/not found/i)) {
ctx.response.status = 404
} else {
ctx.response.status = 500
}
}
})
} else if (methodName.startsWith('delete')) {
if (methodName.startsWith('delete')) {
// Handle DELETE requests

endpoint = methodName.charAt(6).toLocaleLowerCase() + methodName.substring(7)
endpointType = 'DELETE'
this.router.delete(`/api/internal/${endpoint}`, async (ctx) => {
try {
const result = await fcn(ctx.request.query)
const result = await fcn(ctx.request.body)
if (isUndoable(result)) {
ctx.response.body = result.result
} else {
Expand All @@ -98,7 +83,7 @@ export class HTTPAPI {
endpointType = 'POST'
this.router.post(`/api/internal/${endpoint}`, async (ctx) => {
try {
const result = await fcn(ctx.request.query)
const result = await fcn(ctx.request.body)
if (isUndoable(result)) {
ctx.response.body = result.result
} else {
Expand All @@ -118,12 +103,11 @@ export class HTTPAPI {

const fullEndpoint = `${endpointType} ${endpoint}`
if (this.methodSignatures[fullEndpoint]) {
throw new Error(`Dupliacte API endpoints "${fullEndpoint}"!`)
throw new Error(`Duplicate API endpoints "${fullEndpoint}"!`)
}
this.methodSignatures[fullEndpoint] = {
type: endpointType,
endpoint,
// TODO: how to extract the signature of the function here?
}
}

Expand Down
222 changes: 221 additions & 1 deletion apps/app/src/electron/IPCServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
deletePart,
deleteTimelineObj,
findGroup,
findGroupByExternalId,
findPartInGroup,
findPartInGroupByExternalId,
findPartInRundown,
findTimelineObj,
findTimelineObjIndex,
Expand Down Expand Up @@ -51,7 +53,7 @@ import {
TSRDeviceId,
unprotectString,
} from '@shared/models'
import { assertNever, deepClone, getResourceIdFromTimelineObj } from '@shared/lib'
import { assertNever, deepClone, getResourceIdFromTimelineObj, omit } from '@shared/lib'
import { TimelineObj } from '../models/rundown/TimelineObj'
import { Project } from '../models/project/Project'
import { AppData } from '../models/App/AppData'
Expand Down Expand Up @@ -209,12 +211,26 @@ export class IPCServer

return this._getGroupOfRundown(rundown, arg.groupId)
}
private getGroupByExternalId(arg: { rundownId: string; externalId: string }): { rundown: Rundown; group: Group } {
const { rundown } = this.getRundown(arg)

return this._getGroupOfRundownByExternalId(rundown, arg.externalId)
}
private _getGroupOfRundown(rundown: Rundown, groupId: string): { rundown: Rundown; group: Group } {
const group = findGroup(rundown, groupId)
if (!group) throw new Error(`Group ${groupId} not found in rundown "${rundown.id}" ("${rundown.name}").`)

return { rundown, group }
}
private _getGroupOfRundownByExternalId(rundown: Rundown, externalId: string): { rundown: Rundown; group: Group } {
const group = findGroupByExternalId(rundown, externalId)
if (!group)
throw new Error(
`Group with external ID ${externalId} not found in rundown "${rundown.id}" ("${rundown.name}").`
)

return { rundown, group }
}

public getPart(arg: { rundownId: string; groupId: string; partId: string }): {
rundown: Rundown
Expand All @@ -228,6 +244,19 @@ export class IPCServer
return { rundown, group, part }
}

public getPartByExternalId(arg: { rundownId: string; groupId: string; externalId: string }): {
rundown: Rundown
group: Group
part: Part
} {
const { rundown, group } = this.getGroup(arg)
const part = findPartInGroupByExternalId(group, arg.externalId)
if (!part)
throw new Error(`Part with external ID ${arg.externalId} not found in group ${group.id} ("${group.name}").`)

return { rundown, group, part }
}

async undo(): Promise<void> {
const action = this.undoLedger[this.undoPointer]
try {
Expand Down Expand Up @@ -826,6 +855,88 @@ export class IPCServer
description: ActionDescription.UpdatePart,
}
}
async upsertPart(arg: {
rundownId: string
groupId: string
partId: string | undefined
part: Partial<Part>
}): Promise<UndoableResult<void> | undefined> {
const { group } = this.getGroup(arg)
let existingPart: Part | undefined
if (arg.partId) {
try {
const result = this.getPart({ rundownId: arg.rundownId, groupId: arg.groupId, partId: arg.partId })
existingPart = result.part
} catch (_error) {
// Discard error.
}
}

let partIdToUpdate = arg.partId
let newPartResult:
| UndoableResult<{
partId: string
groupId?: string | undefined
}>
| undefined

if (!existingPart) {
newPartResult = await this.newPart({
rundownId: arg.rundownId,
groupId: arg.groupId,
name: arg.part.name ?? `Part #${group.parts.length + 1}`,
})
partIdToUpdate = newPartResult?.result?.partId
}

if (!partIdToUpdate) {
throw new Error('Failed to upsert part because no partId was provided, located, or generated.')
}

const updatePartResult = await this.updatePart({
rundownId: arg.rundownId,
groupId: arg.groupId,
partId: partIdToUpdate,
part: {
...arg.part,
id: partIdToUpdate,
},
})

return {
undo: async () => {
if (updatePartResult) {
await updatePartResult.undo()
}

if (newPartResult) {
await newPartResult.undo()
}
},
description: ActionDescription.UpsertPart,
}
}
async upsertPartByExternalId(arg: {
rundownId: string
groupId: string
externalId: string
part: Partial<Part>
}): Promise<UndoableResult<void> | undefined> {
let partId: string | undefined
try {
const { part } = this.getPartByExternalId(arg)
partId = part.id
} catch (error) {
// Discard error.
}

return this.upsertPart({
rundownId: arg.rundownId,
groupId: arg.groupId,
partId,
part: arg.part,
})
}
async newGroup(arg: { rundownId: string; name: string }): Promise<UndoableResult<string>> {
const newGroup: Group = {
...getDefaultGroup(),
Expand Down Expand Up @@ -987,6 +1098,115 @@ export class IPCServer
description: ActionDescription.UpdateGroup,
}
}
async upsertGroup(arg: {
rundownId: string
groupId: string | undefined
group: PartialDeep<Group>
useExternalIdForParts?: boolean
}): Promise<UndoableResult<void> | undefined> {
const { rundown } = this.getRundown(arg)
let existingGroup: Group | undefined
if (arg.groupId) {
try {
const result = this.getGroup({ rundownId: arg.rundownId, groupId: arg.groupId })
existingGroup = result.group
} catch (_error) {
// Discard error.
}
}

let groupIdToUpdate = arg.groupId
let newGroupResult: UndoableResult<string> | undefined
if (!existingGroup) {
newGroupResult = await this.newGroup({
rundownId: arg.rundownId,
name: arg.group.name ?? `Group #${rundown.groups.length + 1}`,
})
groupIdToUpdate = newGroupResult.result
}

if (!groupIdToUpdate) {
throw new Error('Failed to upsert group because no groupId was provided, located, or generated.')
}

const updateGroupResult = await this.updateGroup({
rundownId: arg.rundownId,
groupId: groupIdToUpdate,
group: {
...omit(arg.group, 'parts'), // We'll update the parts next.
id: groupIdToUpdate,
},
})

const upsertPartResults: UndoableResult<void>[] = []
for (const part of arg.group.parts ?? []) {
let result: UndoableResult<void> | undefined
if (arg.useExternalIdForParts) {
if (!part.externalId) {
throw new Error(`Part named "${part.name}" does not have an external ID`)
}

result = await this.upsertPartByExternalId({
rundownId: arg.rundownId,
groupId: groupIdToUpdate,
externalId: part.externalId,
part,
})
} else {
if (!part.id) {
throw new Error(`Part named "${part.name}" does not have an ID`)
}

result = await this.upsertPart({
rundownId: arg.rundownId,
groupId: groupIdToUpdate,
partId: part.id,
part,
})
}

if (result) {
upsertPartResults.push(result)
}
}

return {
undo: async () => {
for (const result of upsertPartResults) {
await result.undo()
}

if (updateGroupResult) {
await updateGroupResult.undo()
}

if (newGroupResult) {
await newGroupResult.undo()
}
},
description: ActionDescription.UpsertGroup,
}
}
async upsertGroupByExternalId(arg: {
rundownId: string
externalId: string
group: PartialDeep<Group>
}): Promise<UndoableResult<void> | undefined> {
let groupId: string | undefined
try {
const { group } = this.getGroupByExternalId(arg)
groupId = group.id
} catch (error) {
// Ignore error.
}

return this.upsertGroup({
rundownId: arg.rundownId,
groupId,
group: arg.group,
useExternalIdForParts: true,
})
}
async deletePart(arg: {
rundownId: string
groupId: string
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/ipc/IPCAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export const enum ActionDescription {
AssignAreaToGroup = 'Assign Area to Group',
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
SetApplicationTrigger = 'Assign trigger',
UpsertGroup = 'upsert group',
UpsertPart = 'upsert part',
}

export type UndoFunction = () => Promise<void> | void
Expand Down
Loading

0 comments on commit 0d8177f

Please sign in to comment.