Skip to content

Commit 443be89

Browse files
feat: add releaseDetails resolver (#577)
* feat: add releaseDetails resolver * feat: add files length and uploadDate * fix: remove daily events from release details * feat: add factory for releasees * fix: lint * fix: tests * Bump version up to 1.2.17 * docs: add comment * fix: move find files to the factory * fix: fileIds * Bump version up to 1.2.19 * fix: naming * fix: naming and lint * fix: add comma --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 17be2b0 commit 443be89

File tree

8 files changed

+281
-17
lines changed

8 files changed

+281
-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.18",
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
@@ -186,6 +186,7 @@ class EventsFactory extends Factory {
186186
* @param {'BY_DATE' | 'BY_COUNT'} sort - events sort order
187187
* @param {EventsFilters} filters - marks by which events should be filtered
188188
* @param {String} search - Search query
189+
* @param {String} release - release name
189190
*
190191
* @return {DaylyEventsPortionSchema}
191192
*/
@@ -194,7 +195,8 @@ class EventsFactory extends Factory {
194195
paginationCursor = null,
195196
sort = 'BY_DATE',
196197
filters = {},
197-
search = ''
198+
search = '',
199+
release
198200
) {
199201
if (typeof search !== 'string') {
200202
throw new Error('Search parameter must be a string');
@@ -314,6 +316,25 @@ class EventsFactory extends Factory {
314316
)
315317
: {};
316318

319+
// Filter by release if provided (coerce event payload release to string)
320+
const releaseFilter = release
321+
? {
322+
$expr: {
323+
$eq: [
324+
{
325+
$convert: {
326+
input: '$event.payload.release',
327+
to: 'string',
328+
onError: '',
329+
onNull: '',
330+
},
331+
},
332+
String(release),
333+
],
334+
},
335+
}
336+
: {};
337+
317338
pipeline.push(
318339
/**
319340
* Left outer join original event on groupHash field
@@ -350,6 +371,7 @@ class EventsFactory extends Factory {
350371
$match: {
351372
...matchFilter,
352373
...searchFilter,
374+
...releaseFilter,
353375
},
354376
},
355377
{ $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/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 {

src/types/graphql.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ProjectsFactory from '../models/projectsFactory';
55
// import Accounting from 'codex-accounting-sdk';
66
import PlansFactory from '../models/plansFactory';
77
import BusinessOperationsFactory from '../models/businessOperationsFactory';
8+
import ReleasesFactory from '../models/releasesFactory';
89

910
/**
1011
* Resolver's Context argument
@@ -86,6 +87,11 @@ export interface ContextFactories {
8687
* Allows to work with the Business Operations models
8788
*/
8889
businessOperationsFactory: BusinessOperationsFactory;
90+
91+
/**
92+
* Releases factory for working with releases
93+
*/
94+
releasesFactory: ReleasesFactory;
8995
}
9096

9197
/**

0 commit comments

Comments
 (0)