Skip to content

Commit 025fdc1

Browse files
committed
update branch
2 parents 73fa3b9 + 47936e2 commit 025fdc1

27 files changed

+571
-542
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: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.2.31",
3+
"version": "1.2.32",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {
@@ -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.4.0",
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",
@@ -65,7 +64,6 @@
6564
"aws-sdk": "^2.1174.0",
6665
"axios": "^0.27.2",
6766
"body-parser": "^1.19.0",
68-
"bson": "^4.6.5",
6967
"cloudpayments": "^6.0.1",
7068
"codex-accounting-sdk": "https://github.com/codex-team/codex-accounting-sdk.git",
7169
"dataloader": "^2.0.0",
@@ -82,13 +80,16 @@
8280
"lodash.mergewith": "^4.6.2",
8381
"migrate-mongo": "^7.0.1",
8482
"mime-types": "^2.1.25",
85-
"mongodb": "^3.7.3",
83+
"mongodb": "^6.0.0",
8684
"morgan": "^1.10.1",
8785
"prom-client": "^15.1.3",
8886
"redis": "^4.7.0",
8987
"safe-regex": "^2.1.0",
9088
"ts-node-dev": "^2.0.0",
9189
"uuid": "^8.3.2",
9290
"zod": "^3.25.76"
91+
},
92+
"resolutions": {
93+
"bson": "^6.7.0"
9394
}
9495
}

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/integrations/vercel-ai/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ class VercelAIApi {
1717
/**
1818
* @todo make it dynamic, get from project settings
1919
*/
20-
this.modelId = 'gpt-4o';
20+
this.modelId = 'deepseek/deepseek-v3.1';
2121
}
2222

2323
/**
2424
* Generate AI suggestion for the event
2525
*
26-
* @param {EventData<EventAddons>} payload - event data
26+
* @param {EventData<EventAddons>} payload - event data to make suggestion
2727
* @returns {Promise<string>} AI suggestion for the event
2828
* @todo add defence against invalid prompt injection
2929
*/
3030
public async generateSuggestion(payload: EventData<EventAddons>) {
3131
const { text } = await generateText({
32-
model: openai(this.modelId),
32+
model: this.modelId,
3333
system: ctoInstruction,
3434
prompt: eventSolvingInput(payload),
3535
});

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
}

0 commit comments

Comments
 (0)