Skip to content

Commit 31d72ff

Browse files
authored
Merge pull request #1303 from bbc/upstream/server-side-notifications
feat: Server-side notifications #1193
2 parents 34d3f91 + b628877 commit 31d72ff

Some content is hidden

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

47 files changed

+1915
-172
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ import {
4444
TranslationsBundles,
4545
PackageContainerStatuses,
4646
TimelineDatastore,
47+
Notifications,
4748
SofieIngestDataCache,
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
@@ -445,6 +447,21 @@ async function setDefaultDatatoDB(env: DefaultEnvironment, now: number) {
445447
type: '' as any,
446448
})
447449

450+
await Notifications.insertAsync({
451+
_id: getRandomId(),
452+
category: '',
453+
created: now,
454+
localId: '',
455+
message: {} as any,
456+
severity: 0 as any,
457+
modified: now,
458+
relatedTo: {
459+
type: DBNotificationTargetType.RUNDOWN,
460+
studioId,
461+
rundownId,
462+
},
463+
})
464+
448465
// Ensure that we have added one of everything:
449466
for (const [collectionName, collection] of Collections.entries()) {
450467
if (

meteor/server/api/cleanup.ts

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

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

450+
// Notifications
451+
{
452+
const rundownIds = await getAllIdsInCollection(Rundowns)
453+
const playlistIds = await getAllIdsInCollection(RundownPlaylists)
454+
await removeByQuery(Notifications, {
455+
studioId: { $nin: studioIds },
456+
$or: [
457+
// {
458+
// 'relatedTo.type': DBNotificationTargetType.EVERYWHERE,
459+
// },
460+
{
461+
'relatedTo.type': DBNotificationTargetType.PLAYLIST,
462+
'relatedTo.playlistId': { $nin: playlistIds },
463+
},
464+
{
465+
'relatedTo.type': {
466+
$in: [
467+
DBNotificationTargetType.RUNDOWN,
468+
DBNotificationTargetType.PARTINSTANCE,
469+
DBNotificationTargetType.PIECEINSTANCE,
470+
],
471+
},
472+
'relatedTo.rundownId': { $nin: rundownIds },
473+
},
474+
],
475+
})
476+
}
477+
448478
return result
449479
}
450480
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,
@@ -113,6 +114,7 @@ async function removeStudio(context: MethodContext, studioId: StudioId): Promise
113114
ExpectedPackageWorkStatuses.removeAsync({ studioId: studio._id }),
114115
PackageInfos.removeAsync({ studioId: studio._id }),
115116
PackageContainerPackageStatuses.removeAsync({ studioId: studio._id }),
117+
Notifications.removeAsync({ 'relatedTo.studioId': studio._id }),
116118
])
117119
}
118120

meteor/server/collections/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { ObserveChangesForHash } from './lib'
3333
import { logger } from '../logging'
3434
import { allowOnlyFields, rejectFields } from '../security/allowDeny'
3535
import { checkUserIdHasOneOfPermissions } from '../security/auth'
36+
import { DBNotificationObj } from '@sofie-automation/corelib/dist/dataModel/Notifications'
3637

3738
export * from './bucket'
3839
export * from './packages-media'
@@ -88,6 +89,26 @@ registerIndex(ExternalMessageQueue, {
8889
rundownId: 1,
8990
})
9091

92+
export const Notifications = createAsyncOnlyMongoCollection<DBNotificationObj>(CollectionName.Notifications, false)
93+
// For NotificationsModelHelper.getAllNotifications
94+
registerIndex(Notifications, {
95+
// @ts-expect-error nested property
96+
'relatedTo.studioId': 1,
97+
catgory: 1,
98+
})
99+
// For MeteorPubSub.notificationsForRundownPlaylist
100+
registerIndex(Notifications, {
101+
// @ts-expect-error nested property
102+
'relatedTo.studioId': 1,
103+
'relatedTo.playlistId': 1,
104+
})
105+
// For MeteorPubSub.notificationsForRundown
106+
registerIndex(Notifications, {
107+
// @ts-expect-error nested property
108+
'relatedTo.studioId': 1,
109+
'relatedTo.rundownId': 1,
110+
})
111+
91112
export const Organizations = createAsyncOnlyMongoCollection<DBOrganization>(CollectionName.Organizations, {
92113
async update(userId, doc, fields, _modifier) {
93114
if (!checkUserIdHasOneOfPermissions(userId, CollectionName.Organizations, 'configure')) return false

meteor/server/publications/system.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { meteorPublish } from './lib/lib'
22
import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub'
3-
import { CoreSystem } from '../collections'
3+
import { CoreSystem, Notifications } from '../collections'
4+
import { RundownId, RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids'
5+
import { check } from 'meteor/check'
46
import { SYSTEM_ID } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem'
57
import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify'
68

@@ -22,3 +24,34 @@ meteorPublish(MeteorPubSub.coreSystem, async function (_token: string | undefine
2224
},
2325
})
2426
})
27+
28+
meteorPublish(MeteorPubSub.notificationsForRundown, async function (studioId: StudioId, rundownId: RundownId) {
29+
// HACK: This should do real auth
30+
triggerWriteAccessBecauseNoCheckNecessary()
31+
32+
check(studioId, String)
33+
check(rundownId, String)
34+
35+
return Notifications.findWithCursor({
36+
// Loosely match any notifications related to this rundown
37+
'relatedTo.studioId': studioId,
38+
'relatedTo.rundownId': rundownId,
39+
})
40+
})
41+
42+
meteorPublish(
43+
MeteorPubSub.notificationsForRundownPlaylist,
44+
async function (studioId: StudioId, playlistId: RundownPlaylistId) {
45+
// HACK: This should do real auth
46+
triggerWriteAccessBecauseNoCheckNecessary()
47+
48+
check(studioId, String)
49+
check(playlistId, String)
50+
51+
return Notifications.findWithCursor({
52+
// Loosely match any notifications related to this playlist
53+
'relatedTo.studioId': studioId,
54+
'relatedTo.playlistId': playlistId,
55+
})
56+
}
57+
)

packages/corelib/src/dataModel/Collections.ts

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

packages/corelib/src/dataModel/Ids.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export type NrcsIngestDataCacheObjId = ProtectedString<'NrcsIngestDataCacheObjId
4141
/** A string, identifying a SofieIngestDataCacheObj */
4242
export type SofieIngestDataCacheObjId = ProtectedString<'SofieIngestDataCacheObjId'>
4343

44+
/** A string, identifying a DBNotificationObj */
45+
export type NotificationId = ProtectedString<'NotificationId'>
46+
4447
/** A string, identifying a Organization */
4548
export type OrganizationId = ProtectedString<'OrganizationId'>
4649

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
@@ -60,7 +60,7 @@ export interface Rundown {
6060
/** Last sent storyStatus to ingestDevice (MOS) */
6161
notifiedCurrentPlayingPartExternalId?: string
6262

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

6666
externalId: string

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

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

4647
export interface CollectionOperation {
4748
type: string
@@ -284,6 +285,7 @@ export function getMockCollections(): {
284285
BucketAdLibPieces: new MockMongoCollection<BucketAdLib>(CollectionName.BucketAdLibPieces),
285286
ExpectedMediaItems: new MockMongoCollection(CollectionName.ExpectedMediaItems),
286287
ExpectedPlayoutItems: new MockMongoCollection<ExpectedPlayoutItem>(CollectionName.ExpectedPlayoutItems),
288+
Notifications: new MockMongoCollection<DBNotificationObj>(CollectionName.Notifications),
287289
SofieIngestDataCache: new MockMongoCollection<SofieIngestDataCacheObj>(CollectionName.SofieIngestDataCache),
288290
NrcsIngestDataCache: new MockMongoCollection<NrcsIngestDataCacheObj>(CollectionName.NrcsIngestDataCache),
289291
Parts: new MockMongoCollection<DBPart>(CollectionName.Parts),
@@ -341,6 +343,7 @@ export interface IMockCollections {
341343
BucketAdLibPieces: MockMongoCollection<BucketAdLib>
342344
ExpectedMediaItems: MockMongoCollection<ExpectedMediaItem>
343345
ExpectedPlayoutItems: MockMongoCollection<ExpectedPlayoutItem>
346+
Notifications: MockMongoCollection<DBNotificationObj>
344347
SofieIngestDataCache: MockMongoCollection<SofieIngestDataCacheObj>
345348
NrcsIngestDataCache: MockMongoCollection<NrcsIngestDataCacheObj>
346349
Parts: MockMongoCollection<DBPart>

0 commit comments

Comments
 (0)