From d62acab2cd2a8cb2100d8d02877c12e5a25c4d69 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 24 Jun 2026 11:11:03 +0000 Subject: [PATCH 1/4] feat: tighten and unify MongoModifier types This makes it match what the driver and our mock supports, and removes some meteor specifics --- meteor/server/api/blueprints/api.ts | 2 +- meteor/server/api/rest/v1/showstyles.ts | 6 +- meteor/server/api/rest/v1/studios.ts | 4 +- meteor/server/api/rundownLayouts.ts | 3 +- meteor/server/api/translationsBundles.ts | 20 +- meteor/server/collections/collection.ts | 9 + .../implementations/asyncCollection.ts | 21 ++ meteor/server/migration/lib.ts | 2 +- packages/corelib/src/__tests__/mongo.spec.ts | 188 +++++++++++++++++- packages/corelib/src/mongo.ts | 99 ++++++++- .../job-worker/src/__mocks__/collection.ts | 4 +- .../src/__tests__/rundownPlaylist.test.ts | 2 +- packages/job-worker/src/db/collections.ts | 6 +- .../__tests__/LoadPlayoutModel.spec.ts | 12 +- 14 files changed, 341 insertions(+), 37 deletions(-) diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index a02c027868b..2bf241bb9dd 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -248,7 +248,7 @@ async function innerUploadBlueprint( parseVersion(blueprintManifest.integrationVersion) parseVersion(blueprintManifest.TSRVersion) - await Blueprints.upsertAsync(newBlueprint._id, newBlueprint) + await Blueprints.replaceAsync(newBlueprint) // check for translations on the manifest and store them if they exist if ( diff --git a/meteor/server/api/rest/v1/showstyles.ts b/meteor/server/api/rest/v1/showstyles.ts index 08a98690230..e7dfb50a6bb 100644 --- a/meteor/server/api/rest/v1/showstyles.ts +++ b/meteor/server/api/rest/v1/showstyles.ts @@ -107,7 +107,7 @@ class ShowStylesServerAPI implements ShowStylesRestAPI { } } - await ShowStyleBases.upsertAsync(showStyleBaseId, showStyle) + await ShowStyleBases.replaceAsync(showStyle) // wait for the upsert to complete before validation and upgrade read from the showStyleBases collection await new Promise((resolve) => setTimeout(() => resolve(), 200)) @@ -169,7 +169,7 @@ class ShowStylesServerAPI implements ShowStylesRestAPI { const showStyle = await showStyleBaseFrom(apiShowStyleBase, showStyleBaseId) if (!showStyle) throw new Meteor.Error(400, `Invalid ShowStyleBase`) - await ShowStyleBases.upsertAsync(showStyleBaseId, showStyle) + await ShowStyleBases.replaceAsync(showStyle) // wait for the upsert to complete before validation and upgrade read from the showStyleBases collection await new Promise((resolve) => setTimeout(() => resolve(), 200)) @@ -298,7 +298,7 @@ class ShowStylesServerAPI implements ShowStylesRestAPI { } } - await ShowStyleVariants.upsertAsync(showStyleVariantId, showStyle) + await ShowStyleVariants.replaceAsync(showStyle) return ClientAPI.responseSuccess(undefined, 200) } diff --git a/meteor/server/api/rest/v1/studios.ts b/meteor/server/api/rest/v1/studios.ts index c4325e13e73..8614d3ce075 100644 --- a/meteor/server/api/rest/v1/studios.ts +++ b/meteor/server/api/rest/v1/studios.ts @@ -104,7 +104,7 @@ class StudiosServerAPI implements StudiosRestAPI { } } - await Studios.upsertAsync(studioId, newStudio) + await Studios.replaceAsync(newStudio) const validation = await validateConfigForStudio(studioId) checkValidation(`addOrUpdateStudio ${studioId}`, validation.messages) @@ -148,7 +148,7 @@ class StudiosServerAPI implements StudiosRestAPI { const newStudio = await studioFrom(apiStudio, studioId) if (!newStudio) throw new Meteor.Error(400, `Invalid Studio`) - await Studios.upsertAsync(studioId, newStudio) + await Studios.replaceAsync(newStudio) const validation = await validateConfigForStudio(studioId) checkValidation(`updateStudioConfig ${studioId}`, validation.messages) diff --git a/meteor/server/api/rundownLayouts.ts b/meteor/server/api/rundownLayouts.ts index c75570804da..9b5d494266f 100644 --- a/meteor/server/api/rundownLayouts.ts +++ b/meteor/server/api/rundownLayouts.ts @@ -84,9 +84,10 @@ shelfLayoutsRouter.post( check(layout.name, String) check(layout.type, String) + layout._id = layout._id || getRandomId() layout.showStyleBaseId = showStyleBase._id - await RundownLayouts.upsertAsync(layout._id, layout) + await RundownLayouts.replaceAsync(layout) ctx.response.status = 200 ctx.body = '' diff --git a/meteor/server/api/translationsBundles.ts b/meteor/server/api/translationsBundles.ts index fc11e6ee320..1fc21ea8a82 100644 --- a/meteor/server/api/translationsBundles.ts +++ b/meteor/server/api/translationsBundles.ts @@ -39,19 +39,15 @@ export async function upsertBundles( // originating blueprint and language const _id = createBundleId(originId, language) - await TranslationsBundleCollection.upsertAsync( + await TranslationsBundleCollection.replaceAsync({ _id, - { - _id, - originId, - type, - namespace: unprotectString(originId), - language, - data: fromI18NextData(data), - hash: getHash(JSON.stringify(data)), - }, - { multi: false } - ) + originId, + type, + namespace: unprotectString(originId), + language, + data: fromI18NextData(data), + hash: getHash(JSON.stringify(data)), + }) } } diff --git a/meteor/server/collections/collection.ts b/meteor/server/collections/collection.ts index b677cf37ec0..d8167f99453 100644 --- a/meteor/server/collections/collection.ts +++ b/meteor/server/collections/collection.ts @@ -167,6 +167,15 @@ export interface AsyncOnlyMongoCollection< */ upsertManyAsync(doc: DBInterface[]): Promise<{ numberAffected: number; insertedIds: DBInterface['_id'][] }> + /** + * Replace a single document with a full document, matched by its `_id` (upserting if not present). + * Unlike {@link updateAsync}/{@link upsertAsync}, which apply atomic-operator modifiers, this replaces + * the entire stored document (clearing fields absent from `doc`), via the native driver's `replaceOne`. + * @param doc The full document to store + * @returns `true` if an existing document was replaced, `false` if a new one was inserted + */ + replaceAsync(doc: DBInterface): Promise + /** * Remove one or more documents * @param selector A query describing the documents to be deleted diff --git a/meteor/server/collections/implementations/asyncCollection.ts b/meteor/server/collections/implementations/asyncCollection.ts index 1b92d0e9f92..c4e7be7771e 100644 --- a/meteor/server/collections/implementations/asyncCollection.ts +++ b/meteor/server/collections/implementations/asyncCollection.ts @@ -313,6 +313,27 @@ export class WrappedAsyncMongoCollection< return result } + public async replaceAsync(doc: DBInterface): Promise { + const span = profiler.startSpan(`MongoCollection.${this.name}.replace`) + if (span) { + span.addLabels({ + collection: this.name, + id: unprotectString(doc._id), + }) + } + try { + // Unlike updateAsync/upsertAsync (which take atomic-operator modifiers), this performs a + // full-document replacement by `_id` (with upsert). + const result = await this._collection.upsertAsync(doc._id as any, doc as any) + if (span) span.end() + // numberAffected counts the matched (replaced) document; insertedId is only set on insert + return !result.insertedId + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } + } + async bulkWriteAsync(ops: Array>): Promise { const span = profiler.startSpan(`MongoCollection.${this.name}.bulkWrite`) if (span) { diff --git a/meteor/server/migration/lib.ts b/meteor/server/migration/lib.ts index c12a6b978d3..9e6c288d3ac 100644 --- a/meteor/server/migration/lib.ts +++ b/meteor/server/migration/lib.ts @@ -174,7 +174,7 @@ export function renamePropertiesInCollection { const rawDocs = ['1', '2', '3', '4', '5', '6', '7'].map((s) => ({ _id: protectString(s) })) @@ -223,3 +223,189 @@ test('mongoWhere', () => { expect(findFetch({ rank: { $lt: 3 } })).toHaveLength(3) expect(findFetch({ rank: { $lte: 3 } })).toHaveLength(4) }) + +describe('mongoModify', () => { + interface Doc { + _id: ProtectedString + name: string + rank: number + nested: { a: number; b?: number } + values: number[] + objs: Array<{ id: string; kind: string }> + } + const makeDoc = (): Doc => ({ + _id: protectString('id0'), + name: 'original', + rank: 1, + nested: { a: 1, b: 2 }, + values: [1, 2, 3], + objs: [ + { id: 'x', kind: 'apple' }, + { id: 'y', kind: 'banana' }, + { id: 'z', kind: 'apple' }, + ], + }) + const selector: MongoQuery = { _id: protectString('id0') } + const modify = (doc: Doc, modifier: any): Doc => mongoModify(selector, doc, modifier) + + describe('$set', () => { + test('sets a top-level field', () => { + const doc = makeDoc() + modify(doc, { $set: { name: 'changed' } }) + expect(doc.name).toBe('changed') + }) + test('sets a nested field via dotted path', () => { + const doc = makeDoc() + modify(doc, { $set: { 'nested.a': 99 } }) + expect(doc.nested).toEqual({ a: 99, b: 2 }) + }) + test('creates intermediate objects for a missing dotted path', () => { + const doc: any = makeDoc() + modify(doc, { $set: { 'deep.newly.created': 5 } }) + expect(doc.deep).toEqual({ newly: { created: 5 } }) + }) + }) + + describe('$unset', () => { + test('removes a top-level field', () => { + const doc = makeDoc() + modify(doc, { $unset: { name: 1 } }) + expect('name' in doc).toBe(false) + }) + test('removes a nested field', () => { + const doc = makeDoc() + modify(doc, { $unset: { 'nested.b': 1 } }) + expect(doc.nested).toEqual({ a: 1 }) + }) + }) + + describe('$push', () => { + test('appends a value', () => { + const doc = makeDoc() + modify(doc, { $push: { values: 4 } }) + expect(doc.values).toEqual([1, 2, 3, 4]) + }) + test('appends a duplicate (unlike $addToSet)', () => { + const doc = makeDoc() + modify(doc, { $push: { values: 2 } }) + expect(doc.values).toEqual([1, 2, 3, 2]) + }) + test('appends multiple values with $each', () => { + const doc = makeDoc() + modify(doc, { $push: { values: { $each: [4, 5] } } }) + expect(doc.values).toEqual([1, 2, 3, 4, 5]) + }) + test('creates the array when missing', () => { + const doc: any = makeDoc() + modify(doc, { $push: { newArr: 1 } }) + expect(doc.newArr).toEqual([1]) + }) + }) + + describe('$pull', () => { + test('removes elements equal to a primitive value', () => { + const doc = makeDoc() + modify(doc, { $pull: { values: 2 } }) + expect(doc.values).toEqual([1, 3]) + }) + test('does not remove everything when pulling a primitive that is absent', () => { + const doc = makeDoc() + modify(doc, { $pull: { values: 99 } }) + expect(doc.values).toEqual([1, 2, 3]) + }) + test('removes elements matching a sub-document query', () => { + const doc = makeDoc() + modify(doc, { $pull: { objs: { kind: 'apple' } } }) + expect(doc.objs).toEqual([{ id: 'y', kind: 'banana' }]) + }) + test('removes elements via top-level $in', () => { + const doc = makeDoc() + modify(doc, { $pull: { values: { $in: [1, 3] } } }) + expect(doc.values).toEqual([2]) + }) + test('removes embedded documents via $in using deep equality', () => { + const doc = makeDoc() + modify(doc, { + $pull: { + objs: { + $in: [ + { id: 'x', kind: 'apple' }, + { id: 'z', kind: 'apple' }, + ], + }, + }, + }) + expect(doc.objs).toEqual([{ id: 'y', kind: 'banana' }]) + }) + test('removes elements via a nested operator query', () => { + const doc = makeDoc() + modify(doc, { $pull: { objs: { id: { $in: ['x', 'z'] } } } }) + expect(doc.objs).toEqual([{ id: 'y', kind: 'banana' }]) + }) + }) + + describe('$addToSet', () => { + test('adds a new value', () => { + const doc = makeDoc() + modify(doc, { $addToSet: { values: 4 } }) + expect(doc.values).toEqual([1, 2, 3, 4]) + }) + test('does not add an existing primitive value', () => { + const doc = makeDoc() + modify(doc, { $addToSet: { values: 2 } }) + expect(doc.values).toEqual([1, 2, 3]) + }) + test('adds multiple values with $each, skipping duplicates', () => { + const doc = makeDoc() + modify(doc, { $addToSet: { values: { $each: [3, 4, 5] } } }) + expect(doc.values).toEqual([1, 2, 3, 4, 5]) + }) + test('uses deep equality for object members', () => { + const doc = makeDoc() + modify(doc, { $addToSet: { objs: { id: 'x', kind: 'apple' } } }) + expect(doc.objs).toHaveLength(3) + + modify(doc, { $addToSet: { objs: { id: 'w', kind: 'cherry' } } }) + expect(doc.objs).toContainEqual({ id: 'w', kind: 'cherry' }) + }) + test('creates the array when missing', () => { + const doc: any = makeDoc() + modify(doc, { $addToSet: { newArr: { $each: [1, 2] } } }) + expect(doc.newArr).toEqual([1, 2]) + }) + }) + + describe('$rename', () => { + test('renames a top-level field', () => { + const doc: any = makeDoc() + modify(doc, { $rename: { name: 'title' } }) + expect('name' in doc).toBe(false) + expect(doc.title).toBe('original') + }) + }) + + describe('full-document replace', () => { + test('replaces the doc when the modifier has no operators, keeping the _id', () => { + const doc = makeDoc() + const result = mongoModify(selector, doc, { name: 'fresh', rank: 5 } as any) + expect(result.name).toBe('fresh') + expect(result.rank).toBe(5) + expect(result._id).toEqual(protectString('id0')) + }) + }) + + describe('unsupported operators', () => { + test('throws for an operator that is not implemented', () => { + const doc = makeDoc() + expect(() => modify(doc, { $inc: { rank: 1 } })).toThrow(/not implemented/) + }) + }) + + test('applies multiple operators in a single modifier', () => { + const doc = makeDoc() + modify(doc, { $set: { name: 'multi' }, $push: { values: 4 }, $unset: { rank: 1 } }) + expect(doc.name).toBe('multi') + expect(doc.values).toEqual([1, 2, 3, 4]) + expect('rank' in doc).toBe(false) + }) +}) diff --git a/packages/corelib/src/mongo.ts b/packages/corelib/src/mongo.ts index aebfa7fb97b..5417075abd3 100644 --- a/packages/corelib/src/mongo.ts +++ b/packages/corelib/src/mongo.ts @@ -1,7 +1,15 @@ import _ from 'underscore' import { ProtectedString } from './protectedString.js' import * as objectPath from 'object-path' -import type { Condition, Filter, UpdateFilter } from 'mongodb' +import type { + Condition, + Filter, + MatchKeysAndValues, + OnlyFieldsOfType, + PullOperator, + PushOperator, + SetFields, +} from 'mongodb' import { clone } from './lib.js' /** Hack's using typings pulled from meteor */ @@ -59,7 +67,28 @@ export interface ObserveChangesOptions { * */ export type MongoQuery = Filter export type MongoQueryKey = RegExp | T | Condition // Allowed properties in a MongoQuery -export type MongoModifier = UpdateFilter + +/** + * The simplified update operators we support. This is intentionally a hand-rolled subset of mongodb's + * `UpdateFilter` rather than `UpdateFilter` itself: + * + * - `UpdateFilter` ends in `& Document` (a `[key: string]: any` index signature) which lets a whole + * plain document pass as a "modifier"; the native driver then rejects it at runtime ("Update document + * requires atomic operators"). + * - it documents exactly which operators are supported, and keeps the type in lockstep with the + * in-memory `mongoModify` below (used by the unit-test mock). + * + * The per-operator value types are mongodb's own (loose, non-recursive) helpers, so this neither trips + * "excessively deep" instantiation like `StrictUpdateFilter`, nor breaks dynamic `$set[key] = value` use. + */ +export type MongoModifier = { + $set?: MatchKeysAndValues + $unset?: OnlyFieldsOfType + $push?: PushOperator + $pull?: PullOperator + $addToSet?: SetFields + $rename?: Record +} /** End of hacks */ @@ -339,6 +368,10 @@ export function mongoModify }>( _.each(value, (value: any, key: string) => { pullFromPath(doc, key, value) }) + } else if (key === '$addToSet') { + _.each(value, (value: any, key: string) => { + addToSetOntoPath(doc, key, value) + }) } else if (key === '$rename') { _.each(value, (value: any, key: string) => { renamePath(doc, key, value) @@ -458,9 +491,14 @@ export function mutatePath( * Push a value into a object, and ensure the array exists * @param obj Object * @param path Path to array in object - * @param valueToPush Value to push onto array + * @param valueToPush Value to push onto array, or a `{ $each: [...] }` wrapper to push multiple values */ export function pushOntoPath(obj: Record, path: string, valueToPush: T): void { + const valuesToPush = + valueToPush && typeof valueToPush === 'object' && '$each' in valueToPush && Array.isArray(valueToPush.$each) + ? (valueToPush.$each as unknown[]) + : [valueToPush] + const mutator = (o: Record, lastAttr: string) => { if (!_.has(o, lastAttr)) { o[lastAttr] = [] @@ -472,7 +510,41 @@ export function pushOntoPath(obj: Record, path: string, valu } const arr: any = o[lastAttr] - arr.push(valueToPush) + arr.push(...valuesToPush) + return arr + } + mutatePath(obj, path, {}, mutator) +} +/** + * Add one or more values into an array, ensuring the array exists and that values are not duplicated + * (mirroring MongoDB's `$addToSet`). The value may be a single value, or a `{ $each: [...] }` wrapper to + * add multiple values at once. + * @param obj Object + * @param path Path to array in object + * @param valueToAdd Value (or `{ $each: [...] }`) to add to the array + */ +export function addToSetOntoPath(obj: Record, path: string, valueToAdd: T): void { + const valuesToAdd = + valueToAdd && typeof valueToAdd === 'object' && '$each' in valueToAdd && Array.isArray(valueToAdd.$each) + ? (valueToAdd.$each as unknown[]) + : [valueToAdd] + + const mutator = (o: Record, lastAttr: string) => { + if (!_.has(o, lastAttr)) { + o[lastAttr] = [] + } else { + if (!_.isArray(o[lastAttr])) + throw new Error( + 'Object propery "' + lastAttr + '" is not an array ("' + o[lastAttr] + '") (in path "' + path + '")' + ) + } + const arr: any[] = o[lastAttr] as any[] + + for (const value of valuesToAdd) { + if (!arr.some((entry) => _.isEqual(entry, value))) { + arr.push(value) + } + } return arr } mutatePath(obj, path, {}, mutator) @@ -481,7 +553,11 @@ export function pushOntoPath(obj: Record, path: string, valu * Push a value from a object, when the value matches * @param obj Object * @param path Path to array in object - * @param matchValue Value to match for removal. Supports $in operator for matching multiple values. + * @param matchValue Value to match for removal. This mirrors MongoDB's `$pull`: + * - a `{ $in: [...] }` operator removes elements equal to one of the listed values + * - any other object is treated as a query against each element (supporting nested operators such as + * `{ field: { $in: [...] } }`, evaluated by {@link mongoWhere}) + * - a primitive removes elements equal to it */ export function pullFromPath(obj: Record, path: string, matchValue: T): void { const mutator = (o: Record, lastAttr: string) => { @@ -492,7 +568,7 @@ export function pullFromPath(obj: Record, path: string, matc 'Object propery "' + lastAttr + '" is not an array ("' + arrAttr + '") (in path "' + path + '")' ) - // Handle $in operator for matching multiple values + // Handle $in operator for matching multiple values against the array elements directly if ( matchValue && typeof matchValue === 'object' && @@ -500,10 +576,17 @@ export function pullFromPath(obj: Record, path: string, matc Array.isArray((matchValue as Record).$in) ) { const inValues = (matchValue as Record).$in as unknown[] - return (o[lastAttr] = arrAttr.filter((entry: T) => !inValues.includes(entry))) + return (o[lastAttr] = arrAttr.filter((entry: T) => !inValues.some((value) => _.isEqual(entry, value)))) + } + + // An object is treated as a query against each element (handles plain sub-document matches as + // well as nested operators like `{ field: { $in: [...] } }`) + if (matchValue && typeof matchValue === 'object') { + return (o[lastAttr] = arrAttr.filter((entry: T) => !mongoWhere(entry as any, matchValue as any))) } - return (o[lastAttr] = arrAttr.filter((entry: T) => !_.isMatch(entry, matchValue))) + // A primitive removes elements equal to it + return (o[lastAttr] = arrAttr.filter((entry: T) => !_.isEqual(entry, matchValue))) } else { return undefined } diff --git a/packages/job-worker/src/__mocks__/collection.ts b/packages/job-worker/src/__mocks__/collection.ts index d8a0b086ef6..5a02eb3d015 100644 --- a/packages/job-worker/src/__mocks__/collection.ts +++ b/packages/job-worker/src/__mocks__/collection.ts @@ -236,9 +236,9 @@ export class MockMongoCollection }> imp for (const op of ops) { if ('updateMany' in op) { - await this.updateInner(op.updateMany.filter, op.updateMany.update, false) + await this.updateInner(op.updateMany.filter, op.updateMany.update as MongoModifier, false) } else if ('updateOne' in op) { - await this.updateInner(op.updateOne.filter, op.updateOne.update, true) + await this.updateInner(op.updateOne.filter, op.updateOne.update as MongoModifier, true) } else if ('replaceOne' in op) { await this.replace(op.replaceOne.replacement as any) } else if ('insertOne' in op) { diff --git a/packages/job-worker/src/__tests__/rundownPlaylist.test.ts b/packages/job-worker/src/__tests__/rundownPlaylist.test.ts index 7c3282afbb1..c7d70f88534 100644 --- a/packages/job-worker/src/__tests__/rundownPlaylist.test.ts +++ b/packages/job-worker/src/__tests__/rundownPlaylist.test.ts @@ -85,7 +85,7 @@ describe('Rundown', () => { playlist0.externalId, allRundowns ) - await context.mockCollections.RundownPlaylists.update(playlist0._id, rundownPlaylist) + await context.mockCollections.RundownPlaylists.replace(rundownPlaylist) expect(rundownPlaylist.rundownIdsInOrder).toEqual(['rundown00', 'rundown01', 'rundown02']) const getRundownIDs = async (id: RundownPlaylistId) => { diff --git a/packages/job-worker/src/db/collections.ts b/packages/job-worker/src/db/collections.ts index b531a80a6b4..b4b8c810c09 100644 --- a/packages/job-worker/src/db/collections.ts +++ b/packages/job-worker/src/db/collections.ts @@ -4,7 +4,6 @@ import { AnyBulkWriteOperation, Filter, FindOptions, - UpdateFilter, Collection as MongoCollection, ChangeStreamDocument, CountOptions, @@ -38,6 +37,7 @@ import { DBTimelineDatastoreEntry } from '@sofie-automation/corelib/dist/dataMod import { ExpectedPackageDB } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' +import type { MongoModifier as CorelibMongoModifier } from '@sofie-automation/corelib/dist/mongo' import { literal } from '@sofie-automation/corelib/dist/lib' import { ReadonlyDeep } from 'type-fest' import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' @@ -46,7 +46,9 @@ import type { DBNotificationObj } from '@sofie-automation/corelib/dist/dataModel import type { EventEmitter } from 'events' export type MongoQuery = Filter -export type MongoModifier = UpdateFilter +// Aliased from corelib so the job-worker and meteor layers share a single modifier type that stays in +// lockstep with `mongoModify` (the in-memory implementation used by the unit-test mocks). +export type MongoModifier = CorelibMongoModifier export interface IReadOnlyCollection }> { readonly name: string diff --git a/packages/job-worker/src/playout/model/implementation/__tests__/LoadPlayoutModel.spec.ts b/packages/job-worker/src/playout/model/implementation/__tests__/LoadPlayoutModel.spec.ts index aefb364bc34..7f994ec38fb 100644 --- a/packages/job-worker/src/playout/model/implementation/__tests__/LoadPlayoutModel.spec.ts +++ b/packages/job-worker/src/playout/model/implementation/__tests__/LoadPlayoutModel.spec.ts @@ -59,7 +59,9 @@ describe('LoadPlayoutModel', () => { const rundownIdsInOrder = [rundownId01, rundownId02, rundownId00] await context.mockCollections.RundownPlaylists.update(playlistId0, { - rundownIdsInOrder, + $set: { + rundownIdsInOrder, + }, }) await runWithPlaylistLock(context, playlistId0, async (lock) => { @@ -88,7 +90,9 @@ describe('LoadPlayoutModel', () => { const rundownIdsInOrder = [rundownId01] await context.mockCollections.RundownPlaylists.update(playlistId0, { - rundownIdsInOrder, + $set: { + rundownIdsInOrder, + }, }) await runWithPlaylistLock(context, playlistId0, async (lock) => { @@ -129,7 +133,9 @@ describe('LoadPlayoutModel', () => { const rundownIdsInOrder = [rundownId01, rundownId02, rundownId00] await context.mockCollections.RundownPlaylists.update(playlistId0, { - rundownIdsInOrder, + $set: { + rundownIdsInOrder, + }, }) const playlist0 = await context.mockCollections.RundownPlaylists.findOne(playlistId0) From ab0b2916667178d3c08d4d09ba6d92c68effe2d9 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 24 Jun 2026 11:20:11 +0000 Subject: [PATCH 2/4] feat: replace upsertAsync and upsertManyAsync from meteor Their semantics are confusing and make it hard to ensure that complete documents are inserted --- .../api/integration/expectedPackages.ts | 53 +++++++------------ .../server/api/integration/media-scanner.ts | 10 ++-- meteor/server/api/triggeredActions.ts | 4 +- meteor/server/collections/collection.ts | 27 ---------- .../implementations/asyncCollection.ts | 47 ---------------- 5 files changed, 27 insertions(+), 114 deletions(-) diff --git a/meteor/server/api/integration/expectedPackages.ts b/meteor/server/api/integration/expectedPackages.ts index f6d99d242cf..a6d6552feba 100644 --- a/meteor/server/api/integration/expectedPackages.ts +++ b/meteor/server/api/integration/expectedPackages.ts @@ -4,20 +4,14 @@ import { MethodContext } from '../methodContext' import { checkAccessAndGetPeripheralDevice } from '../../security/check' import { ExpectedPackageStatusAPI, PackageInfo } from '@sofie-automation/blueprints-integration' import { ExpectedPackageWorkStatus } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackageWorkStatuses' -import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' +import { assertNever } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { getCurrentTime } from '../../lib/lib' -import { - getPackageContainerPackageId, - PackageContainerPackageStatusDB, -} from '@sofie-automation/corelib/dist/dataModel/PackageContainerPackageStatus' +import { getPackageContainerPackageId } from '@sofie-automation/corelib/dist/dataModel/PackageContainerPackageStatus' import { getPackageInfoId, PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' import type { AnyBulkWriteOperation } from 'mongodb' import { onUpdatedPackageInfo } from '../ingest/packageInfo' -import { - getPackageContainerId, - PackageContainerStatusDB, -} from '@sofie-automation/corelib/dist/dataModel/PackageContainerStatus' +import { getPackageContainerId } from '@sofie-automation/corelib/dist/dataModel/PackageContainerStatus' import { ExpectedPackageId, ExpectedPackageWorkStatusId, @@ -220,16 +214,14 @@ export namespace PackageManagerIntegration { // The PackageContainerStatus doesn't exist // Create it on the fly: - await PackageContainerPackageStatuses.upsertAsync(id, { - $set: literal({ - _id: id, - studioId: studioId, - containerId: change.containerId, - deviceId: peripheralDevice._id, - packageId: protectString(change.packageId), - status: change.status, - modified: getCurrentTime(), - }), + await PackageContainerPackageStatuses.replaceAsync({ + _id: id, + studioId: studioId, + containerId: change.containerId, + deviceId: peripheralDevice._id, + packageId: protectString(change.packageId), + status: change.status, + modified: getCurrentTime(), }) } }) @@ -308,15 +300,13 @@ export namespace PackageManagerIntegration { // The PackageContainerStatus doesn't exist // Create it on the fly: - await PackageContainerStatuses.upsertAsync(id, { - $set: literal({ - _id: id, - studioId: studioId, - containerId: change.containerId, - deviceId: peripheralDevice._id, - status: change.status, - modified: getCurrentTime(), - }), + await PackageContainerStatuses.replaceAsync({ + _id: id, + studioId: studioId, + containerId: change.containerId, + deviceId: peripheralDevice._id, + status: change.status, + modified: getCurrentTime(), }) } }) @@ -416,12 +406,7 @@ export namespace PackageManagerIntegration { type: type, payload: payload, } - await PackageInfos.upsertAsync(id, { - $set: doc, - $unset: { - removeTime: 1, - }, - }) + await PackageInfos.replaceAsync(doc) await onUpdatedPackageInfo(packageId, doc) } diff --git a/meteor/server/api/integration/media-scanner.ts b/meteor/server/api/integration/media-scanner.ts index bcd8cd4eaaa..8312ca44a4c 100644 --- a/meteor/server/api/integration/media-scanner.ts +++ b/meteor/server/api/integration/media-scanner.ts @@ -55,6 +55,8 @@ export namespace MediaScannerIntegration { // logger.debug('updateMediaObject') const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, deviceToken, context) const studioId = await getStudioIdFromDevice(peripheralDevice) + if (!studioId) + throw new Meteor.Error(400, 'updateMediaObject: Device "' + peripheralDevice._id + '" has no studio') const _id: MediaObjId = protectString(collectionId + '_' + objId) if (doc === null) { @@ -63,16 +65,14 @@ export namespace MediaScannerIntegration { if (doc.mediaId !== doc.mediaId.toUpperCase()) throw new Meteor.Error(400, 'mediaId must only use uppercase characters') - const doc2 = { + // logger.debug(doc2) + await MediaObjects.replaceAsync({ ...doc, studioId: studioId, collectionId: collectionId, objId: objId, _id: _id, - } - - // logger.debug(doc2) - await MediaObjects.upsertAsync(_id, { $set: doc2 }) + }) } else { throw new Meteor.Error(400, 'missing doc argument') } diff --git a/meteor/server/api/triggeredActions.ts b/meteor/server/api/triggeredActions.ts index ed64ca075a9..2261d98fe77 100644 --- a/meteor/server/api/triggeredActions.ts +++ b/meteor/server/api/triggeredActions.ts @@ -118,7 +118,9 @@ actionTriggersRouter.post( // TODO - should we clear `blueprintUniqueId`, to avoid blueprints getting them confused with data they own? - await TriggeredActions.upsertManyAsync(triggeredActions) + await Promise.all( + triggeredActions.map((triggeredActionsObj) => TriggeredActions.replaceAsync(triggeredActionsObj)) + ) ctx.response.status = 200 ctx.body = '' diff --git a/meteor/server/collections/collection.ts b/meteor/server/collections/collection.ts index d8167f99453..8f095ac1ebc 100644 --- a/meteor/server/collections/collection.ts +++ b/meteor/server/collections/collection.ts @@ -16,7 +16,6 @@ import { ObserveCallbacks, ObserveChangesCallbacks, UpdateOptions, - UpsertOptions, } from '@sofie-automation/meteor-lib/dist/collections/lib' import { MinimalMongoCursor } from './implementations/asyncCollection' import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' @@ -143,34 +142,8 @@ export interface AsyncOnlyMongoCollection< options: UpdateOptions & Required> ): Promise - /** - * Perform an update/insert of a document - * @param selector A query describing the documents to update. Typically this will be an id - * @param modifier The operation to apply to each matching document - * @param options Options for the operation - */ - upsertAsync( - selector: DBInterface['_id'] | { _id: DBInterface['_id'] }, - modifier: MongoModifier, - options?: UpsertOptions - ): Promise<{ numberAffected?: number; insertedId?: DBInterface['_id'] }> - upsertAsync( - selector: MongoQuery, - modifier: MongoModifier, - // Require { multi } to be set when selecting multiple documents to be updated, otherwise only the first found document will be updated - options: UpdateOptions & Required> - ): Promise<{ numberAffected?: number; insertedId?: DBInterface['_id'] }> - - /** - * Perform an upsert for multiple documents, based on the `_id` of each document - * @param documents Documents to upsert - */ - upsertManyAsync(doc: DBInterface[]): Promise<{ numberAffected: number; insertedIds: DBInterface['_id'][] }> - /** * Replace a single document with a full document, matched by its `_id` (upserting if not present). - * Unlike {@link updateAsync}/{@link upsertAsync}, which apply atomic-operator modifiers, this replaces - * the entire stored document (clearing fields absent from `doc`), via the native driver's `replaceOne`. * @param doc The full document to store * @returns `true` if an existing document was replaced, `false` if a new one was inserted */ diff --git a/meteor/server/collections/implementations/asyncCollection.ts b/meteor/server/collections/implementations/asyncCollection.ts index c4e7be7771e..7bcd00267f1 100644 --- a/meteor/server/collections/implementations/asyncCollection.ts +++ b/meteor/server/collections/implementations/asyncCollection.ts @@ -4,7 +4,6 @@ import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' import { UpdateOptions, - UpsertOptions, IndexSpecifier, MongoCursor, FindOptions, @@ -266,52 +265,6 @@ export class WrappedAsyncMongoCollection< this.wrapMongoError(e) } } - public async upsertAsync( - selector: MongoQuery | DBInterface['_id'] | { _id: DBInterface['_id'] }, - modifier: MongoModifier, - options?: UpsertOptions - ): Promise<{ - numberAffected?: number - insertedId?: DBInterface['_id'] - }> { - const span = profiler.startSpan(`MongoCollection.${this.name}.upsert`) - if (span) { - span.addLabels({ - collection: this.name, - query: JSON.stringify(selector), - }) - } - try { - const result = await this._collection.upsertAsync(selector as any, modifier as any, options) - if (span) span.end() - return { - numberAffected: result.numberAffected, - insertedId: protectString(result.insertedId), - } - } catch (e) { - if (span) span.end() - this.wrapMongoError(e) - } - } - - async upsertManyAsync(docs: DBInterface[]): Promise<{ numberAffected: number; insertedIds: DBInterface['_id'][] }> { - const result: { - numberAffected: number - insertedIds: DBInterface['_id'][] - } = { - numberAffected: 0, - insertedIds: [], - } - await Promise.all( - docs.map(async (doc) => - this.upsertAsync(doc._id, { $set: doc }).then((r) => { - if (r.numberAffected) result.numberAffected += r.numberAffected - if (r.insertedId) result.insertedIds.push(r.insertedId) - }) - ) - ) - return result - } public async replaceAsync(doc: DBInterface): Promise { const span = profiler.startSpan(`MongoCollection.${this.name}.replace`) From 993cdd573dc8f7da6b6842c9ba61ada0ac60d344 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 24 Jun 2026 11:28:00 +0000 Subject: [PATCH 3/4] feat: rework mongo bulkWrite typings, to be an abstracted subset This matches the flow of other operations, and ensures the types match our mock --- .../api/integration/expectedPackages.ts | 4 +-- meteor/server/api/triggeredActions.ts | 10 ++++++-- meteor/server/collections/collection.ts | 12 ++++++--- .../implementations/asyncCollection.ts | 11 +++++--- .../collections/implementations/mock.ts | 6 ++--- meteor/server/lib/database.ts | 5 ++-- meteor/server/migration/upgrades/lib.ts | 4 +-- packages/corelib/src/mongo.ts | 25 +++++++++++++++++++ .../job-worker/src/__mocks__/collection.ts | 5 ++-- packages/job-worker/src/db/changes.ts | 5 ++-- packages/job-worker/src/db/collection.ts | 6 ++--- packages/job-worker/src/db/collections.ts | 9 ++++--- packages/job-worker/src/ingest/commit.ts | 4 +-- .../job-worker/src/ingest/expectedPackages.ts | 4 +-- .../implementation/DocumentChangeTracker.ts | 8 +++--- .../model/implementation/SaveIngestModel.ts | 4 +-- .../job-worker/src/ingest/nrcsIngestCache.ts | 4 +-- .../job-worker/src/ingest/sofieIngestCache.ts | 4 +-- .../notifications/NotificationsModelHelper.ts | 4 +-- .../src/playout/expectedPackages.ts | 4 +-- .../model/implementation/SavePlayoutModel.ts | 10 ++++---- .../playout/timings/timelineTriggerTime.ts | 4 +-- ...updatePartInstanceRanksAndOrphanedState.ts | 4 +-- 23 files changed, 100 insertions(+), 56 deletions(-) diff --git a/meteor/server/api/integration/expectedPackages.ts b/meteor/server/api/integration/expectedPackages.ts index a6d6552feba..14909178b92 100644 --- a/meteor/server/api/integration/expectedPackages.ts +++ b/meteor/server/api/integration/expectedPackages.ts @@ -9,7 +9,7 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { getCurrentTime } from '../../lib/lib' import { getPackageContainerPackageId } from '@sofie-automation/corelib/dist/dataModel/PackageContainerPackageStatus' import { getPackageInfoId, PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' -import type { AnyBulkWriteOperation } from 'mongodb' +import type { MongoBulkWriteOperation } from '@sofie-automation/corelib/dist/mongo' import { onUpdatedPackageInfo } from '../ingest/packageInfo' import { getPackageContainerId } from '@sofie-automation/corelib/dist/dataModel/PackageContainerStatus' import { @@ -58,7 +58,7 @@ export namespace PackageManagerIntegration { if (!peripheralDevice.studioAndConfigId) throw new Meteor.Error(400, 'Device "' + peripheralDevice._id + '" has no studio') - const bulkChanges: AnyBulkWriteOperation[] = [] + const bulkChanges: MongoBulkWriteOperation[] = [] const removedIds: ExpectedPackageWorkStatusId[] = [] const ps: Promise[] = [] diff --git a/meteor/server/api/triggeredActions.ts b/meteor/server/api/triggeredActions.ts index 2261d98fe77..4c9828dbe1b 100644 --- a/meteor/server/api/triggeredActions.ts +++ b/meteor/server/api/triggeredActions.ts @@ -118,8 +118,14 @@ actionTriggersRouter.post( // TODO - should we clear `blueprintUniqueId`, to avoid blueprints getting them confused with data they own? - await Promise.all( - triggeredActions.map((triggeredActionsObj) => TriggeredActions.replaceAsync(triggeredActionsObj)) + await TriggeredActions.bulkWriteAsync( + triggeredActions.map((triggeredActionsObj) => ({ + replaceOne: { + filter: { _id: triggeredActionsObj._id }, + replacement: triggeredActionsObj, + upsert: true, + }, + })) ) ctx.response.status = 200 diff --git a/meteor/server/collections/collection.ts b/meteor/server/collections/collection.ts index 8f095ac1ebc..28723a55473 100644 --- a/meteor/server/collections/collection.ts +++ b/meteor/server/collections/collection.ts @@ -1,10 +1,16 @@ -import { FindOptions, MongoModifier, MongoQuery, ObserveChangesOptions } from '@sofie-automation/corelib/dist/mongo' +import { + FindOptions, + MongoBulkWriteOperation, + MongoModifier, + MongoQuery, + ObserveChangesOptions, +} from '@sofie-automation/corelib/dist/mongo' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' import { NpmModuleMongodb } from 'meteor/npm-mongo' import { PromisifyCallbacks } from '@sofie-automation/shared-lib/dist/lib/types' -import type { AnyBulkWriteOperation, Collection as RawCollection } from 'mongodb' +import type { Collection as RawCollection } from 'mongodb' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { registerCollection } from './lib' import { WrappedMockCollection } from './implementations/mock' @@ -160,7 +166,7 @@ export interface AsyncOnlyMongoCollection< * This should be used instead of Promise.all(...) when doing multiple updates, as it is more performant * @param ops Operations to perform */ - bulkWriteAsync(ops: Array>): Promise + bulkWriteAsync(ops: Array>): Promise } /** diff --git a/meteor/server/collections/implementations/asyncCollection.ts b/meteor/server/collections/implementations/asyncCollection.ts index 7bcd00267f1..9025c7dc766 100644 --- a/meteor/server/collections/implementations/asyncCollection.ts +++ b/meteor/server/collections/implementations/asyncCollection.ts @@ -1,4 +1,9 @@ -import { MongoModifier, MongoQuery, ObserveChangesOptions } from '@sofie-automation/corelib/dist/mongo' +import { + MongoBulkWriteOperation, + MongoModifier, + MongoQuery, + ObserveChangesOptions, +} from '@sofie-automation/corelib/dist/mongo' import { ProtectedString, protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' @@ -287,7 +292,7 @@ export class WrappedAsyncMongoCollection< } } - async bulkWriteAsync(ops: Array>): Promise { + async bulkWriteAsync(ops: Array>): Promise { const span = profiler.startSpan(`MongoCollection.${this.name}.bulkWrite`) if (span) { span.addLabels({ @@ -298,7 +303,7 @@ export class WrappedAsyncMongoCollection< if (ops.length > 0) { const rawCollection = this.rawCollection() - const bulkWriteResult = await rawCollection.bulkWrite(ops, { + const bulkWriteResult = await rawCollection.bulkWrite(ops as AnyBulkWriteOperation[], { ordered: false, }) diff --git a/meteor/server/collections/implementations/mock.ts b/meteor/server/collections/implementations/mock.ts index af5536967a9..f2682049ab0 100644 --- a/meteor/server/collections/implementations/mock.ts +++ b/meteor/server/collections/implementations/mock.ts @@ -1,4 +1,4 @@ -import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { MongoBulkWriteOperation, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' import { FindOptions, MongoCursor } from '@sofie-automation/meteor-lib/dist/collections/lib' @@ -37,10 +37,10 @@ export class WrappedMockCollection>): Promise { + override async bulkWriteAsync(ops: Array>): Promise { if (ops.length > 0) { const rawCollection = this.rawCollection() - const bulkWriteResult = await rawCollection.bulkWrite(ops, { + const bulkWriteResult = await rawCollection.bulkWrite(ops as AnyBulkWriteOperation[], { ordered: false, }) if (bulkWriteResult && bulkWriteResult.hasWriteErrors()) { diff --git a/meteor/server/lib/database.ts b/meteor/server/lib/database.ts index ee8391c727b..fe1fd286bce 100644 --- a/meteor/server/lib/database.ts +++ b/meteor/server/lib/database.ts @@ -1,9 +1,8 @@ import { Meteor } from 'meteor/meteor' -import type { AnyBulkWriteOperation } from 'mongodb' import _ from 'underscore' import { normalizeArrayToMap, deleteAllUndefinedProperties } from '@sofie-automation/corelib/dist/lib' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' -import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { MongoBulkWriteOperation, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { profiler } from '../api/profiler' import { AsyncOnlyMongoCollection } from '../collections/collection' @@ -118,7 +117,7 @@ async function savePreparedChanges( newObjIds.add(id) } - const updates: AnyBulkWriteOperation[] = [] + const updates: MongoBulkWriteOperation[] = [] const removedDocs: DBInterface['_id'][] = [] _.each(preparedChanges.changed || [], (oUpdate) => { diff --git a/meteor/server/migration/upgrades/lib.ts b/meteor/server/migration/upgrades/lib.ts index 8da4dacf89f..1df701d6954 100644 --- a/meteor/server/migration/upgrades/lib.ts +++ b/meteor/server/migration/upgrades/lib.ts @@ -2,7 +2,7 @@ import type { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corel import { TriggeredActions } from '../../collections' import { Complete, getRandomId, literal, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' import type { DBTriggeredActions } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' -import type { AnyBulkWriteOperation } from 'mongodb' +import type { MongoBulkWriteOperation } from '@sofie-automation/corelib/dist/mongo' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import type { IBlueprintTriggeredActions } from '@sofie-automation/blueprints-integration' @@ -17,7 +17,7 @@ export async function updateTriggeredActionsForShowStyleBaseId( const oldTriggeredActions = normalizeArrayToMap(oldTriggeredActionsArray, 'blueprintUniqueId') const newDocIds: TriggeredActionId[] = [] - const bulkOps: AnyBulkWriteOperation[] = [] + const bulkOps: MongoBulkWriteOperation[] = [] for (const newTriggeredAction of triggeredActions) { const oldValue = oldTriggeredActions.get(newTriggeredAction._id) diff --git a/packages/corelib/src/mongo.ts b/packages/corelib/src/mongo.ts index 5417075abd3..1fad305acbd 100644 --- a/packages/corelib/src/mongo.ts +++ b/packages/corelib/src/mongo.ts @@ -3,12 +3,19 @@ import { ProtectedString } from './protectedString.js' import * as objectPath from 'object-path' import type { Condition, + DeleteManyModel, + DeleteOneModel, + Document, Filter, + InsertOneModel, MatchKeysAndValues, OnlyFieldsOfType, PullOperator, PushOperator, + ReplaceOneModel, SetFields, + UpdateManyModel, + UpdateOneModel, } from 'mongodb' import { clone } from './lib.js' @@ -90,6 +97,24 @@ export type MongoModifier = { $rename?: Record } +/** + * A bulkWrite operation, hand-rolled to match exactly the arms our in-memory mocks implement (see the + * `bulkWrite` handlers in the meteor and job-worker collection mocks). Like {@link MongoModifier}, this is + * intentionally NOT mongodb's `AnyBulkWriteOperation`: + * - the `update` of the `updateOne`/`updateMany` arms is constrained to {@link MongoModifier}, so a whole + * plain document cannot sneak in as a "modifier" via the bulkWrite path (the heaviest write path). Use + * the `replaceOne` arm for a genuine full-document replacement. + * - aggregation-pipeline updates (the `Document[]` form) are deliberately omitted, as `mongoModify` does + * not implement them. + */ +export type MongoBulkWriteOperation = + | { insertOne: InsertOneModel } + | { replaceOne: ReplaceOneModel } + | { updateOne: Omit, 'update'> & { update: MongoModifier } } + | { updateMany: Omit, 'update'> & { update: MongoModifier } } + | { deleteOne: DeleteOneModel } + | { deleteMany: DeleteManyModel } + /** End of hacks */ export function mongoWhereFilter>(items: R[], selector: MongoQuery): R[] { diff --git a/packages/job-worker/src/__mocks__/collection.ts b/packages/job-worker/src/__mocks__/collection.ts index 5a02eb3d015..fc426b8e7b0 100644 --- a/packages/job-worker/src/__mocks__/collection.ts +++ b/packages/job-worker/src/__mocks__/collection.ts @@ -27,12 +27,13 @@ import { TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timel import { clone, literal } from '@sofie-automation/corelib/dist/lib' import { FindOptions as CacheFindOptions, + MongoBulkWriteOperation, mongoFindOptions, mongoModify, mongoWhere, } from '@sofie-automation/corelib/dist/mongo' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' -import { AnyBulkWriteOperation, Collection, CountOptions, FindOptions } from 'mongodb' +import { Collection, CountOptions, FindOptions } from 'mongodb' import { ReadonlyDeep } from 'type-fest' import { IChangeStream, @@ -231,7 +232,7 @@ export class MockMongoCollection }> imp this.#documents.set(doc._id, clone(doc)) return exists } - async bulkWrite(ops: AnyBulkWriteOperation[]): Promise { + async bulkWrite(ops: MongoBulkWriteOperation[]): Promise { this.#ops.push({ type: 'bulkWrite', args: [ops.length] }) for (const op of ops) { diff --git a/packages/job-worker/src/db/changes.ts b/packages/job-worker/src/db/changes.ts index fff12e20dd3..e9a6dbb484e 100644 --- a/packages/job-worker/src/db/changes.ts +++ b/packages/job-worker/src/db/changes.ts @@ -1,6 +1,5 @@ import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' -import { AnyBulkWriteOperation } from 'mongodb' -import { ICollection, MongoQuery } from './collections.js' +import { ICollection, MongoBulkWriteOperation, MongoQuery } from './collections.js' import _ from 'underscore' import { deleteAllUndefinedProperties, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' import { JobContext } from '../jobs/index.js' @@ -123,7 +122,7 @@ async function savePreparedChanges }>( newObjIds.add(id) } - const updates: AnyBulkWriteOperation[] = [] + const updates: MongoBulkWriteOperation[] = [] const removedDocs: TDoc['_id'][] = [] _.each(preparedChanges.changed || [], (oUpdate) => { diff --git a/packages/job-worker/src/db/collection.ts b/packages/job-worker/src/db/collection.ts index 913d8ce9fc2..060c1d6c3a0 100644 --- a/packages/job-worker/src/db/collection.ts +++ b/packages/job-worker/src/db/collection.ts @@ -3,7 +3,7 @@ import { EventEmitter } from 'events' import { AnyBulkWriteOperation, ChangeStream, Collection as MongoCollection, FindOptions, CountOptions } from 'mongodb' import { IChangeStreamEvents } from './index.js' import { startSpanManual } from '../profiler.js' -import { IChangeStream, ICollection, MongoModifier, MongoQuery } from './collections.js' +import { IChangeStream, ICollection, MongoBulkWriteOperation, MongoModifier, MongoQuery } from './collections.js' /** Wrap some APM and better error small query modifications around a Mongo.Collection */ class WrappedCollection }> implements ICollection { @@ -155,7 +155,7 @@ class WrappedCollection }> implements I return res.deletedCount } - async bulkWrite(ops: Array>): Promise { + async bulkWrite(ops: Array>): Promise { const span = startSpanManual('WrappedCollection.bulkWrite') if (span) { span.addLabels({ @@ -165,7 +165,7 @@ class WrappedCollection }> implements I } if (ops.length > 0) { - const bulkWriteResult = await this.#collection.bulkWrite(ops, { + const bulkWriteResult = await this.#collection.bulkWrite(ops as AnyBulkWriteOperation[], { ordered: false, }) if (bulkWriteResult && bulkWriteResult.hasWriteErrors()) { diff --git a/packages/job-worker/src/db/collections.ts b/packages/job-worker/src/db/collections.ts index b4b8c810c09..e53432dd409 100644 --- a/packages/job-worker/src/db/collections.ts +++ b/packages/job-worker/src/db/collections.ts @@ -1,7 +1,6 @@ import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { MongoClient, - AnyBulkWriteOperation, Filter, FindOptions, Collection as MongoCollection, @@ -37,7 +36,10 @@ import { DBTimelineDatastoreEntry } from '@sofie-automation/corelib/dist/dataMod import { ExpectedPackageDB } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { PackageInfoDB } from '@sofie-automation/corelib/dist/dataModel/PackageInfos' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' -import type { MongoModifier as CorelibMongoModifier } from '@sofie-automation/corelib/dist/mongo' +import type { + MongoModifier as CorelibMongoModifier, + MongoBulkWriteOperation, +} from '@sofie-automation/corelib/dist/mongo' import { literal } from '@sofie-automation/corelib/dist/lib' import { ReadonlyDeep } from 'type-fest' import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' @@ -49,6 +51,7 @@ export type MongoQuery = Filter // Aliased from corelib so the job-worker and meteor layers share a single modifier type that stays in // lockstep with `mongoModify` (the in-memory implementation used by the unit-test mocks). export type MongoModifier = CorelibMongoModifier +export type { MongoBulkWriteOperation } export interface IReadOnlyCollection }> { readonly name: string @@ -76,7 +79,7 @@ export interface ICollection }> extends /** Returns true if a doc was replaced, false if inserted */ replace(doc: TDoc | ReadonlyDeep): Promise - bulkWrite(ops: Array>): Promise + bulkWrite(ops: Array>): Promise } export type IChangeStreamEvents }> = { diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index 245eac2d80a..6795809e41a 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -43,7 +43,7 @@ import { createPlayoutModelFromIngestModel } from '../playout/model/implementati import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { updateSegmentIdsForAdlibbedPartInstances } from './commit/updateSegmentIdsForAdlibbedPartInstances.js' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { AnyBulkWriteOperation } from 'mongodb' +import { MongoBulkWriteOperation } from '@sofie-automation/corelib/dist/mongo' export type BeforePartMapItem = { id: PartId; rank: number } export type BeforeIngestOperationPartMap = ReadonlyMap> @@ -369,7 +369,7 @@ async function updatePartInstancesSegmentIds( return 0 }) - const writeOps: AnyBulkWriteOperation[] = [] + const writeOps: MongoBulkWriteOperation[] = [] logger.debug(`updatePartInstancesSegmentIds: renameRules: ${JSON.stringify(renameRules)}`) diff --git a/packages/job-worker/src/ingest/expectedPackages.ts b/packages/job-worker/src/ingest/expectedPackages.ts index 4a2479d23fb..8bb86a95cef 100644 --- a/packages/job-worker/src/ingest/expectedPackages.ts +++ b/packages/job-worker/src/ingest/expectedPackages.ts @@ -22,7 +22,7 @@ import { JobContext, JobStudio } from '../jobs/index.js' import { IngestModel } from './model/IngestModel.js' import { IngestPartModel } from './model/IngestPartModel.js' import { hashObj } from '@sofie-automation/corelib/dist/lib' -import { AnyBulkWriteOperation } from 'mongodb' +import { MongoBulkWriteOperation } from '@sofie-automation/corelib/dist/mongo' export function updateExpectedMediaAndPlayoutItemsForPartModel(context: JobContext, part: IngestPartModel): void { updateExpectedPlayoutItemsForPartModel(context, part) @@ -122,7 +122,7 @@ async function writeUpdatedExpectedPackages( documentsToSave: ExpectedPackageDB[], matchSource: Partial ): Promise { - const writeOps: AnyBulkWriteOperation[] = [] + const writeOps: MongoBulkWriteOperation[] = [] const documentIdsToSave = documentsToSave.map((doc) => doc._id) diff --git a/packages/job-worker/src/ingest/model/implementation/DocumentChangeTracker.ts b/packages/job-worker/src/ingest/model/implementation/DocumentChangeTracker.ts index a45e3e19c45..61fdc3f91c2 100644 --- a/packages/job-worker/src/ingest/model/implementation/DocumentChangeTracker.ts +++ b/packages/job-worker/src/ingest/model/implementation/DocumentChangeTracker.ts @@ -1,12 +1,12 @@ import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' -import { AnyBulkWriteOperation } from 'mongodb' +import { MongoBulkWriteOperation } from '@sofie-automation/corelib/dist/mongo' import { LazyInitialise } from '../../../lib/lazy.js' import { DocumentChanges, getDocumentChanges } from './utils.js' export async function generateWriteOpsForLazyDocuments }>( currentDocs: LazyInitialise, changedIds: ReadonlySet -): Promise[]> { +): Promise[]> { const changeTracker = new DocumentChangeTracker() if (changedIds.size > 0 || currentDocs.isLoaded()) { @@ -106,8 +106,8 @@ export class DocumentChangeTracker }> { * Generate the mongodb BulkWrite operations for the documents known to this tracker * @returns mongodb BulkWrite operations */ - generateWriteOps(): AnyBulkWriteOperation[] { - const ops: AnyBulkWriteOperation[] = [] + generateWriteOps(): MongoBulkWriteOperation[] { + const ops: MongoBulkWriteOperation[] = [] for (const doc of this.#documentsToSave) { ops.push({ diff --git a/packages/job-worker/src/ingest/model/implementation/SaveIngestModel.ts b/packages/job-worker/src/ingest/model/implementation/SaveIngestModel.ts index 5bd2806aeba..d9e0e7a615a 100644 --- a/packages/job-worker/src/ingest/model/implementation/SaveIngestModel.ts +++ b/packages/job-worker/src/ingest/model/implementation/SaveIngestModel.ts @@ -17,7 +17,7 @@ import { DocumentChangeTracker } from './DocumentChangeTracker.js' import { logger } from '../../../logging.js' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { IngestExpectedPackage } from '../IngestExpectedPackage.js' -import { AnyBulkWriteOperation } from 'mongodb' +import { MongoBulkWriteOperation } from '@sofie-automation/corelib/dist/mongo' import { normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' export class SaveIngestModelHelper { @@ -153,7 +153,7 @@ export async function writeExpectedPackagesChangesForRundown( } // Generate any insert and update operations - const ops: AnyBulkWriteOperation[] = [] + const ops: MongoBulkWriteOperation[] = [] for (const doc of packagesToSave.values()) { const existingDoc = existingDocsMap.get(doc._id) if (!existingDoc) { diff --git a/packages/job-worker/src/ingest/nrcsIngestCache.ts b/packages/job-worker/src/ingest/nrcsIngestCache.ts index f73b3cb4278..003d52962a9 100644 --- a/packages/job-worker/src/ingest/nrcsIngestCache.ts +++ b/packages/job-worker/src/ingest/nrcsIngestCache.ts @@ -14,7 +14,7 @@ import { JobContext } from '../jobs/index.js' import { getPartId, getSegmentId } from './lib.js' import { SetOptional } from 'type-fest' import { groupByToMap, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' -import { AnyBulkWriteOperation } from 'mongodb' +import { MongoBulkWriteOperation } from '@sofie-automation/corelib/dist/mongo' import { diffAndReturnLatestObjects } from './model/implementation/utils.js' import { ICollection } from '../db/index.js' import { getCurrentTime } from '../lib/index.js' @@ -126,7 +126,7 @@ export class NrcsIngestRundownDataCache { const modifiedTime = getCurrentTime() - const updates: AnyBulkWriteOperation[] = [] + const updates: MongoBulkWriteOperation[] = [] const removedIds: NrcsIngestDataCacheObjId[] = [] for (const changedId of this.#changedDocumentIds) { const newDoc = documentsMap.get(changedId) diff --git a/packages/job-worker/src/ingest/sofieIngestCache.ts b/packages/job-worker/src/ingest/sofieIngestCache.ts index 43fb4e02efb..b78f8f6a376 100644 --- a/packages/job-worker/src/ingest/sofieIngestCache.ts +++ b/packages/job-worker/src/ingest/sofieIngestCache.ts @@ -14,7 +14,7 @@ import { JobContext } from '../jobs/index.js' import { getPartId, getSegmentId } from './lib.js' import { SetOptional } from 'type-fest' import { groupByToMap, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' -import { AnyBulkWriteOperation } from 'mongodb' +import { MongoBulkWriteOperation } from '@sofie-automation/corelib/dist/mongo' import { ICollection } from '../db/index.js' import { getCurrentTime } from '../lib/index.js' @@ -147,7 +147,7 @@ export class SofieIngestRundownDataCache { const modifiedTime = getCurrentTime() - const updates: AnyBulkWriteOperation[] = [] + const updates: MongoBulkWriteOperation[] = [] const removedIds: SofieIngestDataCacheObjId[] = [] for (const changedId of this.#changedDocumentIds) { const newDoc = documentsMap.get(changedId) diff --git a/packages/job-worker/src/notifications/NotificationsModelHelper.ts b/packages/job-worker/src/notifications/NotificationsModelHelper.ts index b1abd7a6748..b76d56b33fd 100644 --- a/packages/job-worker/src/notifications/NotificationsModelHelper.ts +++ b/packages/job-worker/src/notifications/NotificationsModelHelper.ts @@ -9,9 +9,9 @@ import { import { getHash } from '@sofie-automation/corelib/dist/hash' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { assertNever, flatten, omit, type Complete } from '@sofie-automation/corelib/dist/lib' -import { type AnyBulkWriteOperation } from 'mongodb' import { StudioId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { isEqual } from 'underscore' +import type { MongoBulkWriteOperation } from '../db/collections.js' interface NotificationsLoadState { dbNotifications: ReadonlyMap | null @@ -165,7 +165,7 @@ export class NotificationsModelHelper implements INotificationsModel { ...(dbNotifications ? Array.from(dbNotifications.keys()) : []), ]) - const updates: AnyBulkWriteOperation[] = [] + const updates: MongoBulkWriteOperation[] = [] const localIdsToKeep: string[] = [] const localIdsToDelete: string[] = [] for (const localId of allLocalIds) { diff --git a/packages/job-worker/src/playout/expectedPackages.ts b/packages/job-worker/src/playout/expectedPackages.ts index 0fc07f795de..5f85e27a56f 100644 --- a/packages/job-worker/src/playout/expectedPackages.ts +++ b/packages/job-worker/src/playout/expectedPackages.ts @@ -6,8 +6,8 @@ import { isPackageReferencedByPlayout, } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { AnyBulkWriteOperation } from 'mongodb' import { ExpectedPackageId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { MongoBulkWriteOperation } from '../db/collections.js' export async function handleCleanupOrphanedExpectedPackageReferences( context: JobContext, @@ -58,7 +58,7 @@ export async function handleCleanupOrphanedExpectedPackageReferences( pieceInstancePackageMap.set(pieceInstance._id, new Set(pieceInstance.neededExpectedPackageIds)) } - const writeOps: AnyBulkWriteOperation[] = [] + const writeOps: MongoBulkWriteOperation[] = [] for (const expectedPackage of existingPackages) { // Find the pieceInstanceIds that are stale diff --git a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts index e43d55348cf..c833b57cfce 100644 --- a/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/SavePlayoutModel.ts @@ -9,7 +9,6 @@ import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartIns import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { AnyBulkWriteOperation } from 'mongodb' import { JobContext } from '../../../jobs/index.js' import { PlayoutPartInstanceModelImpl } from './PlayoutPartInstanceModelImpl.js' import { PlayoutRundownModelImpl } from './PlayoutRundownModelImpl.js' @@ -18,6 +17,7 @@ import { ExpectedPackage } from '@sofie-automation/blueprints-integration' import { normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' import { ExpectedPackageDB } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' +import { MongoBulkWriteOperation } from '../../../db/collections.js' /** * Save any changed AdlibTesting Segments @@ -28,7 +28,7 @@ export async function writeAdlibTestingSegments( context: JobContext, rundowns: readonly PlayoutRundownModelImpl[] ): Promise { - const writeOps: AnyBulkWriteOperation[] = [] + const writeOps: MongoBulkWriteOperation[] = [] for (const rundown of rundowns) { if (rundown.AdlibTestingSegmentHasChanged) { @@ -73,8 +73,8 @@ export function writePartInstancesAndPieceInstances( context: JobContext, partInstances: Map ): [Promise, Promise] { - const partInstanceOps: AnyBulkWriteOperation[] = [] - const pieceInstanceOps: AnyBulkWriteOperation[] = [] + const partInstanceOps: MongoBulkWriteOperation[] = [] + const pieceInstanceOps: MongoBulkWriteOperation[] = [] const deletedPartInstanceIds: PartInstanceId[] = [] const deletedPieceInstanceIds: PieceInstanceId[] = [] @@ -233,7 +233,7 @@ export async function writeExpectedPackagesForPlayoutSources( // We now know what needs to be written (only the additive changes) - const writeOps: AnyBulkWriteOperation[] = [] + const writeOps: MongoBulkWriteOperation[] = [] for (const [packageId, pieceInstanceIds] of pieceInstancesToAddToPackages.entries()) { writeOps.push({ updateOne: { diff --git a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts index d7a16567ba2..af13216dc3e 100644 --- a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts +++ b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts @@ -11,8 +11,8 @@ import { StudioPlayoutModel } from '../../studio/model/StudioPlayoutModel.js' import { PieceTimelineMetadata } from '../timeline/pieceGroup.js' import { deserializeTimelineBlob } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { ReadonlyDeep } from 'type-fest' -import { AnyBulkWriteOperation } from 'mongodb' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { MongoBulkWriteOperation } from '../../db/collections.js' /** * Called from Playout-gateway when the trigger-time of a timeline object has updated @@ -86,7 +86,7 @@ export async function handleTimelineTriggerTime(context: JobContext, data: OnTim } async function writePieceInstanceChangesToMongo(context: JobContext, changes: PieceInstancesChanges): Promise { - const updates: AnyBulkWriteOperation[] = [] + const updates: MongoBulkWriteOperation[] = [] for (const [pieceInstanceId, newTime] of changes.setStartTime.entries()) { updates.push({ updateOne: { diff --git a/packages/job-worker/src/updatePartInstanceRanksAndOrphanedState.ts b/packages/job-worker/src/updatePartInstanceRanksAndOrphanedState.ts index b0e07cbf50a..08e24641ba6 100644 --- a/packages/job-worker/src/updatePartInstanceRanksAndOrphanedState.ts +++ b/packages/job-worker/src/updatePartInstanceRanksAndOrphanedState.ts @@ -1,7 +1,6 @@ import { PartId, PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { groupByToMap, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' -import { AnyBulkWriteOperation } from 'mongodb' import { ReadonlyDeep } from 'type-fest' import { BeforeIngestOperationPartMap, BeforePartMapItem } from './ingest/commit.js' import { JobContext } from './jobs/index.js' @@ -11,6 +10,7 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PlayoutModel } from './playout/model/PlayoutModel.js' import { IngestModelReadonly } from './ingest/model/IngestModel.js' import { PlayoutPartInstanceModel } from './playout/model/PlayoutPartInstanceModel.js' +import { MongoBulkWriteOperation } from './db/collections.js' type MinimalPartInstance = Pick & { part: Pick @@ -178,7 +178,7 @@ async function updateNormalPartInstanceRanksAndFindOrphans( changedSegmentIds: ReadonlyDeep ) { const orphanedPartInstances: MinimalPartInstance[] = [] - const writeOps: AnyBulkWriteOperation[] = [] + const writeOps: MongoBulkWriteOperation[] = [] const partInstancesInChangedSegments = (await context.directCollections.PartInstances.findFetch( { From f78d0f0a61e7cec41247212f443db7a2428d7481 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 24 Jun 2026 12:38:33 +0000 Subject: [PATCH 4/4] fix: remove unused rawDatabase mongo collection method --- meteor/__mocks__/helpers/lib.ts | 1 - meteor/__mocks__/mongo.ts | 3 --- .../server/collections/implementations/asyncCollection.ts | 8 ++------ meteor/server/collections/implementations/mock.ts | 6 +----- packages/meteor-lib/src/collections/lib.ts | 8 +------- packages/webui/src/client/collections/lib.ts | 5 +---- 6 files changed, 5 insertions(+), 26 deletions(-) diff --git a/meteor/__mocks__/helpers/lib.ts b/meteor/__mocks__/helpers/lib.ts index da2d1864814..ea8e61cf953 100644 --- a/meteor/__mocks__/helpers/lib.ts +++ b/meteor/__mocks__/helpers/lib.ts @@ -21,7 +21,6 @@ const METHOD_NAMES = [ 'findOne', 'insert', 'rawCollection', - 'rawDatabase', 'remove', 'update', 'upsert', diff --git a/meteor/__mocks__/mongo.ts b/meteor/__mocks__/mongo.ts index bf02de72866..0692b3e1c1f 100644 --- a/meteor/__mocks__/mongo.ts +++ b/meteor/__mocks__/mongo.ts @@ -347,9 +347,6 @@ export namespace MongoMock { // todo } - rawDatabase(): any { - throw new Error('Not implemented') - } rawCollection(): any { return { bulkWrite: async (updates: AnyBulkWriteOperation[], _options: unknown) => { diff --git a/meteor/server/collections/implementations/asyncCollection.ts b/meteor/server/collections/implementations/asyncCollection.ts index 9025c7dc766..7e607ac203f 100644 --- a/meteor/server/collections/implementations/asyncCollection.ts +++ b/meteor/server/collections/implementations/asyncCollection.ts @@ -15,7 +15,7 @@ import { ObserveChangesCallbacks, ObserveCallbacks, } from '@sofie-automation/meteor-lib/dist/collections/lib' -import type { AnyBulkWriteOperation, Collection as RawCollection, Db as RawDb } from 'mongodb' +import type { AnyBulkWriteOperation, Collection as RawCollection } from 'mongodb' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { NpmModuleMongodb } from 'meteor/npm-mongo' import { profiler } from '../../api/profiler' @@ -35,7 +35,7 @@ export type MinimalMongoCursor }> = Pick< */ export type MinimalMeteorMongoCollection }> = Pick< Mongo.Collection, - 'insertAsync' | 'removeAsync' | 'updateAsync' | 'upsertAsync' | 'rawCollection' | 'rawDatabase' | 'createIndex' + 'insertAsync' | 'removeAsync' | 'updateAsync' | 'upsertAsync' | 'rawCollection' | 'createIndex' > & { find: (...args: Parameters['find']>) => MinimalMongoCursor } @@ -73,10 +73,6 @@ export class WrappedAsyncMongoCollection< rawCollection(): RawCollection { return this._collection.rawCollection() as any } - protected rawDatabase(): RawDb { - return this._collection.rawDatabase() as any - } - async findFetchAsync( selector: MongoQuery | DBInterface['_id'], options?: FindOptions diff --git a/meteor/server/collections/implementations/mock.ts b/meteor/server/collections/implementations/mock.ts index f2682049ab0..a3cd8b8d74d 100644 --- a/meteor/server/collections/implementations/mock.ts +++ b/meteor/server/collections/implementations/mock.ts @@ -2,7 +2,7 @@ import { MongoBulkWriteOperation, MongoQuery } from '@sofie-automation/corelib/d import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' import { FindOptions, MongoCursor } from '@sofie-automation/meteor-lib/dist/collections/lib' -import type { AnyBulkWriteOperation, Db as RawDb } from 'mongodb' +import type { AnyBulkWriteOperation } from 'mongodb' import { AsyncOnlyMongoCollection } from '../collection' import { WrappedAsyncMongoCollection } from './asyncCollection' import { Mongo } from 'meteor/mongo' @@ -22,10 +22,6 @@ export class WrappedMockCollection - /** - * Returns the [`Db`](http://mongodb.github.io/node-mongodb-native/3.0/api/Db.html) object corresponding to this collection's database connection from the - * [npm `mongodb` driver module](https://www.npmjs.com/package/mongodb) which is wrapped by `Mongo.Collection`. - */ - rawDatabase(): RawDb - /** * Remove documents from the collection * @param selector Specifies which documents to remove diff --git a/packages/webui/src/client/collections/lib.ts b/packages/webui/src/client/collections/lib.ts index 72b6da6fc53..27fdff497f6 100644 --- a/packages/webui/src/client/collections/lib.ts +++ b/packages/webui/src/client/collections/lib.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' import { type ProtectedString, protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import type { Collection as RawCollection, Db as RawDb } from 'mongodb' +import type { Collection as RawCollection } from 'mongodb' import type { CollectionName, CustomCollectionName as CustomCorelibCollectionName, @@ -228,9 +228,6 @@ export class WrappedMongoCollection { return this._collection.rawCollection() as any } - rawDatabase(): RawDb { - return this._collection.rawDatabase() as any - } remove(selector: MongoQuery | DBInterface['_id']): number { try { return this._collection.remove(selector as any)