Skip to content

Commit a40d522

Browse files
committed
merge master
2 parents 0599da4 + 443be89 commit a40d522

File tree

13 files changed

+527
-170
lines changed

13 files changed

+527
-170
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
require('dotenv').config();
2+
require('process');
3+
const { setup } = require('./setup');
4+
5+
/**
6+
* Method that runs convertor script
7+
*/
8+
async function run() {
9+
const { client, hawkDb } = await setup();
10+
11+
const collections = await hawkDb.listCollections({}, {
12+
authorizedCollections: true,
13+
nameOnly: true,
14+
}).toArray();
15+
16+
let usersInProjectCollectionsToCheck = collections.filter(col => /^users-in-project:/.test(col.name)).map(col => col.name);
17+
18+
console.log(`Found ${usersInProjectCollectionsToCheck.length} users in project collections.`);
19+
20+
const usersDocuments = await hawkDb.collection('users').find({}).toArray();
21+
22+
// Convert events
23+
let i = 1;
24+
25+
for (const collectionName of usersInProjectCollectionsToCheck) {
26+
console.log(`[${i}/${usersInProjectCollectionsToCheck.length}] Processing ${collectionName}`);
27+
28+
const usersInProject = await hawkDb.collection(collectionName).find({}).toArray();
29+
30+
console.log(`Found ${usersInProject.length} users in project ${collectionName}.`);
31+
32+
let usersUpdatedCount = 0;
33+
34+
for (const userInProject of usersInProject) {
35+
const userDocument = usersDocuments.find(u => u._id.toString() === userInProject.userId.toString());
36+
if (userDocument) {
37+
const projectId = collectionName.split(':')[1];
38+
await hawkDb.collection('users').updateOne({ _id: userDocument._id }, { $set: { [`projectsLastVisit.${projectId}`]: userInProject.timestamp } });
39+
usersUpdatedCount++;
40+
console.log(`Updated ${usersUpdatedCount}/${usersInProject.length} users in project ${collectionName}.`);
41+
}
42+
}
43+
44+
i++;
45+
}
46+
47+
await client.close();
48+
}
49+
50+
run().catch(err => {
51+
console.error('❌ Script failed:', err);
52+
process.exit(1);
53+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
require('dotenv').config();
2+
require('process');
3+
const { setup } = require('./setup');
4+
5+
/**
6+
* Method that runs convertor script
7+
*/
8+
async function run() {
9+
const { client, hawkDb } = await setup();
10+
11+
const collections = await hawkDb.listCollections({}, {
12+
authorizedCollections: true,
13+
nameOnly: true,
14+
}).toArray();
15+
16+
let usersMembershipCollectionsToCheck = collections.filter(col => /^membership:/.test(col.name)).map(col => col.name);
17+
18+
console.log(`Found ${usersMembershipCollectionsToCheck.length} users membership collections.`);
19+
20+
const usersDocuments = await hawkDb.collection('users').find({}).toArray();
21+
22+
let i = 1;
23+
24+
for (const collectionName of usersMembershipCollectionsToCheck) {
25+
console.log(`[${i}/${usersMembershipCollectionsToCheck.length}] Processing ${collectionName}`);
26+
27+
const userId = collectionName.split(':')[1];
28+
29+
const userDocument = usersDocuments.find(u => u._id.toString() === userId);
30+
31+
if (!userDocument) {
32+
i++;
33+
continue;
34+
}
35+
36+
const memberships = await hawkDb.collection(collectionName).find({}).toArray();
37+
38+
for (const membership of memberships) {
39+
const workspaceId = membership.workspaceId.toString();
40+
const isPending = membership.isPending || false;
41+
await hawkDb.collection('users').updateOne({ _id: userDocument._id }, { $set: { [`workspaces.${workspaceId}`]: { isPending } } });
42+
}
43+
44+
i++;
45+
}
46+
47+
await client.close();
48+
}
49+
50+
run().catch(err => {
51+
console.error('❌ Script failed:', err);
52+
process.exit(1);
53+
});

convertors/setup.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const { MongoClient } = require('mongodb');
2+
3+
async function setup() {
4+
const fullUri = process.env.MONGO_HAWK_DB_URL;
5+
6+
// Parse the Mongo URL manually
7+
const mongoUrl = new URL(fullUri);
8+
const hawkDatabaseName = 'hawk';
9+
10+
// Extract query parameters
11+
const queryParams = Object.fromEntries(mongoUrl.searchParams.entries());
12+
13+
// Compose connection options manually
14+
const options = {
15+
useNewUrlParser: true,
16+
useUnifiedTopology: true,
17+
authSource: queryParams.authSource || 'admin',
18+
replicaSet: queryParams.replicaSet || undefined,
19+
tls: queryParams.tls === 'true',
20+
tlsInsecure: queryParams.tlsInsecure === 'true',
21+
// connectTimeoutMS: 3600000,
22+
// socketTimeoutMS: 3600000,
23+
};
24+
25+
// Remove query string from URI
26+
mongoUrl.search = '';
27+
const cleanUri = mongoUrl.toString();
28+
29+
console.log('Connecting to:', cleanUri);
30+
console.log('With options:', options);
31+
32+
const client = new MongoClient(cleanUri, options);
33+
34+
await client.connect();
35+
const hawkDb = client.db(hawkDatabaseName);
36+
37+
console.log(`Connected to database: ${hawkDatabaseName}`);
38+
39+
return { client, hawkDb };
40+
}
41+
42+
module.exports = { setup };
43+
44+

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.2.14",
3+
"version": "1.2.19",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {
@@ -11,6 +11,7 @@
1111
"dev:up": "docker-compose -f docker-compose.dev.yml up -d",
1212
"dev:down": "docker-compose -f docker-compose.dev.yml down",
1313
"build": "tsc",
14+
"convert": "node ./convertors/set-user-project-last-visit.js",
1415
"migrations:create": "docker-compose exec api yarn migrate-mongo create",
1516
"migrations:up": "docker-compose exec api yarn migrate-mongo up",
1617
"migrations:down": "docker-compose exec api yarn migrate-mongo down",

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import schema from './schema';
2828
import { graphqlUploadExpress } from 'graphql-upload';
2929
import { metricsMiddleware, createMetricsServer, graphqlMetricsPlugin } from './metrics';
3030
import { requestLogger } from './utils/logger';
31+
import ReleasesFactory from './models/releasesFactory';
3132

3233
/**
3334
* Option to enable playground
@@ -164,12 +165,16 @@ class HawkAPI {
164165
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
165166
const businessOperationsFactory = new BusinessOperationsFactory(mongo.databases.hawk!, dataLoaders);
166167

168+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
169+
const releasesFactory = new ReleasesFactory(mongo.databases.events!);
170+
167171
return {
168172
usersFactory,
169173
workspacesFactory,
170174
projectsFactory,
171175
plansFactory,
172176
businessOperationsFactory,
177+
releasesFactory,
173178
};
174179
}
175180

src/models/eventsFactory.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ class EventsFactory extends Factory {
196196
* @param {'BY_DATE' | 'BY_COUNT'} sort - events sort order
197197
* @param {EventsFilters} filters - marks by which events should be filtered
198198
* @param {String} search - Search query
199+
* @param {String} release - release name
199200
*
200201
* @return {DaylyEventsPortionSchema}
201202
*/
@@ -204,7 +205,8 @@ class EventsFactory extends Factory {
204205
paginationCursor = null,
205206
sort = 'BY_DATE',
206207
filters = {},
207-
search = ''
208+
search = '',
209+
release
208210
) {
209211
if (typeof search !== 'string') {
210212
throw new Error('Search parameter must be a string');
@@ -324,6 +326,25 @@ class EventsFactory extends Factory {
324326
)
325327
: {};
326328

329+
// Filter by release if provided (coerce event payload release to string)
330+
const releaseFilter = release
331+
? {
332+
$expr: {
333+
$eq: [
334+
{
335+
$convert: {
336+
input: '$event.payload.release',
337+
to: 'string',
338+
onError: '',
339+
onNull: '',
340+
},
341+
},
342+
String(release),
343+
],
344+
},
345+
}
346+
: {};
347+
327348
pipeline.push(
328349
/**
329350
* Left outer join original event on groupHash field
@@ -360,6 +381,7 @@ class EventsFactory extends Factory {
360381
$match: {
361382
...matchFilter,
362383
...searchFilter,
384+
...releaseFilter,
363385
},
364386
},
365387
{ $limit: limit + 1 },

src/models/releasesFactory.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// TODO: it would be great to move release logic from another factories/resolvers to this class
2+
import type { Collection, Db, ObjectId } from 'mongodb';
3+
import type { ReleaseDBScheme } from '@hawk.so/types';
4+
5+
/**
6+
* Interface representing how release files are stored in the DB
7+
*/
8+
export interface ReleaseFileDBScheme {
9+
/**
10+
* File's id
11+
*/
12+
_id: ObjectId;
13+
14+
/**
15+
* File length in bytes
16+
*/
17+
length: number;
18+
19+
/**
20+
* File upload date
21+
*/
22+
uploadDate: Date;
23+
24+
/**
25+
* File chunk size
26+
*/
27+
chunkSize: number;
28+
29+
/**
30+
* File map name
31+
*/
32+
filename: string;
33+
34+
/**
35+
* File MD5 hash
36+
*/
37+
md5: string;
38+
}
39+
40+
/**
41+
* ReleasesFactory
42+
* Helper for accessing releases collection
43+
*/
44+
export default class ReleasesFactory {
45+
/**
46+
* DataBase collection to work with
47+
*/
48+
private readonly collection: Collection<ReleaseDBScheme>;
49+
private readonly filesCollection: Collection<ReleaseFileDBScheme>;
50+
51+
/**
52+
* Creates releases factory instance
53+
* @param dbConnection - connection to Events DB
54+
*/
55+
constructor(dbConnection: Db) {
56+
this.collection = dbConnection.collection<ReleaseDBScheme>('releases');
57+
this.filesCollection = dbConnection.collection<ReleaseFileDBScheme>('releases.files');
58+
}
59+
60+
/**
61+
* Find one release document by projectId and release label.
62+
* Tries both exact string match and numeric fallback (if release can be cast to number).
63+
*/
64+
public async findByProjectAndRelease(
65+
projectId: string | ObjectId,
66+
release: string
67+
): Promise<ReleaseDBScheme | null> {
68+
const projectIdStr = projectId.toString();
69+
70+
// Try exact match as stored
71+
let doc = await this.collection.findOne({
72+
projectId: projectIdStr,
73+
release: release as ReleaseDBScheme['release'],
74+
});
75+
76+
// Fallback if release stored as number
77+
if (!doc) {
78+
const asNumber = Number(release);
79+
80+
if (!Number.isNaN(asNumber)) {
81+
doc = await this.collection.findOne({
82+
projectId: projectIdStr,
83+
release: asNumber as unknown as ReleaseDBScheme['release'],
84+
});
85+
}
86+
}
87+
88+
return doc;
89+
}
90+
91+
/**
92+
* Find files by file ids
93+
* @param fileIds - file ids
94+
* @returns files
95+
*/
96+
public async findFilesByFileIds(fileIds: ObjectId[]): Promise<ReleaseFileDBScheme[]> {
97+
return this.filesCollection.find({ _id: { $in: fileIds } }).toArray();
98+
}
99+
}

0 commit comments

Comments
 (0)