Skip to content

Commit bfbd273

Browse files
committed
Merge branch 'master' of github.com:codex-team/hawk.api.nodejs into feat/openai
2 parents 2b848d2 + 443be89 commit bfbd273

File tree

9 files changed

+285
-17
lines changed

9 files changed

+285
-17
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.2.17",
3+
"version": "1.2.19",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {

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
@@ -208,6 +208,7 @@ class EventsFactory extends Factory {
208208
* @param {'BY_DATE' | 'BY_COUNT'} sort - events sort order
209209
* @param {EventsFilters} filters - marks by which events should be filtered
210210
* @param {String} search - Search query
211+
* @param {String} release - release name
211212
*
212213
* @return {DaylyEventsPortionSchema}
213214
*/
@@ -216,7 +217,8 @@ class EventsFactory extends Factory {
216217
paginationCursor = null,
217218
sort = 'BY_DATE',
218219
filters = {},
219-
search = ''
220+
search = '',
221+
release
220222
) {
221223
if (typeof search !== 'string') {
222224
throw new Error('Search parameter must be a string');
@@ -336,6 +338,25 @@ class EventsFactory extends Factory {
336338
)
337339
: {};
338340

341+
// Filter by release if provided (coerce event payload release to string)
342+
const releaseFilter = release
343+
? {
344+
$expr: {
345+
$eq: [
346+
{
347+
$convert: {
348+
input: '$event.payload.release',
349+
to: 'string',
350+
onError: '',
351+
onNull: '',
352+
},
353+
},
354+
String(release),
355+
],
356+
},
357+
}
358+
: {};
359+
339360
pipeline.push(
340361
/**
341362
* Left outer join original event on groupHash field
@@ -372,6 +393,7 @@ class EventsFactory extends Factory {
372393
$match: {
373394
...matchFilter,
374395
...searchFilter,
396+
...releaseFilter,
375397
},
376398
},
377399
{ $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+
}

src/models/user.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,10 @@ export default class UserModel extends AbstractModel<UserDBScheme> implements Us
370370
public async getWorkspacesIds(ids: (string | ObjectId)[] = []): Promise<string[]> {
371371
const res = [];
372372

373+
if (ids.length === 0) {
374+
return Object.keys(this.workspaces);
375+
}
376+
373377
for (const id of ids) {
374378
const workspaceId = id.toString();
375379
const workspace = this.workspaces[workspaceId];

src/resolvers/project.js

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ReceiveTypes } from '@hawk.so/types';
22
import * as telegram from '../utils/telegram';
33
const mongo = require('../mongo');
4+
const { ObjectId } = require('mongodb');
45
const { ApolloError, UserInputError } = require('apollo-server-express');
56
const Validator = require('../utils/validator');
67
const EventsFactory = require('../models/eventsFactory');
@@ -454,11 +455,12 @@ module.exports = {
454455
* @param {DailyEventsCursor} cursor - object with boundary values of the first event in the next portion
455456
* @param {'BY_DATE' | 'BY_COUNT'} sort - events sort order
456457
* @param {EventsFilters} filters - marks by which events should be filtered
458+
* @param {String} release - release name
457459
* @param {String} search - search query
458460
*
459461
* @return {Promise<RecentEventSchema[]>}
460462
*/
461-
async dailyEventsPortion(project, { limit, nextCursor, sort, filters, search }, context) {
463+
async dailyEventsPortion(project, { limit, nextCursor, sort, filters, search, release }, context) {
462464
if (search) {
463465
if (search.length > MAX_SEARCH_QUERY_LENGTH) {
464466
search = search.slice(0, MAX_SEARCH_QUERY_LENGTH);
@@ -467,7 +469,7 @@ module.exports = {
467469

468470
const factory = getEventsFactory(context, project._id);
469471

470-
const dailyEventsPortion = await factory.findDailyEventsPortion(limit, nextCursor, sort, filters, search);
472+
const dailyEventsPortion = await factory.findDailyEventsPortion(limit, nextCursor, sort, filters, search, release);
471473

472474
return dailyEventsPortion;
473475
},
@@ -561,5 +563,64 @@ module.exports = {
561563

562564
return result;
563565
},
566+
567+
/**
568+
* Return detailed info for a specific release
569+
* @param {ProjectDBScheme} project
570+
* @param {Object} args
571+
* @param {string} args.release - release identifier
572+
*/
573+
async releaseDetails(project, { release }, { factories }) {
574+
const releasesFactory = factories.releasesFactory;
575+
const releaseDoc = await releasesFactory.findByProjectAndRelease(project._id, release);
576+
577+
let enrichedFiles = Array.isArray(releaseDoc.files) ? releaseDoc.files : [];
578+
579+
// If there are files to enrich, try to get their metadata
580+
if (enrichedFiles.length > 0) {
581+
try {
582+
const fileIds = [
583+
...new Set(enrichedFiles.map(file => String(file._id))),
584+
].map(id => new ObjectId(id));
585+
586+
if (fileIds.length > 0) {
587+
const filesInfo = await factories.releasesFactory.findFilesByFileIds(
588+
fileIds
589+
);
590+
591+
const metaById = new Map(
592+
filesInfo.map(fileInfo => [String(fileInfo._id), {
593+
length: fileInfo.length,
594+
uploadDate: fileInfo.uploadDate,
595+
} ])
596+
);
597+
598+
enrichedFiles = enrichedFiles.map((entry) => {
599+
const meta = metaById.get(String(entry._id));
600+
601+
return {
602+
mapFileName: entry.mapFileName,
603+
originFileName: entry.originFileName,
604+
length: meta.length ? meta.length : null,
605+
uploadDate: meta.uploadDate ? meta.uploadDate : null,
606+
};
607+
});
608+
}
609+
} catch (e) {
610+
// In case of any error with enrichment, fallback to original structure
611+
enrichedFiles = releaseDoc.files ? releaseDoc.files : [];
612+
}
613+
}
614+
615+
return {
616+
release,
617+
projectId: project._id,
618+
commitsCount: Array.isArray(releaseDoc.commits) ? releaseDoc.commits.length : 0,
619+
filesCount: Array.isArray(releaseDoc.files) ? releaseDoc.files.length : 0,
620+
commits: releaseDoc.commits ? releaseDoc.commits : [],
621+
files: enrichedFiles,
622+
timestamp: releaseDoc._id ? dateFromObjectId(releaseDoc._id) : null,
623+
};
624+
},
564625
},
565626
};

src/typeDefs/project.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,66 @@ type ProjectRelease {
155155
filesCount: Int!
156156
}
157157
158+
"""
159+
Source map file information
160+
"""
161+
type SourceMapDataExtended {
162+
"""
163+
Name of source-map file
164+
"""
165+
mapFileName: String!
166+
167+
"""
168+
Bundle or chunk name
169+
"""
170+
originFileName: String!
171+
172+
"""
173+
File size in bytes (from releases-js.files)
174+
"""
175+
length: Int
176+
177+
"""
178+
Upload date (from releases-js.files)
179+
"""
180+
uploadDate: DateTime
181+
}
182+
183+
"""
184+
Detailed info for a specific release
185+
"""
186+
type ProjectReleaseDetails {
187+
"""
188+
Release identifier
189+
"""
190+
release: String!
191+
192+
"""
193+
Number of commits in this release
194+
"""
195+
commitsCount: Int!
196+
197+
"""
198+
Number of files in this release
199+
"""
200+
filesCount: Int!
201+
202+
"""
203+
Release creation timestamp
204+
"""
205+
timestamp: Float!
206+
207+
"""
208+
Commits (from releases collection)
209+
"""
210+
commits: [Commit!]
211+
212+
"""
213+
Changed files (from releases collection)
214+
"""
215+
files: [SourceMapDataExtended!]
216+
}
217+
158218
"""
159219
Respose object with updated project and his id
160220
"""
@@ -282,6 +342,11 @@ type Project {
282342
Search query
283343
"""
284344
search: String
345+
346+
"""
347+
Release label to filter events by payload.release
348+
"""
349+
release: String
285350
): DailyEventsPortion
286351
287352
"""
@@ -322,6 +387,11 @@ type Project {
322387
List of releases with unique events count, commits count and files count
323388
"""
324389
releases: [ProjectRelease!]!
390+
391+
"""
392+
Detailed info for a specific release
393+
"""
394+
releaseDetails(release: String!): ProjectReleaseDetails!
325395
}
326396
327397
extend type Query {

0 commit comments

Comments
 (0)