Skip to content

Commit d9fafe0

Browse files
committed
feat: Server-side notifications Sofie-Automation#1193
1 parent c5b833d commit d9fafe0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1944
-200
lines changed

meteor/server/api/__tests__/cleanup.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,12 @@ import {
4545
TranslationsBundles,
4646
PackageContainerStatuses,
4747
TimelineDatastore,
48+
Notifications,
4849
} from '../../collections'
4950
import { Collections } from '../../collections/lib'
5051
import { generateTranslationBundleOriginId } from '../translationsBundles'
5152
import { CollectionCleanupResult } from '@sofie-automation/meteor-lib/dist/api/system'
53+
import { DBNotificationTargetType } from '@sofie-automation/corelib/dist/dataModel/Notifications'
5254

5355
describe('Cleanup', () => {
5456
let env: DefaultEnvironment
@@ -438,6 +440,21 @@ async function setDefaultDatatoDB(env: DefaultEnvironment, now: number) {
438440
type: '' as any,
439441
})
440442

443+
await Notifications.insertAsync({
444+
_id: getRandomId(),
445+
category: '',
446+
created: now,
447+
localId: '',
448+
message: {} as any,
449+
severity: 0 as any,
450+
modified: now,
451+
relatedTo: {
452+
type: DBNotificationTargetType.RUNDOWN,
453+
studioId,
454+
rundownId,
455+
},
456+
})
457+
441458
// Ensure that we have added one of everything:
442459
for (const [collectionName, collection] of Collections.entries()) {
443460
if (

meteor/server/api/cleanup.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,12 @@ import {
6969
UserActionsLog,
7070
Workers,
7171
WorkerThreadStatuses,
72+
Notifications,
7273
} from '../collections'
7374
import { AsyncOnlyMongoCollection, AsyncOnlyReadOnlyMongoCollection } from '../collections/collection'
7475
import { getCollectionKey } from '../collections/lib'
7576
import { generateTranslationBundleOriginId } from './translationsBundles'
77+
import { DBNotificationTargetType } from '@sofie-automation/corelib/dist/dataModel/Notifications'
7678

7779
/**
7880
* If actuallyCleanup=true, cleans up old data. Otherwise just checks what old data there is
@@ -447,6 +449,34 @@ export async function cleanupOldDataInner(actuallyCleanup = false): Promise<Coll
447449
addToResult(getCollectionKey(WorkerThreadStatuses), 0)
448450
}
449451

452+
// Notifications
453+
{
454+
const rundownIds = await getAllIdsInCollection(Rundowns)
455+
const playlistIds = await getAllIdsInCollection(RundownPlaylists)
456+
await removeByQuery(Notifications, {
457+
studioId: { $nin: studioIds },
458+
$or: [
459+
// {
460+
// 'relatedTo.type': DBNotificationTargetType.EVERYWHERE,
461+
// },
462+
{
463+
'relatedTo.type': DBNotificationTargetType.PLAYLIST,
464+
'relatedTo.playlistId': { $nin: playlistIds },
465+
},
466+
{
467+
'relatedTo.type': {
468+
$in: [
469+
DBNotificationTargetType.RUNDOWN,
470+
DBNotificationTargetType.PARTINSTANCE,
471+
DBNotificationTargetType.PIECEINSTANCE,
472+
],
473+
},
474+
'relatedTo.rundownId': { $nin: rundownIds },
475+
},
476+
],
477+
})
478+
}
479+
450480
return result
451481
}
452482
async function isAllowedToRunCleanup(): Promise<string | void> {

meteor/server/api/studio/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ExpectedPackageWorkStatuses,
1212
ExternalMessageQueue,
1313
MediaObjects,
14+
Notifications,
1415
PackageContainerPackageStatuses,
1516
PackageInfos,
1617
PeripheralDevices,
@@ -102,6 +103,7 @@ async function removeStudio(context: MethodContext, studioId: StudioId): Promise
102103
ExpectedPackageWorkStatuses.removeAsync({ studioId: studio._id }),
103104
PackageInfos.removeAsync({ studioId: studio._id }),
104105
PackageContainerPackageStatuses.removeAsync({ studioId: studio._id }),
106+
Notifications.removeAsync({ 'relatedTo.studioId': studio._id }),
105107
])
106108
}
107109

meteor/server/collections/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
allowAccessToStudio,
4747
} from '../security/lib/security'
4848
import { SystemWriteAccess } from '../security/system'
49+
import type { DBNotificationObj } from '@sofie-automation/corelib/dist/dataModel/Notifications'
4950

5051
export * from './bucket'
5152
export * from './packages-media'
@@ -102,6 +103,26 @@ registerIndex(ExternalMessageQueue, {
102103
rundownId: 1,
103104
})
104105

106+
export const Notifications = createAsyncOnlyMongoCollection<DBNotificationObj>(CollectionName.Notifications, false)
107+
// For NotificationsModelHelper.getAllNotifications
108+
registerIndex(Notifications, {
109+
// @ts-expect-error nested property
110+
'relatedTo.studioId': 1,
111+
catgory: 1,
112+
})
113+
// For MeteorPubSub.notificationsForRundownPlaylist
114+
registerIndex(Notifications, {
115+
// @ts-expect-error nested property
116+
'relatedTo.studioId': 1,
117+
'relatedTo.playlistId': 1,
118+
})
119+
// For MeteorPubSub.notificationsForRundown
120+
registerIndex(Notifications, {
121+
// @ts-expect-error nested property
122+
'relatedTo.studioId': 1,
123+
'relatedTo.rundownId': 1,
124+
})
125+
105126
export const Organizations = createAsyncOnlyMongoCollection<DBOrganization>(CollectionName.Organizations, {
106127
async update(userId, doc, fields, _modifier) {
107128
const access = await allowAccessToOrganization({ userId: userId }, doc._id)

meteor/server/publications/system.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { meteorPublish } from './lib'
33
import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub'
44
import { SystemReadAccess } from '../security/system'
55
import { OrganizationReadAccess } from '../security/organization'
6-
import { CoreSystem, Users } from '../collections'
6+
import { CoreSystem, Notifications, Users } from '../collections'
77
import { SYSTEM_ID } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem'
8-
import { OrganizationId } from '@sofie-automation/corelib/dist/dataModel/Ids'
8+
import { OrganizationId, RundownId, RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids'
9+
import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/lib/securityVerify'
10+
import { check } from 'meteor/check'
911

1012
meteorPublish(MeteorPubSub.coreSystem, async function (token: string | undefined) {
1113
if (await SystemReadAccess.coreSystem({ userId: this.userId, token })) {
@@ -74,3 +76,34 @@ meteorPublish(
7476
return null
7577
}
7678
)
79+
80+
meteorPublish(MeteorPubSub.notificationsForRundown, async function (studioId: StudioId, rundownId: RundownId) {
81+
// HACK: This should do real auth
82+
triggerWriteAccessBecauseNoCheckNecessary()
83+
84+
check(studioId, String)
85+
check(rundownId, String)
86+
87+
return Notifications.findWithCursor({
88+
// Loosely match any notifications related to this rundown
89+
'relatedTo.studioId': studioId,
90+
'relatedTo.rundownId': rundownId,
91+
})
92+
})
93+
94+
meteorPublish(
95+
MeteorPubSub.notificationsForRundownPlaylist,
96+
async function (studioId: StudioId, playlistId: RundownPlaylistId) {
97+
// HACK: This should do real auth
98+
triggerWriteAccessBecauseNoCheckNecessary()
99+
100+
check(studioId, String)
101+
check(playlistId, String)
102+
103+
return Notifications.findWithCursor({
104+
// Loosely match any notifications related to this playlist
105+
'relatedTo.studioId': studioId,
106+
'relatedTo.playlistId': playlistId,
107+
})
108+
}
109+
)

packages/corelib/src/dataModel/Collections.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export enum CollectionName {
1717
MediaObjects = 'mediaObjects',
1818
MediaWorkFlows = 'mediaWorkFlows',
1919
MediaWorkFlowSteps = 'mediaWorkFlowSteps',
20+
Notifications = 'notifications',
2021
Organizations = 'organizations',
2122
PartInstances = 'partInstances',
2223
PackageInfos = 'packageInfos',

packages/corelib/src/dataModel/Ids.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export type ExternalMessageQueueObjId = ProtectedString<'ExternalMessageQueueObj
3838
/** A string, identifying a IngestDataCacheObj */
3939
export type IngestDataCacheObjId = ProtectedString<'IngestDataCacheObjId'>
4040

41+
/** A string, identifying a DBNotificationObj */
42+
export type NotificationId = ProtectedString<'NotificationId'>
43+
4144
/** A string, identifying a Organization */
4245
export type OrganizationId = ProtectedString<'OrganizationId'>
4346

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import type { NoteSeverity } from '@sofie-automation/blueprints-integration'
2+
import type { NotificationId, PartInstanceId, PieceInstanceId, RundownId, RundownPlaylistId, StudioId } from './Ids'
3+
import type { ITranslatableMessage } from '../TranslatableMessage'
4+
5+
/**
6+
* This describes a notification that should be shown to a user
7+
* These can come from various sources, and are added and removed dynamically during system usage
8+
*/
9+
export interface DBNotificationObj {
10+
_id: NotificationId
11+
12+
/**
13+
* Used to group a certain group of notifications
14+
* Each source of these notifications should use its own value, so that it can find and cleanup after itself when appropriate
15+
* Typically, a method will clear all previous notifications for a category when it is called, and then possibly add new ones
16+
* This is a technical value, not intended to be conusmed outside of the generation/update logic
17+
*/
18+
category: string
19+
20+
/**
21+
* Unique id for this notification within the category
22+
*/
23+
localId: string
24+
25+
severity: NoteSeverity
26+
message: ITranslatableMessage
27+
// type: 'event' | 'persistent'
28+
29+
/** Description of what the notification is related to */
30+
relatedTo: DBNotificationTarget
31+
32+
created: number // unix timestamp
33+
modified: number // unix timestamp
34+
35+
// /**
36+
// * When set, the notification will be automatically dismissed after this time
37+
// * For events, this is typically set to less than a minute
38+
// * For persistent notifications, this is never set
39+
// */
40+
// autoTimeout?: number // unix timestamp
41+
}
42+
43+
export type DBNotificationTarget =
44+
// | DBNotificationTargetEverywhere
45+
// | DBNotificationTargetStudio
46+
| DBNotificationTargetRundown
47+
// | DBNotificationTargetSegment
48+
// | DBNotificationTargetPart
49+
// | DBNotificationTargetPiece
50+
| DBNotificationTargetRundownPlaylist
51+
| DBNotificationTargetPartInstance
52+
| DBNotificationTargetPieceInstance
53+
54+
export enum DBNotificationTargetType {
55+
// EVERYWHERE = 'everywhere',
56+
// STUDIO = 'studio',
57+
RUNDOWN = 'rundown',
58+
// SEGMENT = 'segment',
59+
// PART = 'part',
60+
// PIECE = 'piece',
61+
PLAYLIST = 'playlist',
62+
PARTINSTANCE = 'partInstance',
63+
PIECEINSTANCE = 'pieceInstance',
64+
}
65+
66+
// export interface DBNotificationTargetEverywhere {
67+
// type: DBNotificationTargetType.EVERYWHERE
68+
// }
69+
70+
// export interface DBNotificationTargetStudio {
71+
// type: DBNotificationTargetType.STUDIO
72+
// studioId: StudioId
73+
// }
74+
75+
export interface DBNotificationTargetRundown {
76+
type: DBNotificationTargetType.RUNDOWN
77+
studioId: StudioId
78+
rundownId: RundownId
79+
}
80+
81+
// export interface DBNotificationTargetSegment {
82+
// type: DBNotificationTargetType.SEGMENT
83+
// studioId: StudioId
84+
// rundownId: RundownId
85+
// segmentId: SegmentId
86+
// }
87+
88+
// export interface DBNotificationTargetPart {
89+
// type: DBNotificationTargetType.PART
90+
// studioId: StudioId
91+
// rundownId: RundownId
92+
// // segmentId: SegmentId
93+
// partId: PartId
94+
// }
95+
96+
// export interface DBNotificationTargetPiece {
97+
// type: DBNotificationTargetType.PIECE
98+
// studioId: StudioId
99+
// rundownId: RundownId
100+
// // segmentId: SegmentId
101+
// partId: PartId
102+
// pieceId: PieceId
103+
// }
104+
105+
export interface DBNotificationTargetRundownPlaylist {
106+
type: DBNotificationTargetType.PLAYLIST
107+
studioId: StudioId
108+
playlistId: RundownPlaylistId
109+
}
110+
111+
export interface DBNotificationTargetPartInstance {
112+
type: DBNotificationTargetType.PARTINSTANCE
113+
studioId: StudioId
114+
rundownId: RundownId
115+
partInstanceId: PartInstanceId
116+
}
117+
118+
export interface DBNotificationTargetPieceInstance {
119+
type: DBNotificationTargetType.PIECEINSTANCE
120+
studioId: StudioId
121+
rundownId: RundownId
122+
partInstanceId: PartInstanceId
123+
pieceInstanceId: PieceInstanceId
124+
}

packages/corelib/src/dataModel/Rundown.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export interface Rundown {
5959
/** Last sent storyStatus to ingestDevice (MOS) */
6060
notifiedCurrentPlayingPartExternalId?: string
6161

62-
/** Holds notes (warnings / errors) thrown by the blueprints during creation, or appended after */
62+
/** Holds notes (warnings / errors) thrown by the blueprints during creation */
6363
notes?: Array<RundownNote>
6464

6565
externalId: string

packages/job-worker/src/__mocks__/collection.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataMod
4141
import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue'
4242
import { MediaObjects } from '@sofie-automation/corelib/dist/dataModel/MediaObjects'
4343
import { PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos'
44+
import type { DBNotificationObj } from '@sofie-automation/corelib/dist/dataModel/Notifications'
4445

4546
export interface CollectionOperation {
4647
type: string
@@ -284,6 +285,7 @@ export function getMockCollections(): {
284285
ExpectedMediaItems: new MockMongoCollection(CollectionName.ExpectedMediaItems),
285286
ExpectedPlayoutItems: new MockMongoCollection<ExpectedPlayoutItem>(CollectionName.ExpectedPlayoutItems),
286287
IngestDataCache: new MockMongoCollection<IngestDataCacheObj>(CollectionName.IngestDataCache),
288+
Notifications: new MockMongoCollection<DBNotificationObj>(CollectionName.Notifications),
287289
Parts: new MockMongoCollection<DBPart>(CollectionName.Parts),
288290
PartInstances: new MockMongoCollection<DBPartInstance>(CollectionName.PartInstances),
289291
PeripheralDevices: new MockMongoCollection<PeripheralDevice>(CollectionName.PeripheralDevices),
@@ -340,6 +342,7 @@ export interface IMockCollections {
340342
ExpectedMediaItems: MockMongoCollection<ExpectedMediaItem>
341343
ExpectedPlayoutItems: MockMongoCollection<ExpectedPlayoutItem>
342344
IngestDataCache: MockMongoCollection<IngestDataCacheObj>
345+
Notifications: MockMongoCollection<DBNotificationObj>
343346
Parts: MockMongoCollection<DBPart>
344347
PartInstances: MockMongoCollection<DBPartInstance>
345348
PeripheralDevices: MockMongoCollection<PeripheralDevice>

0 commit comments

Comments
 (0)