diff --git a/package.json b/package.json index 10a4f8c0..1b70044d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.1.17", + "version": "1.1.18", "main": "index.ts", "license": "UNLICENSED", "scripts": { diff --git a/src/index.ts b/src/index.ts index 1ed92aa8..2117cd49 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import PlansFactory from './models/plansFactory'; import BusinessOperationsFactory from './models/businessOperationsFactory'; import schema from './schema'; import { graphqlUploadExpress } from 'graphql-upload'; +import ReleasesFactory from './models/releaseFactory'; /** * Option to enable playground @@ -145,12 +146,16 @@ class HawkAPI { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const businessOperationsFactory = new BusinessOperationsFactory(mongo.databases.hawk!, dataLoaders); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const releasesFactory = new ReleasesFactory(mongo.databases.events!, dataLoaders); + return { usersFactory, workspacesFactory, projectsFactory, plansFactory, businessOperationsFactory, + releasesFactory, }; } diff --git a/src/models/releaseFactory.ts b/src/models/releaseFactory.ts new file mode 100644 index 00000000..a80dd654 --- /dev/null +++ b/src/models/releaseFactory.ts @@ -0,0 +1,92 @@ +import { Collection, Db } from 'mongodb'; +import { ReleaseDBScheme, SourceMapFileChunk } from '@hawk.so/types'; +import DataLoaders from '../dataLoaders'; + +interface ReleaseWithFileDetails extends ReleaseDBScheme { + fileDetails?: SourceMapFileChunk[]; +} + +export default class ReleasesFactory { + /** + * Releases collection + */ + private collection: Collection; + + /** + * DataLoader for releases + */ + private dataLoaders: DataLoaders; + + /** + * Creates an instance of the releases factory + * @param dbConnection - database connection + * @param dataLoaders - DataLoaders instance for request batching + */ + constructor(dbConnection: Db, dataLoaders: DataLoaders) { + this.collection = dbConnection.collection('releases'); + this.dataLoaders = dataLoaders; + } + + /** + * Get releases by project identifier with file sizes + * @param projectId - project identifier + */ + public async findManyByProjectId(projectId: string): Promise { + try { + const releases = await this.collection.aggregate([ + { + $match: { + projectId: projectId, + }, + }, + { + $lookup: { + from: 'releases.files', + let: { fileIds: '$files._id' }, + pipeline: [ + { + $match: { + $expr: { + $in: ['$_id', '$$fileIds'], + }, + }, + }, + { + $project: { + _id: 1, + length: 1, + chunkSize: 1, + }, + }, + ], + as: 'fileDetails', + }, + }, + ]).toArray(); + + return releases.map(release => this.enrichReleaseWithFileSizes(release)); + } catch (error) { + console.error(`[ReleasesFactory] Error in findManyByProjectId:`, error); + throw error; + } + } + + /** + * Enriches release with file sizes from file details + * @param release - release with file details + * @returns enriched release + */ + private enrichReleaseWithFileSizes(release: ReleaseWithFileDetails): ReleaseDBScheme { + const fileDetailsMap = new Map( + release.fileDetails?.map(detail => [detail._id.toString(), detail.length]) || [] + ); + + return { + ...release, + files: release.files?.map(file => ({ + ...file, + size: fileDetailsMap.get(file._id?.toString() || '') || 0, + })), + }; + } +} diff --git a/src/resolvers/project.js b/src/resolvers/project.js index 9c0ada37..f153038b 100644 --- a/src/resolvers/project.js +++ b/src/resolvers/project.js @@ -265,6 +265,26 @@ module.exports = { return event; }, + /** + * Returns project releases + * + * @param {ProjectDBScheme} project - result of parent resolver + * @param {ContextFactories} context - Global GraphQL context with factories + * @returns {Promise} + */ + async releases(project, _, { factories }) { + if (!project._id) { + throw new Error('projectId is required to fetch releases'); + } + + try { + return await factories.releasesFactory.findManyByProjectId(project._id.toString()); + } catch (error) { + console.error('Error fetching releases:', error); + throw new Error('Failed to get the releases'); + } + }, + /** * Find project events * diff --git a/src/typeDefs/event.ts b/src/typeDefs/event.ts index 8d609a52..ef5e45fb 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/event.ts @@ -16,46 +16,6 @@ type SourceCodeLine { content: String } -""" -Release commit -""" -type Commit { - """ - Hash of the commit - """ - hash: String! - - """ - Commit author - """ - author: String! - - """ - Commit title - """ - title: String! - - """ - Commit creation date - """ - date: DateTime! -} - -""" -Release data of the corresponding event -""" -type Release { - """ - Release name - """ - releaseName: String! @renameFrom(name: "release") - - """ - Release commits - """ - commits: [Commit!]! -} - """ Event backtrace representation """ diff --git a/src/typeDefs/index.ts b/src/typeDefs/index.ts index 49b1a7ca..db1ea29f 100644 --- a/src/typeDefs/index.ts +++ b/src/typeDefs/index.ts @@ -17,6 +17,7 @@ import workspaceMutations from './workspaceMutations'; import chart from './chart'; import plans from './plans'; import seed from './seed'; +import release from './release'; import isE2E from '../utils/isE2E'; const rootSchema = gql` @@ -100,6 +101,7 @@ const typeDefinitions = [ workspaceMutations, chart, plans, + release, projectEventGroupingPattern, projectEventGroupingPatternMutations, ]; diff --git a/src/typeDefs/project.ts b/src/typeDefs/project.ts index f0fab0e2..b1be43fc 100644 --- a/src/typeDefs/project.ts +++ b/src/typeDefs/project.ts @@ -109,6 +109,11 @@ type Project { skip: Int = 0 ): [Event!] + """ + Project releases + """ + releases: [Release!]! + """ Returns recent events grouped by day """ diff --git a/src/typeDefs/release.ts b/src/typeDefs/release.ts new file mode 100644 index 00000000..99781f8e --- /dev/null +++ b/src/typeDefs/release.ts @@ -0,0 +1,78 @@ +import { gql } from 'apollo-server-express'; + +export default gql` +""" +Source map file details +""" +type SourceMapData { + """ + Source map filename + """ + mapFileName: String! + + """ + Original source filename + """ + originFileName: String! + + """ + File size in bytes + """ + size: Int! +} + +""" +Release commit +""" +type Commit { + """ + Hash of the commit + """ + hash: String! + + """ + Commit author + """ + author: String! + + """ + Commit title + """ + title: String! + + """ + Commit creation date + """ + date: DateTime! +} + +""" +Release data +""" +type Release { + """ + Release ID + """ + id: ID! @renameFrom(name: "_id") + + """ + Release name + """ + releaseName: String! @renameFrom(name: "release") + + """ + Project ID associated with the release + """ + projectId: ID! + + """ + Release commits + """ + commits: [Commit!]! + + """ + Source maps associated with the release + """ + files: [SourceMapData!]! +} +`; \ No newline at end of file diff --git a/src/types/graphql.ts b/src/types/graphql.ts index d3ee4095..57cd71ec 100644 --- a/src/types/graphql.ts +++ b/src/types/graphql.ts @@ -2,9 +2,9 @@ import UsersFactory from '../models/usersFactory'; import WorkspacesFactory from '../models/workspacesFactory'; import { GraphQLField } from 'graphql'; import ProjectsFactory from '../models/projectsFactory'; -// import Accounting from 'codex-accounting-sdk'; import PlansFactory from '../models/plansFactory'; import BusinessOperationsFactory from '../models/businessOperationsFactory'; +import ReleasesFactory from '../models/releaseFactory'; /** * Resolver's Context argument @@ -79,6 +79,11 @@ export interface ContextFactories { * Allows to work with the Business Operations models */ businessOperationsFactory: BusinessOperationsFactory; + + /** + * Allows to work with releases + */ + releasesFactory: ReleasesFactory; } /**