Skip to content

Commit 061b4ba

Browse files
committed
wip: simplify mongo mock types
1 parent 7be1181 commit 061b4ba

File tree

10 files changed

+209
-294
lines changed

10 files changed

+209
-294
lines changed

meteor/__mocks__/mongo.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
22
import * as _ from 'underscore'
33
import { literal, ProtectedString, unprotectString, protectString, getRandomString } from '../server/lib/tempLib'
4-
import { sleep } from '../server/lib/lib'
54
import { RandomMock } from './random'
65
import { MeteorMock } from './meteor'
76
import { Random } from 'meteor/random'
@@ -18,7 +17,10 @@ import {
1817
} from '@sofie-automation/meteor-lib/dist/collections/lib'
1918
import { mongoWhere, mongoFindOptions, mongoModify, MongoQuery } from '@sofie-automation/corelib/dist/mongo'
2019
import { AsyncOnlyMongoCollection, AsyncOnlyReadOnlyMongoCollection } from '../server/collections/collection'
21-
import type { MinimalMeteorMongoCollection, MinimalMongoCursor } from '../server/collections/implementations/base'
20+
import type {
21+
MinimalMeteorMongoCollection,
22+
MinimalMongoCursor,
23+
} from '../server/collections/implementations/asyncCollection'
2224
const clone = require('fast-clone')
2325

2426
export namespace MongoMock {
@@ -90,12 +92,21 @@ export namespace MongoMock {
9092
return docs
9193
},
9294
fetchAsync: async () => {
95+
// Force this to be performed async
96+
await MeteorMock.sleepNoFakeTimers(0)
97+
9398
return clone(docs)
9499
},
95100
countAsync: async () => {
101+
// Force this to be performed async
102+
await MeteorMock.sleepNoFakeTimers(0)
103+
96104
return docs.length
97105
},
98106
async observeAsync(clbs: ObserveCallbacks<T>): Promise<Meteor.LiveQueryHandle> {
107+
// Force this to be performed async
108+
await MeteorMock.sleepNoFakeTimers(0)
109+
99110
const id = Random.id(5)
100111
observers.push(
101112
literal<ObserverEntry<T>>({
@@ -111,6 +122,9 @@ export namespace MongoMock {
111122
}
112123
},
113124
async observeChangesAsync(clbs: ObserveChangesCallbacks<T>): Promise<Meteor.LiveQueryHandle> {
125+
// Force this to be performed async
126+
await MeteorMock.sleepNoFakeTimers(0)
127+
114128
// todo - finish implementing uses of callbacks
115129
const id = Random.id(5)
116130
observers.push(
@@ -139,6 +153,9 @@ export namespace MongoMock {
139153
return docs[0]
140154
}
141155
async updateAsync(query: any, modifier: any, options?: UpdateOptions): Promise<number> {
156+
// Force this to be performed async
157+
await MeteorMock.sleepNoFakeTimers(0)
158+
142159
const unimplementedUsedOptions = _.without(_.keys(options), 'multi')
143160
if (unimplementedUsedOptions.length > 0) {
144161
throw new Error(`update being performed using unimplemented options: ${unimplementedUsedOptions}`)
@@ -173,6 +190,9 @@ export namespace MongoMock {
173190
return docs.length
174191
}
175192
async insertAsync(doc: any): Promise<string> {
193+
// Force this to be performed async
194+
await MeteorMock.sleepNoFakeTimers(0)
195+
176196
const d = _.clone(doc)
177197
if (!d._id) d._id = protectString(RandomMock.id())
178198

@@ -206,6 +226,9 @@ export namespace MongoMock {
206226
modifier: any,
207227
options?: UpsertOptions
208228
): Promise<{ numberAffected: number | undefined; insertedId: string | undefined }> {
229+
// Force this to be performed async
230+
await MeteorMock.sleepNoFakeTimers(0)
231+
209232
const id = _.isString(query) ? query : query._id
210233

211234
const docs = this.find(id)._fetchRaw()
@@ -220,6 +243,9 @@ export namespace MongoMock {
220243
}
221244
}
222245
async removeAsync(query: any): Promise<number> {
246+
// Force this to be performed async
247+
await MeteorMock.sleepNoFakeTimers(0)
248+
223249
const docs = this.find(query)._fetchRaw()
224250

225251
_.each(docs, (doc) => {
@@ -254,7 +280,7 @@ export namespace MongoMock {
254280
rawCollection(): any {
255281
return {
256282
bulkWrite: async (updates: AnyBulkWriteOperation<any>[], _options: unknown) => {
257-
await sleep(this.asyncBulkWriteDelay)
283+
await MeteorMock.sleepNoFakeTimers(this.asyncBulkWriteDelay)
258284

259285
for (const update of updates) {
260286
if ('insertOne' in update) {

meteor/server/__tests__/_testEnvironment.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio'
3232
import { Mongo } from 'meteor/mongo'
3333
import { defaultStudio } from '../../__mocks__/defaultCollectionObjects'
34-
import { MinimalMeteorMongoCollection } from '../collections/implementations/base'
34+
import { MinimalMeteorMongoCollection } from '../collections/implementations/asyncCollection'
3535

3636
describe('Basic test of test environment', () => {
3737
test('Meteor Random mock', () => {

meteor/server/api/deviceTriggers/StudioObserver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { RundownContentObserver } from './RundownContentObserver'
2020
import { RundownsObserver } from './RundownsObserver'
2121
import { RundownPlaylists, Rundowns, ShowStyleBases } from '../../collections'
2222
import { PromiseDebounce } from '../../publications/lib/debounce'
23-
import { MinimalMongoCursor } from '../../collections/implementations/base'
23+
import { MinimalMongoCursor } from '../../collections/implementations/asyncCollection'
2424

2525
type ChangedHandler = (showStyleBaseId: ShowStyleBaseId, cache: ContentCache) => () => void
2626

meteor/server/collections/collection.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
UpdateOptions,
2020
UpsertOptions,
2121
} from '@sofie-automation/meteor-lib/dist/collections/lib'
22-
import { MinimalMongoCursor } from './implementations/base'
22+
import { MinimalMongoCursor } from './implementations/asyncCollection'
2323

2424
export interface MongoAllowRules<DBInterface> {
2525
insert?: (userId: UserId, doc: DBInterface) => Promise<boolean> | boolean
@@ -116,8 +116,7 @@ function wrapMeteorCollectionIntoAsyncCollection<DBInterface extends { _id: Prot
116116
name: CollectionName
117117
) {
118118
if ((collection as any)._isMock) {
119-
// We use a special one in tests, to reduce the amount of hops between fibers and promises
120-
// nocommit - is this still needed?
119+
// We use a special one in tests, to add some async which naturally doesn't happen in the collection
121120
return new WrappedMockCollection<DBInterface>(collection, name)
122121
} else {
123122
// Override the default mongodb methods, because the errors thrown by them doesn't contain the proper call stack

meteor/server/collections/implementations/asyncCollection.ts

Lines changed: 164 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,74 @@
1-
import { MongoQuery } from '@sofie-automation/corelib/dist/mongo'
2-
import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString'
1+
import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo'
2+
import { ProtectedString, protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString'
33
import { Meteor } from 'meteor/meteor'
4+
import { Mongo } from 'meteor/mongo'
45
import {
6+
UpdateOptions,
7+
UpsertOptions,
8+
IndexSpecifier,
9+
MongoCursor,
510
FindOptions,
611
ObserveChangesCallbacks,
712
ObserveCallbacks,
813
} from '@sofie-automation/meteor-lib/dist/collections/lib'
14+
import type { AnyBulkWriteOperation, Collection as RawCollection, Db as RawDb } from 'mongodb'
15+
import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError'
16+
import { NpmModuleMongodb } from 'meteor/npm-mongo'
17+
import { profiler } from '../../api/profiler'
918
import { PromisifyCallbacks } from '@sofie-automation/shared-lib/dist/lib/types'
10-
import type { AnyBulkWriteOperation } from 'mongodb'
11-
import { MinimalMongoCursor, WrappedMongoCollectionBase } from './base'
1219
import { AsyncOnlyMongoCollection } from '../collection'
13-
import { profiler } from '../../api/profiler'
20+
21+
export type MinimalMongoCursor<T extends { _id: ProtectedString<any> }> = Pick<
22+
MongoCursor<T>,
23+
'fetchAsync' | 'observeChangesAsync' | 'observeAsync' | 'countAsync'
24+
// | 'forEach' | 'map' |
25+
>
26+
27+
export type MinimalMeteorMongoCollection<T extends { _id: ProtectedString<any> }> = Pick<
28+
Mongo.Collection<T>,
29+
// | 'find'
30+
'insertAsync' | 'removeAsync' | 'updateAsync' | 'upsertAsync' | 'rawCollection' | 'rawDatabase' | 'createIndex'
31+
> & {
32+
find: (...args: Parameters<Mongo.Collection<T>['find']>) => MinimalMongoCursor<T>
33+
}
1434

1535
export class WrappedAsyncMongoCollection<DBInterface extends { _id: ProtectedString<any> }>
16-
extends WrappedMongoCollectionBase<DBInterface>
1736
implements AsyncOnlyMongoCollection<DBInterface>
1837
{
38+
protected readonly _collection: MinimalMeteorMongoCollection<DBInterface>
39+
40+
public readonly name: string | null
41+
42+
constructor(collection: Mongo.Collection<DBInterface>, name: string | null) {
43+
this._collection = collection as any
44+
this.name = name
45+
}
46+
47+
protected get _isMock(): boolean {
48+
// @ts-expect-error re-export private property
49+
return this._collection._isMock
50+
}
51+
52+
public get mockCollection(): MinimalMeteorMongoCollection<DBInterface> {
53+
return this._collection
54+
}
55+
1956
get mutableCollection(): AsyncOnlyMongoCollection<DBInterface> {
2057
return this
2158
}
2259

60+
protected wrapMongoError(e: unknown): never {
61+
const str = stringifyError(e) || 'Unknown MongoDB Error'
62+
throw new Meteor.Error(e instanceof Meteor.Error ? e.error : 500, `Collection "${this.name}": ${str}`)
63+
}
64+
65+
rawCollection(): RawCollection<DBInterface> {
66+
return this._collection.rawCollection() as any
67+
}
68+
protected rawDatabase(): RawDb {
69+
return this._collection.rawDatabase() as any
70+
}
71+
2372
async findFetchAsync(
2473
selector: MongoQuery<DBInterface> | DBInterface['_id'],
2574
options?: FindOptions<DBInterface>
@@ -131,10 +180,115 @@ export class WrappedAsyncMongoCollection<DBInterface extends { _id: ProtectedStr
131180
}
132181
}
133182

183+
public async countDocuments(
184+
selector?: MongoQuery<DBInterface>,
185+
options?: FindOptions<DBInterface>
186+
): Promise<number> {
187+
const span = profiler.startSpan(`MongoCollection.${this.name}.countDocuments`)
188+
if (span) {
189+
span.addLabels({
190+
collection: this.name,
191+
query: JSON.stringify(selector),
192+
})
193+
}
194+
try {
195+
const res = await this._collection.find((selector ?? {}) as any, options as any).countAsync()
196+
if (span) span.end()
197+
return res
198+
} catch (e) {
199+
if (span) span.end()
200+
this.wrapMongoError(e)
201+
}
202+
}
203+
204+
public async insertAsync(doc: DBInterface): Promise<DBInterface['_id']> {
205+
const span = profiler.startSpan(`MongoCollection.${this.name}.insert`)
206+
if (span) {
207+
span.addLabels({
208+
collection: this.name,
209+
id: unprotectString(doc._id),
210+
})
211+
}
212+
try {
213+
const resultId = await this._collection.insertAsync(doc as unknown as Mongo.OptionalId<DBInterface>)
214+
if (span) span.end()
215+
return protectString(resultId)
216+
} catch (e) {
217+
if (span) span.end()
218+
this.wrapMongoError(e)
219+
}
220+
}
221+
134222
async insertManyAsync(docs: DBInterface[]): Promise<Array<DBInterface['_id']>> {
135223
return Promise.all(docs.map(async (doc) => this.insertAsync(doc)))
136224
}
137225

226+
public async removeAsync(selector: MongoQuery<DBInterface> | DBInterface['_id']): Promise<number> {
227+
const span = profiler.startSpan(`MongoCollection.${this.name}.remove`)
228+
if (span) {
229+
span.addLabels({
230+
collection: this.name,
231+
query: JSON.stringify(selector),
232+
})
233+
}
234+
try {
235+
const res = await this._collection.removeAsync(selector as any)
236+
if (span) span.end()
237+
return res
238+
} catch (e) {
239+
if (span) span.end()
240+
this.wrapMongoError(e)
241+
}
242+
}
243+
public async updateAsync(
244+
selector: MongoQuery<DBInterface> | DBInterface['_id'] | { _id: DBInterface['_id'] },
245+
modifier: MongoModifier<DBInterface>,
246+
options?: UpdateOptions
247+
): Promise<number> {
248+
const span = profiler.startSpan(`MongoCollection.${this.name}.update`)
249+
if (span) {
250+
span.addLabels({
251+
collection: this.name,
252+
query: JSON.stringify(selector),
253+
})
254+
}
255+
try {
256+
const res = await this._collection.updateAsync(selector as any, modifier as any, options)
257+
if (span) span.end()
258+
return res
259+
} catch (e) {
260+
if (span) span.end()
261+
this.wrapMongoError(e)
262+
}
263+
}
264+
public async upsertAsync(
265+
selector: MongoQuery<DBInterface> | DBInterface['_id'] | { _id: DBInterface['_id'] },
266+
modifier: MongoModifier<DBInterface>,
267+
options?: UpsertOptions
268+
): Promise<{
269+
numberAffected?: number
270+
insertedId?: DBInterface['_id']
271+
}> {
272+
const span = profiler.startSpan(`MongoCollection.${this.name}.upsert`)
273+
if (span) {
274+
span.addLabels({
275+
collection: this.name,
276+
query: JSON.stringify(selector),
277+
})
278+
}
279+
try {
280+
const result = await this._collection.upsertAsync(selector as any, modifier as any, options)
281+
if (span) span.end()
282+
return {
283+
numberAffected: result.numberAffected,
284+
insertedId: protectString(result.insertedId),
285+
}
286+
} catch (e) {
287+
if (span) span.end()
288+
this.wrapMongoError(e)
289+
}
290+
}
291+
138292
async upsertManyAsync(docs: DBInterface[]): Promise<{ numberAffected: number; insertedIds: DBInterface['_id'][] }> {
139293
const result: {
140294
numberAffected: number
@@ -178,16 +332,16 @@ export class WrappedAsyncMongoCollection<DBInterface extends { _id: ProtectedStr
178332
if (span) span.end()
179333
}
180334

181-
async countDocuments(selector?: MongoQuery<DBInterface>, options?: FindOptions<DBInterface>): Promise<number> {
182-
const span = profiler.startSpan(`MongoCollection.${this.name}.countDocuments`)
335+
createIndex(keys: IndexSpecifier<DBInterface> | string, options?: NpmModuleMongodb.CreateIndexesOptions): void {
336+
const span = profiler.startSpan(`MongoCollection.${this.name}.createIndex`)
183337
if (span) {
184338
span.addLabels({
185339
collection: this.name,
186-
query: JSON.stringify(selector),
340+
keys: JSON.stringify(keys),
187341
})
188342
}
189343
try {
190-
const res = await this._collection.find((selector ?? {}) as any, options as any).countAsync()
344+
const res = this._collection.createIndex(keys as any, options)
191345
if (span) span.end()
192346
return res
193347
} catch (e) {

0 commit comments

Comments
 (0)