@@ -177,6 +177,79 @@ module.exports = {
177177 }
178178 } ,
179179
180+ /**
181+ * Update project rate limits settings
182+ *
183+ * @param {ResolverObj } _obj
184+ * @param {string } id - project id
185+ * @param {Object | null } rateLimitSettings - rate limit settings (null to remove)
186+ * @param {UserInContext } user - current authorized user {@see ../index.js}
187+ * @param {ContextFactories } factories - factories for working with models
188+ *
189+ * @returns {Project }
190+ */
191+ async updateProjectRateLimits ( _obj , { id, rateLimitSettings } , { user, factories } ) {
192+ const project = await factories . projectsFactory . findById ( id ) ;
193+
194+ if ( ! project ) {
195+ throw new ApolloError ( 'There is no project with that id' ) ;
196+ }
197+
198+ if ( project . workspaceId . toString ( ) === '6213b6a01e6281087467cc7a' ) {
199+ throw new ApolloError ( 'Unable to update demo project' ) ;
200+ }
201+
202+ // Validate rate limit settings if provided
203+ if ( rateLimitSettings ) {
204+ const { N, T } = rateLimitSettings ;
205+
206+ // Validate that N and T exist
207+ if ( ! N || ! T ) {
208+ throw new UserInputError (
209+ 'Rate limit settings must contain both N (threshold) and T (period) fields.'
210+ ) ;
211+ }
212+
213+ // Validate N (threshold) - must be positive integer > 0
214+ if ( typeof N !== 'number' || ! Number . isInteger ( N ) || N <= 0 ) {
215+ throw new UserInputError (
216+ 'Invalid rate limit threshold. Must be a positive integer greater than 0.'
217+ ) ;
218+ }
219+
220+ // Validate T (period) - must be positive integer >= 60 (1 minute)
221+ if ( typeof T !== 'number' || ! Number . isInteger ( T ) || T < 60 ) {
222+ throw new UserInputError (
223+ 'Invalid rate limit period. Must be a positive integer greater than or equal to 60 seconds.'
224+ ) ;
225+ }
226+
227+ // Validate reasonable maximums (prevent extremely large values)
228+ const MAX_THRESHOLD = 1000000000 ; // 1 billion
229+ const MAX_PERIOD = 60 * 60 * 24 * 31 ; // 1 month in seconds
230+
231+ if ( N > MAX_THRESHOLD ) {
232+ throw new UserInputError (
233+ `Rate limit threshold cannot exceed ${ MAX_THRESHOLD . toLocaleString ( ) } .`
234+ ) ;
235+ }
236+
237+ if ( T > MAX_PERIOD ) {
238+ throw new UserInputError (
239+ `Rate limit period cannot exceed ${ MAX_PERIOD . toLocaleString ( ) } seconds (1 month).`
240+ ) ;
241+ }
242+ }
243+
244+ try {
245+ return project . updateProject ( {
246+ rateLimitSettings : rateLimitSettings || null ,
247+ } ) ;
248+ } catch ( err ) {
249+ throw new ApolloError ( 'Failed to update project rate limit settings' , { originalError : err } ) ;
250+ }
251+ } ,
252+
180253 /**
181254 * Generates new project integration token by id
182255 *
@@ -374,5 +447,80 @@ module.exports = {
374447
375448 return factory . findChartData ( days , timezoneOffset ) ;
376449 } ,
450+
451+ /**
452+ * Returns list of not archived releases with number of events that were introduced in this release
453+ * We count events as new, cause payload.release only contain the same release name if the event is original
454+ *
455+ * @param {ProjectDBScheme } project - result of parent resolver
456+ * @returns {Promise<Array<{release: string, timestamp: number, newEventsCount: number, commitsCount: number, filesCount: number}>> }
457+ */
458+ async releases ( project ) {
459+ const releasesCollection = mongo . databases . events . collection ( 'releases' ) ;
460+
461+ const pipeline = [
462+ { $match : { projectId : project . _id . toString ( ) } } ,
463+ {
464+ $project : {
465+ release : {
466+ $convert : {
467+ input : '$release' ,
468+ to : 'string' ,
469+ onError : '' ,
470+ onNull : '' ,
471+ } ,
472+ } ,
473+ commitsCount : { $size : { $ifNull : [ '$commits' , [ ] ] } } ,
474+ filesCount : { $size : { $ifNull : [ '$files' , [ ] ] } } ,
475+ _releaseIdSec : { $floor : { $divide : [ { $toLong : { $toDate : '$_id' } } , 1000 ] } } ,
476+ } ,
477+ } ,
478+ {
479+ $lookup : {
480+ from : 'events:' + project . _id ,
481+ let : { rel : '$release' } ,
482+ pipeline : [
483+ {
484+ $match : {
485+ $expr : {
486+ $eq : [ {
487+ $convert : {
488+ input : '$payload.release' ,
489+ to : 'string' ,
490+ onError : '' ,
491+ onNull : '' ,
492+ } ,
493+ } , '$$rel' ] ,
494+ } ,
495+ } ,
496+ } ,
497+ {
498+ $group : {
499+ _id : null ,
500+ count : { $sum : 1 } ,
501+ } ,
502+ } ,
503+ ] ,
504+ as : 'eventAgg' ,
505+ } ,
506+ } ,
507+ {
508+ $project : {
509+ _id : 0 ,
510+ release : 1 ,
511+ commitsCount : 1 ,
512+ filesCount : 1 ,
513+ newEventsCount : { $ifNull : [ { $arrayElemAt : [ '$eventAgg.count' , 0 ] } , 0 ] } ,
514+ timestamp : '$_releaseIdSec' ,
515+ } ,
516+ } ,
517+ { $sort : { _id : - 1 } } ,
518+ ] ;
519+
520+ const cursor = releasesCollection . aggregate ( pipeline ) ;
521+ const result = await cursor . toArray ( ) ;
522+
523+ return result ;
524+ } ,
377525 } ,
378526} ;
0 commit comments