Skip to content

Commit 7c907cc

Browse files
authored
feat: Server-side notifications SOFIE-152 (#20)
1 parent 734eb98 commit 7c907cc

File tree

62 files changed

+2103
-240
lines changed

Some content is hidden

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

62 files changed

+2103
-240
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ import {
4646
PackageContainerStatuses,
4747
TimelineDatastore,
4848
SofieIngestDataCache,
49+
Notifications,
4950
} from '../../collections'
5051
import { Collections } from '../../collections/lib'
5152
import { generateTranslationBundleOriginId } from '../translationsBundles'
5253
import { CollectionCleanupResult } from '@sofie-automation/meteor-lib/dist/api/system'
54+
import { DBNotificationTargetType } from '@sofie-automation/corelib/dist/dataModel/Notifications'
5355

5456
describe('Cleanup', () => {
5557
let env: DefaultEnvironment
@@ -446,6 +448,21 @@ async function setDefaultDatatoDB(env: DefaultEnvironment, now: number) {
446448
type: '' as any,
447449
})
448450

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

meteor/server/api/cleanup.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,12 @@ import {
7070
Workers,
7171
WorkerThreadStatuses,
7272
SofieIngestDataCache,
73+
Notifications,
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
@@ -449,6 +451,34 @@ export async function cleanupOldDataInner(actuallyCleanup = false): Promise<Coll
449451
addToResult(getCollectionKey(WorkerThreadStatuses), 0)
450452
}
451453

454+
// Notifications
455+
{
456+
const rundownIds = await getAllIdsInCollection(Rundowns)
457+
const playlistIds = await getAllIdsInCollection(RundownPlaylists)
458+
await removeByQuery(Notifications, {
459+
studioId: { $nin: studioIds },
460+
$or: [
461+
// {
462+
// 'relatedTo.type': DBNotificationTargetType.EVERYWHERE,
463+
// },
464+
{
465+
'relatedTo.type': DBNotificationTargetType.PLAYLIST,
466+
'relatedTo.playlistId': { $nin: playlistIds },
467+
},
468+
{
469+
'relatedTo.type': {
470+
$in: [
471+
DBNotificationTargetType.RUNDOWN,
472+
DBNotificationTargetType.PARTINSTANCE,
473+
DBNotificationTargetType.PIECEINSTANCE,
474+
],
475+
},
476+
'relatedTo.rundownId': { $nin: rundownIds },
477+
},
478+
],
479+
})
480+
}
481+
452482
return result
453483
}
454484
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/lib/quickLoop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { MarkerPosition, compareMarkerPositions } from '@sofie-automation/coreli
99
import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString'
1010
import { DEFAULT_FALLBACK_PART_DURATION } from '@sofie-automation/shared-lib/dist/core/constants'
1111
import { getCurrentTime } from '../../lib/lib'
12-
import { generateTranslation } from '@sofie-automation/meteor-lib/dist/lib'
12+
import { generateTranslation } from '@sofie-automation/corelib/dist/lib'
1313
import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio'
1414
import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance'
1515
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'

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
@@ -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

packages/corelib/src/dataModel/Notes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export interface GenericNote extends INoteBase {
2424
name: string
2525
}
2626
}
27+
export interface RundownPlaylistNote extends INoteBase {
28+
origin: {
29+
name: string
30+
}
31+
}
2732
export interface RundownNote extends INoteBase {
2833
origin: {
2934
name: string
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+
}

0 commit comments

Comments
 (0)