@@ -36,10 +36,77 @@ const paidReservedPlans = new Set<string>([
3636 ReservedPlanId . Pro202509 ,
3737] ) ;
3838
39+ /**
40+ * Options for quota guard operations.
41+ */
42+ type GuardTenantUsageOptions = {
43+ /**
44+ * Entity ID for entity-based quota limits.
45+ *
46+ * @remarks
47+ * Required for entity-based quotas like `scopesPerRoleLimit`, `scopesPerResourceLimit`, etc.
48+ * Optional for tenant-level quotas like `applicationsLimit`, `resourcesLimit`, etc.
49+ *
50+ * @example
51+ * ```ts
52+ * // Entity-based quota: entityId is required
53+ * await quota.guardTenantUsageByKey('scopesPerRoleLimit', { entityId: roleId });
54+ *
55+ * // Tenant-level quota: entityId not needed
56+ * await quota.guardTenantUsageByKey('applicationsLimit');
57+ * ```
58+ */
59+ entityId ?: string ;
60+
61+ /**
62+ * The number of resources to consume in this operation.
63+ *
64+ * @remarks
65+ * This is used to validate batch operations where multiple resources are added at once.
66+ * For example, when assigning 10 scopes to a role in a single request, set this to 10.
67+ *
68+ * The guard will check: `currentUsage + consumeUsageCount <= limit`
69+ *
70+ * @default 1 - Assumes single resource consumption if not specified
71+ *
72+ * @example
73+ * ```ts
74+ * // Adding a single application (default behavior)
75+ * await quota.guardTenantUsageByKey('applicationsLimit');
76+ *
77+ * // Adding 5 scopes to a role at once
78+ * await quota.guardTenantUsageByKey('scopesPerRoleLimit', {
79+ * entityId: roleId,
80+ * consumeUsageCount: 5,
81+ * });
82+ * ```
83+ */
84+ consumeUsageCount ?: number ;
85+ } ;
86+
87+ /**
88+ * Function type for guarding tenant usage against quota limits.
89+ * Provides type-safe overloads for tenant-level and entity-based quotas.
90+ */
3991type GuardTenantUsageByKeyFunction = {
40- ( key : Exclude < UsageKey , EntityBasedUsageKey > , entityId ?: string ) : Promise < void > ;
41- ( key : EntityBasedUsageKey , entityId : string ) : Promise < void > ;
92+ /**
93+ * Guard tenant-level quota (e.g., applicationsLimit, resourcesLimit)
94+ * @param key - Tenant-level quota key
95+ * @param options - Optional guard options
96+ */
97+ ( key : Exclude < UsageKey , EntityBasedUsageKey > , options ?: GuardTenantUsageOptions ) : Promise < void > ;
98+
99+ /**
100+ * Guard entity-based quota (e.g., scopesPerRoleLimit, scopesPerResourceLimit)
101+ * @param key - Entity-based quota key
102+ * @param options - Guard options with required entityId
103+ */
104+ (
105+ key : EntityBasedUsageKey ,
106+ options : GuardTenantUsageOptions & { entityId : string }
107+ ) : Promise < void > ;
42108} ;
109+
43110export class QuotaLibrary {
44111 constructor (
45112 public readonly tenantId : string ,
@@ -49,7 +116,44 @@ export class QuotaLibrary {
49116 private readonly subscription : SubscriptionLibrary
50117 ) { }
51118
52- guardTenantUsageByKey : GuardTenantUsageByKeyFunction = async ( key , entityId ) => {
119+ /**
120+ * Guards tenant usage against quota and system limits before performing an operation.
121+ *
122+ * @remarks
123+ * This method checks if the current usage plus the resources to be consumed would exceed
124+ * the configured limits. It validates against both system limits (hard caps) and
125+ * subscription quota limits.
126+ *
127+ * The check formula is: `currentUsage + consumeUsageCount <= limit`
128+ *
129+ * If the limit would be exceeded, this method throws a RequestError. Otherwise, it
130+ * returns silently, allowing the operation to proceed.
131+ *
132+ * @param key - The quota key to check (e.g., 'applicationsLimit', 'scopesPerRoleLimit')
133+ * @param options - Optional configuration for the guard operation
134+ * @param options.entityId - Entity ID for entity-based quotas (required for entity-based keys)
135+ * @param options.consumeUsageCount - Number of resources to consume (default: 1)
136+ *
137+ * @throws {RequestError } With code 'system_limit.limit_exceeded' if system limit is exceeded
138+ * @throws {RequestError } With code 'subscription.limit_exceeded' if quota limit is exceeded
139+ *
140+ * @example
141+ * ```ts
142+ * // Guard before adding a single application
143+ * await quota.guardTenantUsageByKey('applicationsLimit');
144+ *
145+ * // Guard before adding multiple scopes to a role
146+ * const scopeIds = ['scope1', 'scope2', 'scope3'];
147+ * await quota.guardTenantUsageByKey('scopesPerRoleLimit', {
148+ * entityId: 'role_id',
149+ * consumeUsageCount: scopeIds.length,
150+ * });
151+ * ```
152+ */
153+ guardTenantUsageByKey : GuardTenantUsageByKeyFunction = async (
154+ key ,
155+ { entityId, consumeUsageCount = 1 } = { }
156+ ) => {
53157 const { isCloud } = EnvSet . values ;
54158
55159 // Cloud only feature, skip in non-cloud environments
@@ -67,7 +171,13 @@ export class QuotaLibrary {
67171
68172 // Todo: @xiaoyijun : remove dev feature flag
69173 if ( EnvSet . values . isDevFeaturesEnabled && isSystemUsageKey ( key ) ) {
70- await this . assertSystemLimit ( { key, entityId, subscriptionData, tenantUsageQuery } ) ;
174+ await this . assertSystemLimit ( {
175+ key,
176+ entityId,
177+ subscriptionData,
178+ tenantUsageQuery,
179+ consumeUsageCount,
180+ } ) ;
71181 }
72182
73183 if ( isQuotaUsageKey ( key ) ) {
@@ -76,6 +186,7 @@ export class QuotaLibrary {
76186 entityId,
77187 subscriptionData,
78188 tenantUsageQuery,
189+ consumeUsageCount,
79190 } ) ;
80191 }
81192 } ;
@@ -118,11 +229,13 @@ export class QuotaLibrary {
118229 entityId,
119230 subscriptionData,
120231 tenantUsageQuery,
232+ consumeUsageCount,
121233 } : {
122234 key : SystemUsageKey ;
123235 entityId ?: string ;
124236 subscriptionData : Subscription ;
125237 tenantUsageQuery : TenantUsageQuery ;
238+ consumeUsageCount : number ;
126239 } ) => {
127240 const { systemLimit } = subscriptionData ;
128241
@@ -139,7 +252,7 @@ export class QuotaLibrary {
139252 const usage = await tenantUsageQuery . get ( key , entityId ) ;
140253
141254 assertThat (
142- usage < limit ,
255+ usage + consumeUsageCount <= limit ,
143256 new RequestError (
144257 {
145258 code : 'system_limit.limit_exceeded' ,
@@ -155,11 +268,13 @@ export class QuotaLibrary {
155268 entityId,
156269 subscriptionData,
157270 tenantUsageQuery,
271+ consumeUsageCount,
158272 } : {
159273 key : QuotaUsageKey ;
160274 entityId ?: string ;
161275 subscriptionData : Subscription ;
162276 tenantUsageQuery : TenantUsageQuery ;
277+ consumeUsageCount : number ;
163278 } ) => {
164279 const { planId, isEnterprisePlan, quota } = subscriptionData ;
165280
@@ -205,7 +320,7 @@ export class QuotaLibrary {
205320 const usage = await tenantUsageQuery . get ( key , entityId ) ;
206321
207322 assertThat (
208- usage < limit ,
323+ usage + consumeUsageCount <= limit ,
209324 new RequestError ( {
210325 code : 'subscription.limit_exceeded' ,
211326 status : 403 ,
0 commit comments