Skip to content

Commit b712f71

Browse files
chore(deps): update mongo driver (#610) (#611)
* update mongo driver * fix tests * fix integration tests, remove old billing * Bump version up to 1.2.32 * lint --------- Co-authored-by: Peter <[email protected]> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent a4e9a75 commit b712f71

26 files changed

+573
-532
lines changed

.env.sample

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@ PLAYGROUND_ENABLE=false
3535
# AMQP URL
3636
AMQP_URL=amqp://guest:guest@rabbitmq
3737

38-
# Billing settings
39-
BILLING_DEBUG=true
40-
BILLING_COMPANY_EMAIL="[email protected]"
41-
4238
### Accounting module ###
4339
# Accounting service URL
4440
# CODEX_ACCOUNTING_URL=http://accounting:3999/graphql

.env.test

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,6 @@ SMTP_SENDER_ADDRESS=
4646
# AMQP URL
4747
AMQP_URL=amqp://guest:guest@rabbitmq:5672/
4848

49-
# Billing settings
50-
BILLING_DEBUG=true
51-
BILLING_COMPANY_EMAIL="[email protected]"
52-
5349
### Accounting module ###
5450
# Accounting service URL
5551
# CODEX_ACCOUNTING_URL=

jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ module.exports = {
1515
*/
1616
preset: '@shelf/jest-mongodb',
1717

18+
/**
19+
* Setup file to provide global APIs needed by MongoDB driver
20+
*/
21+
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
22+
1823
/**
1924
* TypeScript support
2025
*/

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"test:integration:down": "docker compose -f docker-compose.test.yml down --volumes"
2121
},
2222
"devDependencies": {
23-
"@shelf/jest-mongodb": "^1.2.2",
23+
"@shelf/jest-mongodb": "^6.0.2",
24+
"@swc/core": "^1.3.0",
2425
"@types/jest": "^26.0.8",
2526
"eslint": "^6.7.2",
2627
"eslint-config-codex": "1.2.4",
@@ -43,15 +44,13 @@
4344
"@hawk.so/types": "^0.1.37",
4445
"@n1ru4l/json-patch-plus": "^0.2.0",
4546
"@types/amqp-connection-manager": "^2.0.4",
46-
"@types/bson": "^4.0.5",
4747
"@types/debug": "^4.1.5",
4848
"@types/escape-html": "^1.0.0",
4949
"@types/graphql-upload": "^8.0.11",
5050
"@types/jsonwebtoken": "^8.3.5",
5151
"@types/lodash.clonedeep": "^4.5.9",
5252
"@types/lodash.mergewith": "^4.6.9",
5353
"@types/mime-types": "^2.1.0",
54-
"@types/mongodb": "^3.6.20",
5554
"@types/morgan": "^1.9.10",
5655
"@types/node": "^16.11.46",
5756
"@types/safe-regex": "^1.1.6",
@@ -64,7 +63,6 @@
6463
"aws-sdk": "^2.1174.0",
6564
"axios": "^0.27.2",
6665
"body-parser": "^1.19.0",
67-
"bson": "^4.6.5",
6866
"cloudpayments": "^6.0.1",
6967
"codex-accounting-sdk": "https://github.com/codex-team/codex-accounting-sdk.git",
7068
"dataloader": "^2.0.0",
@@ -81,13 +79,16 @@
8179
"lodash.mergewith": "^4.6.2",
8280
"migrate-mongo": "^7.0.1",
8381
"mime-types": "^2.1.25",
84-
"mongodb": "^3.7.3",
82+
"mongodb": "^6.0.0",
8583
"morgan": "^1.10.1",
8684
"prom-client": "^15.1.3",
8785
"redis": "^4.7.0",
8886
"safe-regex": "^2.1.0",
8987
"ts-node-dev": "^2.0.0",
9088
"uuid": "^8.3.2",
9189
"zod": "^3.25.76"
90+
},
91+
"resolutions": {
92+
"bson": "^6.7.0"
9293
}
9394
}

src/dataLoaders.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import DataLoader from 'dataloader';
2-
import { Db, ObjectId } from 'mongodb';
2+
import { Db, ObjectId, WithId } from 'mongodb';
33
import { PlanDBScheme, UserDBScheme, WorkspaceDBScheme, ProjectDBScheme, EventData, EventAddons } from '@hawk.so/types';
44

55
type EventDbScheme = {
@@ -47,7 +47,7 @@ export default class DataLoaders {
4747
*/
4848
public userByEmail = new DataLoader<string, UserDBScheme | null>(
4949
(userEmails) =>
50-
this.batchByField<UserDBScheme, string>('users', userEmails, 'email'),
50+
this.batchByField<UserDBScheme, 'email'>('users', 'email', userEmails),
5151
{ cache: false }
5252
);
5353

@@ -69,41 +69,51 @@ export default class DataLoaders {
6969
* @param collectionName - collection name to get entities
7070
* @param ids - ids for resolving
7171
*/
72-
private async batchByIds<T extends { _id: ObjectId }>(collectionName: string, ids: ReadonlyArray<string>): Promise<(T | null | Error)[]> {
73-
return this.batchByField<T, ObjectId>(collectionName, ids.map(id => new ObjectId(id)), '_id');
72+
private async batchByIds<T extends { _id: ObjectId }>(
73+
collectionName: string,
74+
ids: ReadonlyArray<string>
75+
): Promise<(WithId<T> | null)[]> {
76+
return this.batchByField<T, '_id'>(collectionName, '_id', ids.map(id => new ObjectId(id)));
7477
}
7578

7679
/**
7780
* Batching function for resolving entities by certain field
7881
* @param collectionName - collection name to get entities
79-
* @param values - values for resolving
8082
* @param fieldName - field name to resolve
83+
* @param values - values for resolving
8184
*/
8285
private async batchByField<
83-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
84-
T extends { [key: string]: any },
85-
FieldType extends ObjectId | string
86-
>(collectionName: string, values: ReadonlyArray<FieldType>, fieldName: string): Promise<(T | null | Error)[]> {
86+
T extends Record<string, any>,
87+
FieldType extends keyof T
88+
>(
89+
collectionName: string,
90+
fieldName: FieldType,
91+
values: ReadonlyArray<T[FieldType]>
92+
): Promise<(WithId<T> | null)[]> {
93+
type Doc = WithId<T>;
8794
const valuesMap = new Map<string, FieldType>();
8895

8996
for (const value of values) {
9097
valuesMap.set(value.toString(), value);
9198
}
9299

93-
const queryResult = await this.dbConnection.collection(collectionName)
100+
const queryResult = await this.dbConnection
101+
.collection<T>(collectionName)
94102
.find({
95103
[fieldName]: { $in: Array.from(valuesMap.values()) },
96-
})
104+
} as any)
97105
.toArray();
98106

99107
/**
100108
* Map for making associations between given id and fetched entity
101109
* It's because MongoDB `find` mixed all entities
102110
*/
103-
const entitiesMap: Record<string, T> = {};
111+
const entitiesMap: Record<string, Doc> = {};
112+
113+
queryResult.forEach((entity) => {
114+
const key = entity[fieldName as keyof Doc];
104115

105-
queryResult.forEach((entity: T) => {
106-
entitiesMap[entity[fieldName].toString()] = entity;
116+
entitiesMap[key.toString()] = entity;
107117
}, {});
108118

109119
return values.map((field) => entitiesMap[field.toString()] || null);

src/metrics/mongodb.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,11 @@ export function setupMongoMetrics(client: MongoClient): void {
317317
.observe(duration);
318318

319319
// Track error
320-
const errorCode = event.failure?.code?.toString() || 'unknown';
320+
/**
321+
* MongoDB failure objects may have additional properties like 'code'
322+
* that aren't part of the standard Error type
323+
*/
324+
const errorCode = (event.failure as any)?.code?.toString() || 'unknown';
321325

322326
mongoCommandErrors
323327
.labels(metadata.commandName, errorCode)

src/models/abstactModelFactory.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { Collection, Db, ObjectID } from 'mongodb';
1+
import { Collection, Db, Document, ObjectId } from 'mongodb';
22
import AbstractModel, { ModelConstructor } from './abstractModel';
33

44
/**
55
* Model Factory class
66
*/
7-
export default abstract class AbstractModelFactory<DBScheme, Model extends AbstractModel<DBScheme>> {
7+
export default abstract class AbstractModelFactory<DBScheme extends Document, Model extends AbstractModel<DBScheme>> {
88
/**
99
* Database connection to interact with
1010
*/
@@ -17,11 +17,8 @@ export default abstract class AbstractModelFactory<DBScheme, Model extends Abstr
1717

1818
/**
1919
* Collection to work with
20-
* We can't use generic type for collection because of bug in TS
21-
* @see {@link https://github.com/DefinitelyTyped/DefinitelyTyped/issues/39358#issuecomment-546559564}
22-
* So we should override collection type in child classes
2320
*/
24-
protected abstract collection: Collection;
21+
protected abstract collection: Collection<DBScheme>;
2522

2623
/**
2724
* Creates factory instance
@@ -44,7 +41,12 @@ export default abstract class AbstractModelFactory<DBScheme, Model extends Abstr
4441
return null;
4542
}
4643

47-
return new this.Model(searchResult);
44+
/**
45+
* MongoDB returns WithId<DBScheme>, but Model constructor expects DBScheme.
46+
* Since WithId<DBScheme> is DBScheme & { _id: ObjectId } and DBScheme already
47+
* includes _id: ObjectId, they are structurally compatible.
48+
*/
49+
return new this.Model(searchResult as DBScheme);
4850
}
4951

5052
/**
@@ -53,13 +55,18 @@ export default abstract class AbstractModelFactory<DBScheme, Model extends Abstr
5355
*/
5456
public async findById(id: string): Promise<Model | null> {
5557
const searchResult = await this.collection.findOne({
56-
_id: new ObjectID(id),
57-
});
58+
_id: new ObjectId(id),
59+
} as any);
5860

5961
if (!searchResult) {
6062
return null;
6163
}
6264

63-
return new this.Model(searchResult);
65+
/**
66+
* MongoDB returns WithId<DBScheme>, but Model constructor expects DBScheme.
67+
* Since WithId<DBScheme> is DBScheme & { _id: ObjectId } and DBScheme already
68+
* includes _id: ObjectId, they are structurally compatible.
69+
*/
70+
return new this.Model(searchResult as DBScheme);
6471
}
6572
}

src/models/abstractModel.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { Collection, Db } from 'mongodb';
1+
import { Collection, Db, Document } from 'mongodb';
22
import { databases } from '../mongo';
33

44
/**
55
* Model constructor type
66
*/
7-
export type ModelConstructor<DBScheme, Model extends AbstractModel<DBScheme>> = new (modelData: DBScheme) => Model;
7+
export type ModelConstructor<DBScheme extends Document, Model extends AbstractModel<DBScheme>> = new (modelData: DBScheme) => Model;
88

99
/**
1010
* Base model
1111
*/
12-
export default abstract class AbstractModel<DBScheme> {
12+
export default abstract class AbstractModel<DBScheme extends Document> {
1313
/**
1414
* Database connection to interact with DB
1515
*/
@@ -19,7 +19,7 @@ export default abstract class AbstractModel<DBScheme> {
1919
/**
2020
* Model's collection
2121
*/
22-
protected abstract collection: Collection;
22+
protected abstract collection: Collection<DBScheme>;
2323

2424
/**
2525
* Creates model instance
@@ -32,10 +32,16 @@ export default abstract class AbstractModel<DBScheme> {
3232
/**
3333
* Update entity data
3434
* @param query - query to match
35-
* @param data - update data
35+
* @param data - update data (supports MongoDB dot notation for nested fields)
3636
* @return number of documents modified
3737
*/
38-
public async update(query: object, data: object): Promise<number> {
39-
return (await this.collection.updateOne(query, { $set: data })).modifiedCount;
38+
public async update(query: object, data: Partial<DBScheme> | Record<string, any>): Promise<number> {
39+
/**
40+
* Type assertion is needed because MongoDB's updateOne accepts both
41+
* Partial<DBScheme> (for regular updates) and Record<string, any>
42+
* (for dot notation like 'identities.workspaceId.saml.id'), but the
43+
* type system requires MatchKeysAndValues<DBScheme>.
44+
*/
45+
return (await this.collection.updateOne(query, { $set: data as any })).modifiedCount;
4046
}
4147
}

src/models/eventsFactory.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import ChartDataService from '../services/chartDataService';
77
const Factory = require('./modelFactory');
88
const mongo = require('../mongo');
99
const Event = require('../models/event');
10-
const { ObjectID } = require('mongodb');
10+
const { ObjectId } = require('mongodb');
1111
const { composeEventPayloadByRepetition } = require('../utils/merge');
1212

1313
const MAX_DB_READ_BATCH_SIZE = Number(process.env.MAX_DB_READ_BATCH_SIZE);
@@ -174,7 +174,7 @@ class EventsFactory extends Factory {
174174
/**
175175
* Find event by id
176176
*
177-
* @param {string|ObjectID} id - event's id
177+
* @param {string|ObjectId} id - event's id
178178
* @returns {Event|null}
179179
*/
180180
async findById(id) {
@@ -282,7 +282,7 @@ class EventsFactory extends Factory {
282282
$and: [
283283
{ groupingTimestamp: paginationCursor.groupingTimestampBoundary },
284284
{ [sort]: paginationCursor.sortValueBoundary },
285-
{ _id: { $lte: new ObjectID(paginationCursor.idBoundary) } },
285+
{ _id: { $lte: new ObjectId(paginationCursor.idBoundary) } },
286286
],
287287
},
288288
],
@@ -654,7 +654,7 @@ class EventsFactory extends Factory {
654654
/**
655655
* Returns Event repetitions
656656
*
657-
* @param {string|ObjectID} originalEventId - id of the original event
657+
* @param {string|ObjectId} originalEventId - id of the original event
658658
* @param {Number} limit - count limitations
659659
* @param {Number} cursor - pointer to the next repetition
660660
*
@@ -663,7 +663,7 @@ class EventsFactory extends Factory {
663663
async getEventRepetitions(originalEventId, limit = 10, cursor = null) {
664664
limit = this.validateLimit(limit);
665665

666-
cursor = cursor ? new ObjectID(cursor) : null;
666+
cursor = cursor ? new ObjectId(cursor) : null;
667667

668668
const result = {
669669
repetitions: [],
@@ -766,7 +766,7 @@ class EventsFactory extends Factory {
766766
*/
767767
const repetition = await this.getCollection(this.TYPES.REPETITIONS)
768768
.findOne({
769-
_id: ObjectID(repetitionId),
769+
_id: new ObjectId(repetitionId),
770770
});
771771

772772
const originalEvent = await this.eventsDataLoader.load(originalEventId);
@@ -828,8 +828,8 @@ class EventsFactory extends Factory {
828828
async visitEvent(eventId, userId) {
829829
const result = await this.getCollection(this.TYPES.EVENTS)
830830
.updateOne(
831-
{ _id: new ObjectID(eventId) },
832-
{ $addToSet: { visitedBy: new ObjectID(userId) } }
831+
{ _id: new ObjectId(eventId) },
832+
{ $addToSet: { visitedBy: new ObjectId(userId) } }
833833
);
834834

835835
if (result.matchedCount === 0) {
@@ -856,7 +856,7 @@ class EventsFactory extends Factory {
856856
throw new Error(`Event not found for eventId: ${eventId}`);
857857
}
858858

859-
const query = { _id: new ObjectID(event._id) };
859+
const query = { _id: new ObjectId(event._id) };
860860

861861
const markKey = `marks.${mark}`;
862862

@@ -908,7 +908,7 @@ class EventsFactory extends Factory {
908908
async updateAssignee(eventId, assignee) {
909909
const collection = this.getCollection(this.TYPES.EVENTS);
910910

911-
const query = { _id: new ObjectID(eventId) };
911+
const query = { _id: new ObjectId(eventId) };
912912

913913
const update = {
914914
$set: { assignee: assignee },

src/models/model.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
const mongodbDriver = require('mongodb');
2-
const ObjectID = mongodbDriver.ObjectID;
2+
const ObjectId = mongodbDriver.ObjectId;
33

44
/**
55
* @typedef {Object} BaseModel
6-
* @typedef {string|ObjectID} id - record id
6+
* @typedef {string|ObjectId} id - record id
77
*/
88

99
/**
@@ -44,7 +44,7 @@ class Model {
4444
*/
4545
static async findById(id) {
4646
const searchResult = await this.collection.findOne({
47-
_id: new ObjectID(id),
47+
_id: new ObjectId(id),
4848
});
4949

5050
return new this({

0 commit comments

Comments
 (0)