@forestadmin-experimental/datasource-cosmos 1.6.2 → 1.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collection.d.ts +5 -0
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +52 -16
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/model-builder/model.d.ts +34 -11
- package/dist/model-builder/model.d.ts.map +1 -1
- package/dist/model-builder/model.js +145 -85
- package/dist/utils/aggregation-converter.d.ts +6 -0
- package/dist/utils/aggregation-converter.d.ts.map +1 -1
- package/dist/utils/aggregation-converter.js +21 -7
- package/dist/utils/pagination-cache.d.ts +116 -0
- package/dist/utils/pagination-cache.d.ts.map +1 -0
- package/dist/utils/pagination-cache.js +157 -0
- package/dist/utils/partition-key-extractor.d.ts +19 -0
- package/dist/utils/partition-key-extractor.d.ts.map +1 -0
- package/dist/utils/partition-key-extractor.js +61 -0
- package/dist/utils/query-converter.d.ts +20 -0
- package/dist/utils/query-converter.d.ts.map +1 -1
- package/dist/utils/query-converter.js +19 -2
- package/dist/utils/query-validation-error.d.ts +21 -0
- package/dist/utils/query-validation-error.d.ts.map +1 -0
- package/dist/utils/query-validation-error.js +29 -0
- package/dist/utils/query-validator.d.ts +66 -0
- package/dist/utils/query-validator.d.ts.map +1 -0
- package/dist/utils/query-validator.js +218 -0
- package/dist/utils/retry-handler.d.ts +101 -0
- package/dist/utils/retry-handler.d.ts.map +1 -0
- package/dist/utils/retry-handler.js +210 -0
- package/package.json +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.createQueryValidator = exports.QueryValidationErrorCode = exports.QueryValidationError = void 0;
|
|
27
|
+
const query_validation_error_1 = __importStar(require("./query-validation-error"));
|
|
28
|
+
exports.QueryValidationError = query_validation_error_1.default;
|
|
29
|
+
Object.defineProperty(exports, "QueryValidationErrorCode", { enumerable: true, get: function () { return query_validation_error_1.QueryValidationErrorCode; } });
|
|
30
|
+
// Characters that are dangerous in Cosmos DB SQL queries
|
|
31
|
+
// These patterns could be used for injection attacks
|
|
32
|
+
const DANGEROUS_PATTERNS = [
|
|
33
|
+
/[;'"\\]/, // SQL injection characters
|
|
34
|
+
/--/, // SQL comment
|
|
35
|
+
/\/\*/, // Block comment start
|
|
36
|
+
/\*\//, // Block comment end
|
|
37
|
+
/\bSELECT\b/i, // SQL keyword
|
|
38
|
+
/\bFROM\b/i, // SQL keyword
|
|
39
|
+
/\bWHERE\b/i, // SQL keyword
|
|
40
|
+
/\bDROP\b/i, // SQL keyword
|
|
41
|
+
/\bDELETE\b/i, // SQL keyword
|
|
42
|
+
/\bUPDATE\b/i, // SQL keyword
|
|
43
|
+
/\bINSERT\b/i, // SQL keyword
|
|
44
|
+
/\bEXEC\b/i, // SQL keyword
|
|
45
|
+
/\bUNION\b/i, // SQL keyword
|
|
46
|
+
/\0/, // Null byte
|
|
47
|
+
];
|
|
48
|
+
// Valid field name pattern: alphanumeric, underscore, and arrow notation for nesting
|
|
49
|
+
// Must start with a letter or underscore
|
|
50
|
+
const VALID_FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(?:->?[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
51
|
+
// Special Cosmos DB system fields that are always valid
|
|
52
|
+
const SYSTEM_FIELDS = new Set(['id', '_ts', '_etag', '_rid', '_self', '_attachments']);
|
|
53
|
+
class QueryValidator {
|
|
54
|
+
constructor(schemaFields, options = {}) {
|
|
55
|
+
this.schema = null;
|
|
56
|
+
this.conditionCount = 0;
|
|
57
|
+
this.options = {
|
|
58
|
+
maxFieldDepth: options.maxFieldDepth ?? 10,
|
|
59
|
+
validateAgainstSchema: options.validateAgainstSchema ?? schemaFields !== undefined,
|
|
60
|
+
allowUnknownFields: options.allowUnknownFields ?? false,
|
|
61
|
+
maxFieldNameLength: options.maxFieldNameLength ?? 256,
|
|
62
|
+
maxConditions: options.maxConditions ?? 100,
|
|
63
|
+
};
|
|
64
|
+
if (schemaFields) {
|
|
65
|
+
this.schema = new Map();
|
|
66
|
+
for (const field of schemaFields) {
|
|
67
|
+
this.schema.set(field, true);
|
|
68
|
+
// Also add parent paths as valid for nested fields
|
|
69
|
+
// e.g., for 'address->city', 'address' is also valid
|
|
70
|
+
const parts = field.split('->');
|
|
71
|
+
for (let i = 1; i < parts.length; i += 1) {
|
|
72
|
+
const parentPath = parts.slice(0, i).join('->');
|
|
73
|
+
this.schema.set(parentPath, true);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Validate a field name for security and correctness
|
|
80
|
+
*/
|
|
81
|
+
validateFieldName(field, context = 'field') {
|
|
82
|
+
// Check length
|
|
83
|
+
if (field.length > this.options.maxFieldNameLength) {
|
|
84
|
+
throw new query_validation_error_1.default(`${context} name exceeds maximum length of ${this.options.maxFieldNameLength} characters`, query_validation_error_1.QueryValidationErrorCode.INVALID_FIELD_NAME, field);
|
|
85
|
+
}
|
|
86
|
+
// Check for empty field
|
|
87
|
+
if (!field || field.trim() === '') {
|
|
88
|
+
throw new query_validation_error_1.default(`${context} name cannot be empty`, query_validation_error_1.QueryValidationErrorCode.INVALID_FIELD_NAME, field);
|
|
89
|
+
}
|
|
90
|
+
// Check for dangerous patterns (potential injection)
|
|
91
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
92
|
+
if (pattern.test(field)) {
|
|
93
|
+
throw new query_validation_error_1.default(`${context} contains potentially dangerous characters or patterns`, query_validation_error_1.QueryValidationErrorCode.POTENTIAL_INJECTION, field);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Skip further validation for system fields
|
|
97
|
+
if (SYSTEM_FIELDS.has(field)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Check valid field name pattern
|
|
101
|
+
if (!VALID_FIELD_NAME_PATTERN.test(field)) {
|
|
102
|
+
throw new query_validation_error_1.default(`${context} '${field}' contains invalid characters. ` +
|
|
103
|
+
`Field names must start with a letter or underscore and contain only ` +
|
|
104
|
+
`alphanumeric characters, underscores, and '->' for nesting.`, query_validation_error_1.QueryValidationErrorCode.INVALID_FIELD_NAME, field);
|
|
105
|
+
}
|
|
106
|
+
// Check nesting depth
|
|
107
|
+
const depth = (field.match(/->/g) || []).length + 1;
|
|
108
|
+
if (depth > this.options.maxFieldDepth) {
|
|
109
|
+
throw new query_validation_error_1.default(`${context} '${field}' exceeds maximum nesting depth of ${this.options.maxFieldDepth}`, query_validation_error_1.QueryValidationErrorCode.MAX_DEPTH_EXCEEDED, field);
|
|
110
|
+
}
|
|
111
|
+
// Validate against schema if enabled
|
|
112
|
+
if (this.options.validateAgainstSchema && this.schema && !this.options.allowUnknownFields) {
|
|
113
|
+
if (!this.schema.has(field) && !SYSTEM_FIELDS.has(field)) {
|
|
114
|
+
throw new query_validation_error_1.default(`${context} '${field}' is not defined in the collection schema`, query_validation_error_1.QueryValidationErrorCode.FIELD_NOT_IN_SCHEMA, field);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Validate a complete condition tree
|
|
120
|
+
*/
|
|
121
|
+
validateConditionTree(conditionTree) {
|
|
122
|
+
if (!conditionTree)
|
|
123
|
+
return;
|
|
124
|
+
this.conditionCount = 0;
|
|
125
|
+
this.validateConditionTreeRecursive(conditionTree);
|
|
126
|
+
}
|
|
127
|
+
validateConditionTreeRecursive(conditionTree) {
|
|
128
|
+
// Check condition count limit
|
|
129
|
+
this.conditionCount += 1;
|
|
130
|
+
if (this.conditionCount > this.options.maxConditions) {
|
|
131
|
+
throw new query_validation_error_1.default(`Condition tree exceeds maximum of ${this.options.maxConditions} conditions`, query_validation_error_1.QueryValidationErrorCode.MAX_DEPTH_EXCEEDED);
|
|
132
|
+
}
|
|
133
|
+
if (conditionTree.aggregator !== undefined) {
|
|
134
|
+
const { conditions } = conditionTree;
|
|
135
|
+
if (conditions) {
|
|
136
|
+
for (const condition of conditions) {
|
|
137
|
+
this.validateConditionTreeRecursive(condition);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (conditionTree.operator !== undefined) {
|
|
143
|
+
const { field, value } = conditionTree;
|
|
144
|
+
this.validateFieldName(field, 'Filter field');
|
|
145
|
+
this.validateValue(value, field);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Validate a value for potential injection
|
|
150
|
+
*/
|
|
151
|
+
validateValue(value, field) {
|
|
152
|
+
if (value === null || value === undefined) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Check string values for injection attempts
|
|
156
|
+
if (typeof value === 'string') {
|
|
157
|
+
// Only check for null bytes in string values - other characters are safe
|
|
158
|
+
// when used with parameterized queries
|
|
159
|
+
if (value.includes('\0')) {
|
|
160
|
+
throw new query_validation_error_1.default(`Value for field '${field}' contains null bytes`, query_validation_error_1.QueryValidationErrorCode.POTENTIAL_INJECTION, field);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Recursively check array values
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
for (const item of value) {
|
|
166
|
+
this.validateValue(item, field);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Check object values (for complex filters)
|
|
170
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
171
|
+
for (const key of Object.keys(value)) {
|
|
172
|
+
this.validateFieldName(key, `Object key in value for '${field}'`);
|
|
173
|
+
this.validateValue(value[key], `${field}.${key}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Validate sort fields
|
|
179
|
+
*/
|
|
180
|
+
validateSort(sort) {
|
|
181
|
+
if (!sort || sort.length === 0)
|
|
182
|
+
return;
|
|
183
|
+
for (const sortClause of sort) {
|
|
184
|
+
this.validateFieldName(sortClause.field, 'Sort field');
|
|
185
|
+
// Ensure ascending is a boolean
|
|
186
|
+
if (typeof sortClause.ascending !== 'boolean') {
|
|
187
|
+
throw new query_validation_error_1.default(`Sort direction for '${sortClause.field}' must be a boolean`, query_validation_error_1.QueryValidationErrorCode.INVALID_SORT_FIELD, sortClause.field);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Validate projection fields
|
|
193
|
+
*/
|
|
194
|
+
validateProjection(projection) {
|
|
195
|
+
if (!projection || projection.length === 0)
|
|
196
|
+
return;
|
|
197
|
+
for (const field of projection) {
|
|
198
|
+
this.validateFieldName(field, 'Projection field');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Validate all query parameters at once
|
|
203
|
+
*/
|
|
204
|
+
validateQuery(conditionTree, sort, projection) {
|
|
205
|
+
this.validateConditionTree(conditionTree);
|
|
206
|
+
this.validateSort(sort);
|
|
207
|
+
this.validateProjection(projection);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
exports.default = QueryValidator;
|
|
211
|
+
/**
|
|
212
|
+
* Create a validator instance for a given schema
|
|
213
|
+
*/
|
|
214
|
+
function createQueryValidator(schemaFields, options) {
|
|
215
|
+
return new QueryValidator(schemaFields, options);
|
|
216
|
+
}
|
|
217
|
+
exports.createQueryValidator = createQueryValidator;
|
|
218
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicXVlcnktdmFsaWRhdG9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3F1ZXJ5LXZhbGlkYXRvci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQU9BLG1GQUEwRjtBQUVqRiwrQkFGRixnQ0FBb0IsQ0FFRTtBQUFFLHlHQUZBLGlEQUF3QixPQUVBO0FBa0N2RCx5REFBeUQ7QUFDekQscURBQXFEO0FBQ3JELE1BQU0sa0JBQWtCLEdBQUc7SUFDekIsU0FBUyxFQUFFLDJCQUEyQjtJQUN0QyxJQUFJLEVBQUUsY0FBYztJQUNwQixNQUFNLEVBQUUsc0JBQXNCO0lBQzlCLE1BQU0sRUFBRSxvQkFBb0I7SUFDNUIsYUFBYSxFQUFFLGNBQWM7SUFDN0IsV0FBVyxFQUFFLGNBQWM7SUFDM0IsWUFBWSxFQUFFLGNBQWM7SUFDNUIsV0FBVyxFQUFFLGNBQWM7SUFDM0IsYUFBYSxFQUFFLGNBQWM7SUFDN0IsYUFBYSxFQUFFLGNBQWM7SUFDN0IsYUFBYSxFQUFFLGNBQWM7SUFDN0IsV0FBVyxFQUFFLGNBQWM7SUFDM0IsWUFBWSxFQUFFLGNBQWM7SUFDNUIsSUFBSSxFQUFFLFlBQVk7Q0FDbkIsQ0FBQztBQUVGLHFGQUFxRjtBQUNyRix5Q0FBeUM7QUFDekMsTUFBTSx3QkFBd0IsR0FBRyx3REFBd0QsQ0FBQztBQUUxRix3REFBd0Q7QUFDeEQsTUFBTSxhQUFhLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLE9BQU8sRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLGNBQWMsQ0FBQyxDQUFDLENBQUM7QUFFdkYsTUFBcUIsY0FBYztJQUtqQyxZQUFZLFlBQXVCLEVBQUUsVUFBaUMsRUFBRTtRQUhoRSxXQUFNLEdBQWdDLElBQUksQ0FBQztRQUMzQyxtQkFBYyxHQUFHLENBQUMsQ0FBQztRQUd6QixJQUFJLENBQUMsT0FBTyxHQUFHO1lBQ2IsYUFBYSxF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
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry handler for Cosmos DB rate limiting (429 errors)
|
|
3
|
+
*
|
|
4
|
+
* Cosmos DB returns 429 (Too Many Requests) when RU/s limits are exceeded.
|
|
5
|
+
* The response includes a 'x-ms-retry-after-ms' header indicating how long to wait.
|
|
6
|
+
* This handler implements exponential backoff with respect to Cosmos DB's retry-after hints.
|
|
7
|
+
*/
|
|
8
|
+
export interface RetryOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Maximum number of retry attempts
|
|
11
|
+
* Default: 9 (aligns with Azure SDK default)
|
|
12
|
+
*/
|
|
13
|
+
maxRetries?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Initial delay in milliseconds before first retry
|
|
16
|
+
* Default: 1000 (1 second)
|
|
17
|
+
*/
|
|
18
|
+
initialDelayMs?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Maximum delay in milliseconds between retries
|
|
21
|
+
* Default: 30000 (30 seconds)
|
|
22
|
+
*/
|
|
23
|
+
maxDelayMs?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Multiplier for exponential backoff
|
|
26
|
+
* Default: 2
|
|
27
|
+
*/
|
|
28
|
+
backoffMultiplier?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Add random jitter to prevent thundering herd
|
|
31
|
+
* Default: true
|
|
32
|
+
*/
|
|
33
|
+
useJitter?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Optional callback for retry events (useful for logging/monitoring)
|
|
36
|
+
*/
|
|
37
|
+
onRetry?: (attempt: number, delayMs: number, error: Error) => void;
|
|
38
|
+
}
|
|
39
|
+
export interface RetryableError extends Error {
|
|
40
|
+
code?: number | string;
|
|
41
|
+
headers?: {
|
|
42
|
+
'x-ms-retry-after-ms'?: string;
|
|
43
|
+
[key: string]: string | undefined;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check if an error is a retryable rate limit error (429)
|
|
48
|
+
*/
|
|
49
|
+
export declare function isRateLimitError(error: unknown): error is RetryableError;
|
|
50
|
+
/**
|
|
51
|
+
* Check if an error is a retryable transient error (e.g., network issues, service unavailable)
|
|
52
|
+
*/
|
|
53
|
+
export declare function isTransientError(error: unknown): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Extract retry-after delay from Cosmos DB error response
|
|
56
|
+
*/
|
|
57
|
+
export declare function getRetryAfterMs(error: RetryableError): number | null;
|
|
58
|
+
/**
|
|
59
|
+
* Calculate delay for the next retry attempt using exponential backoff
|
|
60
|
+
*/
|
|
61
|
+
export declare function calculateBackoffDelay(attempt: number, options: Required<Omit<RetryOptions, 'onRetry'>>, retryAfterMs?: number | null): number;
|
|
62
|
+
/**
|
|
63
|
+
* Execute an async operation with retry logic for rate limiting
|
|
64
|
+
*
|
|
65
|
+
* @param operation The async operation to execute
|
|
66
|
+
* @param options Retry configuration options
|
|
67
|
+
* @returns The result of the operation
|
|
68
|
+
* @throws The last error if all retries are exhausted
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const result = await withRetry(
|
|
73
|
+
* () => container.items.create(item),
|
|
74
|
+
* { maxRetries: 5, onRetry: (attempt, delay) => console.log(`Retry ${attempt}`) }
|
|
75
|
+
* );
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function withRetry<T>(operation: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
79
|
+
/**
|
|
80
|
+
* Create a retry wrapper with pre-configured options
|
|
81
|
+
*
|
|
82
|
+
* @param defaultOptions Default retry options to use
|
|
83
|
+
* @returns A withRetry function with the default options applied
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* const retryWithDefaults = createRetryWrapper({ maxRetries: 5 });
|
|
88
|
+
* const result = await retryWithDefaults(() => container.items.create(item));
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare function createRetryWrapper(defaultOptions: RetryOptions): <T>(operation: () => Promise<T>, overrideOptions?: RetryOptions) => Promise<T>;
|
|
92
|
+
/**
|
|
93
|
+
* Configure the shared retry options for all Cosmos DB operations
|
|
94
|
+
* @param options Retry configuration
|
|
95
|
+
*/
|
|
96
|
+
export declare function configureRetryOptions(options: RetryOptions): void;
|
|
97
|
+
/**
|
|
98
|
+
* Get the current shared retry options
|
|
99
|
+
*/
|
|
100
|
+
export declare function getSharedRetryOptions(): RetryOptions;
|
|
101
|
+
//# sourceMappingURL=retry-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-handler.d.ts","sourceRoot":"","sources":["../../src/utils/retry-handler.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACpE;AAED,MAAM,WAAW,cAAe,SAAQ,KAAK;IAC3C,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE;QACR,qBAAqB,CAAC,EAAE,MAAM,CAAC;QAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KACnC,CAAC;CACH;AAaD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAmBxE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAoBxD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAwBpE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC,EAChD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC3B,MAAM,CA6BR;AAWD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,CAAC,CAAC,CA4CZ;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,cAAc,EAAE,YAAY,GAC3B,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,CAGhF;AAQD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEjE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,YAAY,CAEpD"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Retry handler for Cosmos DB rate limiting (429 errors)
|
|
4
|
+
*
|
|
5
|
+
* Cosmos DB returns 429 (Too Many Requests) when RU/s limits are exceeded.
|
|
6
|
+
* The response includes a 'x-ms-retry-after-ms' header indicating how long to wait.
|
|
7
|
+
* This handler implements exponential backoff with respect to Cosmos DB's retry-after hints.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.getSharedRetryOptions = exports.configureRetryOptions = exports.createRetryWrapper = exports.withRetry = exports.calculateBackoffDelay = exports.getRetryAfterMs = exports.isTransientError = exports.isRateLimitError = void 0;
|
|
11
|
+
/**
|
|
12
|
+
* Default retry configuration
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_OPTIONS = {
|
|
15
|
+
maxRetries: 9,
|
|
16
|
+
initialDelayMs: 1000,
|
|
17
|
+
maxDelayMs: 30000,
|
|
18
|
+
backoffMultiplier: 2,
|
|
19
|
+
useJitter: true,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Check if an error is a retryable rate limit error (429)
|
|
23
|
+
*/
|
|
24
|
+
function isRateLimitError(error) {
|
|
25
|
+
if (!error || typeof error !== 'object')
|
|
26
|
+
return false;
|
|
27
|
+
const err = error;
|
|
28
|
+
// Check for Cosmos DB 429 error code
|
|
29
|
+
if (err.code === 429)
|
|
30
|
+
return true;
|
|
31
|
+
// Check message for rate limiting indicators
|
|
32
|
+
if (err.message?.includes('429') || err.message?.toLowerCase().includes('too many requests')) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// Check for TooManyRequests in error name or message
|
|
36
|
+
if (err.name === 'TooManyRequests' || err.message?.includes('TooManyRequests')) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
exports.isRateLimitError = isRateLimitError;
|
|
42
|
+
/**
|
|
43
|
+
* Check if an error is a retryable transient error (e.g., network issues, service unavailable)
|
|
44
|
+
*/
|
|
45
|
+
function isTransientError(error) {
|
|
46
|
+
if (!error || typeof error !== 'object')
|
|
47
|
+
return false;
|
|
48
|
+
const err = error;
|
|
49
|
+
const { code } = err;
|
|
50
|
+
// Service unavailable (503), Gateway timeout (504), Request timeout (408)
|
|
51
|
+
if (code === 503 || code === 504 || code === 408)
|
|
52
|
+
return true;
|
|
53
|
+
// Network-related errors
|
|
54
|
+
if (err.message?.includes('ECONNRESET') ||
|
|
55
|
+
err.message?.includes('ETIMEDOUT') ||
|
|
56
|
+
err.message?.includes('ENOTFOUND') ||
|
|
57
|
+
err.message?.includes('socket hang up')) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
exports.isTransientError = isTransientError;
|
|
63
|
+
/**
|
|
64
|
+
* Extract retry-after delay from Cosmos DB error response
|
|
65
|
+
*/
|
|
66
|
+
function getRetryAfterMs(error) {
|
|
67
|
+
// Check for x-ms-retry-after-ms header (Cosmos DB specific)
|
|
68
|
+
const retryAfterMs = error.headers?.['x-ms-retry-after-ms'];
|
|
69
|
+
if (retryAfterMs) {
|
|
70
|
+
const parsed = parseInt(retryAfterMs, 10);
|
|
71
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Check for standard Retry-After header (in seconds)
|
|
76
|
+
const retryAfter = error.headers?.['retry-after'];
|
|
77
|
+
if (retryAfter) {
|
|
78
|
+
const parsed = parseInt(retryAfter, 10);
|
|
79
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
80
|
+
return parsed * 1000; // Convert to milliseconds
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
exports.getRetryAfterMs = getRetryAfterMs;
|
|
86
|
+
/**
|
|
87
|
+
* Calculate delay for the next retry attempt using exponential backoff
|
|
88
|
+
*/
|
|
89
|
+
function calculateBackoffDelay(attempt, options, retryAfterMs) {
|
|
90
|
+
// If Cosmos DB specified a retry-after delay, use it as the base
|
|
91
|
+
if (retryAfterMs && retryAfterMs > 0) {
|
|
92
|
+
// Add a small buffer to the Cosmos DB suggested delay
|
|
93
|
+
const bufferedDelay = Math.min(retryAfterMs * 1.1, options.maxDelayMs);
|
|
94
|
+
if (options.useJitter) {
|
|
95
|
+
// Add up to 20% jitter
|
|
96
|
+
const jitter = Math.random() * 0.2 * bufferedDelay;
|
|
97
|
+
return Math.min(bufferedDelay + jitter, options.maxDelayMs);
|
|
98
|
+
}
|
|
99
|
+
return bufferedDelay;
|
|
100
|
+
}
|
|
101
|
+
// Calculate exponential backoff: initialDelay * (multiplier ^ attempt)
|
|
102
|
+
const exponentialDelay = options.initialDelayMs * options.backoffMultiplier ** attempt;
|
|
103
|
+
const cappedDelay = Math.min(exponentialDelay, options.maxDelayMs);
|
|
104
|
+
if (options.useJitter) {
|
|
105
|
+
// Add jitter: randomize between 50% and 100% of the delay
|
|
106
|
+
const jitterMin = cappedDelay * 0.5;
|
|
107
|
+
const jitterMax = cappedDelay;
|
|
108
|
+
return jitterMin + Math.random() * (jitterMax - jitterMin);
|
|
109
|
+
}
|
|
110
|
+
return cappedDelay;
|
|
111
|
+
}
|
|
112
|
+
exports.calculateBackoffDelay = calculateBackoffDelay;
|
|
113
|
+
/**
|
|
114
|
+
* Sleep for the specified duration
|
|
115
|
+
*/
|
|
116
|
+
function sleep(ms) {
|
|
117
|
+
return new Promise(resolve => {
|
|
118
|
+
setTimeout(resolve, ms);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Execute an async operation with retry logic for rate limiting
|
|
123
|
+
*
|
|
124
|
+
* @param operation The async operation to execute
|
|
125
|
+
* @param options Retry configuration options
|
|
126
|
+
* @returns The result of the operation
|
|
127
|
+
* @throws The last error if all retries are exhausted
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const result = await withRetry(
|
|
132
|
+
* () => container.items.create(item),
|
|
133
|
+
* { maxRetries: 5, onRetry: (attempt, delay) => console.log(`Retry ${attempt}`) }
|
|
134
|
+
* );
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
async function withRetry(operation, options) {
|
|
138
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
139
|
+
let lastError = null;
|
|
140
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {
|
|
141
|
+
try {
|
|
142
|
+
// eslint-disable-next-line no-await-in-loop -- Sequential retry logic
|
|
143
|
+
return await operation();
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
lastError = error;
|
|
147
|
+
// Check if error is retryable
|
|
148
|
+
const isRateLimit = isRateLimitError(error);
|
|
149
|
+
const isTransient = isTransientError(error);
|
|
150
|
+
if (!isRateLimit && !isTransient) {
|
|
151
|
+
// Non-retryable error, throw immediately
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
// Check if we've exhausted retries
|
|
155
|
+
if (attempt >= config.maxRetries) {
|
|
156
|
+
throw new Error(`Operation failed after ${config.maxRetries} retries. Last error: ${lastError.message}`);
|
|
157
|
+
}
|
|
158
|
+
// Calculate delay for next retry
|
|
159
|
+
const retryAfterMs = isRateLimit ? getRetryAfterMs(error) : null;
|
|
160
|
+
const delayMs = calculateBackoffDelay(attempt, config, retryAfterMs);
|
|
161
|
+
// Call onRetry callback if provided
|
|
162
|
+
if (options?.onRetry) {
|
|
163
|
+
options.onRetry(attempt + 1, delayMs, lastError);
|
|
164
|
+
}
|
|
165
|
+
// Wait before retrying
|
|
166
|
+
// eslint-disable-next-line no-await-in-loop -- Required for retry delay
|
|
167
|
+
await sleep(delayMs);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// This should never be reached, but TypeScript needs it
|
|
171
|
+
throw lastError || new Error('Retry failed unexpectedly');
|
|
172
|
+
}
|
|
173
|
+
exports.withRetry = withRetry;
|
|
174
|
+
/**
|
|
175
|
+
* Create a retry wrapper with pre-configured options
|
|
176
|
+
*
|
|
177
|
+
* @param defaultOptions Default retry options to use
|
|
178
|
+
* @returns A withRetry function with the default options applied
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* const retryWithDefaults = createRetryWrapper({ maxRetries: 5 });
|
|
183
|
+
* const result = await retryWithDefaults(() => container.items.create(item));
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
function createRetryWrapper(defaultOptions) {
|
|
187
|
+
return (operation, overrideOptions) => withRetry(operation, { ...defaultOptions, ...overrideOptions });
|
|
188
|
+
}
|
|
189
|
+
exports.createRetryWrapper = createRetryWrapper;
|
|
190
|
+
/**
|
|
191
|
+
* Shared retry options instance for all models
|
|
192
|
+
* This allows centralized configuration of retry behavior
|
|
193
|
+
*/
|
|
194
|
+
let sharedRetryOptions = {};
|
|
195
|
+
/**
|
|
196
|
+
* Configure the shared retry options for all Cosmos DB operations
|
|
197
|
+
* @param options Retry configuration
|
|
198
|
+
*/
|
|
199
|
+
function configureRetryOptions(options) {
|
|
200
|
+
sharedRetryOptions = { ...options };
|
|
201
|
+
}
|
|
202
|
+
exports.configureRetryOptions = configureRetryOptions;
|
|
203
|
+
/**
|
|
204
|
+
* Get the current shared retry options
|
|
205
|
+
*/
|
|
206
|
+
function getSharedRetryOptions() {
|
|
207
|
+
return { ...sharedRetryOptions };
|
|
208
|
+
}
|
|
209
|
+
exports.getSharedRetryOptions = getSharedRetryOptions;
|
|
210
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmV0cnktaGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9yZXRyeS1oYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7Ozs7O0dBTUc7OztBQStDSDs7R0FFRztBQUNILE1BQU0sZUFBZSxHQUE0QztJQUMvRCxVQUFVLEVBQUUsQ0FBQztJQUNiLGNBQWMsRUFBRSxJQUFJO0lBQ3BCLFVBQVUsRUFBRSxLQUFLO0lBQ2pCLGlCQUFpQixFQUFFLENBQUM7SUFDcEIsU0FBUyxFQUFFLElBQUk7Q0FDaEIsQ0FBQztBQUVGOztHQUVHO0FBQ0gsU0FBZ0IsZ0JBQWdCLENBQUMsS0FBYztJQUM3QyxJQUFJLENBQUMsS0FBSyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVE7UUFBRSxPQUFPLEtBQUssQ0FBQztJQUV0RCxNQUFNLEdBQUcsR0FBRyxLQUF1QixDQUFDO0lBRXBDLHFDQUFxQztJQUNyQyxJQUFJLEdBQUcsQ0FBQyxJQUFJLEtBQUssR0FBRztRQUFFLE9BQU8sSUFBSSxDQUFDO0lBRWxDLDZDQUE2QztJQUM3QyxJQUFJLEdBQUcsQ0FBQyxPQUFPLEVBQUUsUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLEdBQUcsQ0FBQyxPQUFPLEVBQUUsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLG1CQUFtQixDQUFDLEVBQUUsQ0FBQztRQUM3RixPQUFPLElBQUksQ0FBQztJQUNkLENBQUM7SUFFRCxxREFBcUQ7SUFDckQsSUFBSSxHQUFHLENBQUMsSUFBSSxLQUFLLGlCQUFpQixJQUFJLEdBQUcsQ0FBQyxPQUFPLEVBQUUsUUFBUSxDQUFDLGlCQUFpQixDQUFDLEVBQUUsQ0FBQztRQUMvRSxPQUFPLElBQUksQ0FBQztJQUNkLENBQUM7SUFFRCxPQUFPLEtBQUssQ0FBQztBQUNmLENBQUM7QUFuQkQsNENBbUJDO0FBRUQ7O0dBRUc7QUFDSCxTQUFnQixnQkFBZ0IsQ0FBQyxLQUFjO0lBQzdDLElBQUksQ0FB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
|