@rbaileysr/zephyr-managed-api 1.0.0

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.
Files changed (56) hide show
  1. package/README.md +618 -0
  2. package/dist/error-strategy.d.ts +69 -0
  3. package/dist/error-strategy.d.ts.map +1 -0
  4. package/dist/error-strategy.js +125 -0
  5. package/dist/groups/All.d.ts +90 -0
  6. package/dist/groups/All.d.ts.map +1 -0
  7. package/dist/groups/All.js +236 -0
  8. package/dist/groups/Automation.d.ts +75 -0
  9. package/dist/groups/Automation.d.ts.map +1 -0
  10. package/dist/groups/Automation.js +133 -0
  11. package/dist/groups/Environment.d.ts +73 -0
  12. package/dist/groups/Environment.d.ts.map +1 -0
  13. package/dist/groups/Environment.js +93 -0
  14. package/dist/groups/Folder.d.ts +55 -0
  15. package/dist/groups/Folder.d.ts.map +1 -0
  16. package/dist/groups/Folder.js +68 -0
  17. package/dist/groups/IssueLink.d.ts +59 -0
  18. package/dist/groups/IssueLink.d.ts.map +1 -0
  19. package/dist/groups/IssueLink.js +70 -0
  20. package/dist/groups/Link.d.ts +23 -0
  21. package/dist/groups/Link.d.ts.map +1 -0
  22. package/dist/groups/Link.js +34 -0
  23. package/dist/groups/Priority.d.ts +77 -0
  24. package/dist/groups/Priority.d.ts.map +1 -0
  25. package/dist/groups/Priority.js +97 -0
  26. package/dist/groups/Project.d.ts +36 -0
  27. package/dist/groups/Project.d.ts.map +1 -0
  28. package/dist/groups/Project.js +42 -0
  29. package/dist/groups/Status.d.ts +82 -0
  30. package/dist/groups/Status.d.ts.map +1 -0
  31. package/dist/groups/Status.js +102 -0
  32. package/dist/groups/TestCase.d.ts +254 -0
  33. package/dist/groups/TestCase.d.ts.map +1 -0
  34. package/dist/groups/TestCase.js +327 -0
  35. package/dist/groups/TestCycle.d.ts +127 -0
  36. package/dist/groups/TestCycle.d.ts.map +1 -0
  37. package/dist/groups/TestCycle.js +166 -0
  38. package/dist/groups/TestExecution.d.ts +176 -0
  39. package/dist/groups/TestExecution.d.ts.map +1 -0
  40. package/dist/groups/TestExecution.js +239 -0
  41. package/dist/groups/TestPlan.d.ts +103 -0
  42. package/dist/groups/TestPlan.d.ts.map +1 -0
  43. package/dist/groups/TestPlan.js +137 -0
  44. package/dist/index.d.ts +119 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +124 -0
  47. package/dist/types.d.ts +1353 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +7 -0
  50. package/dist/utils-api-call.d.ts +22 -0
  51. package/dist/utils-api-call.d.ts.map +1 -0
  52. package/dist/utils-api-call.js +80 -0
  53. package/dist/utils.d.ts +144 -0
  54. package/dist/utils.d.ts.map +1 -0
  55. package/dist/utils.js +432 -0
  56. package/package.json +54 -0
package/dist/utils.js ADDED
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Utility functions for Zephyr API operations
3
+ */
4
+ // Define error classes that match Commons Core error types
5
+ // These work in both ScriptRunner Connect runtime (where Commons Core is available)
6
+ // and local Node.js environments (where we provide fallback implementations)
7
+ class BaseHttpError extends Error {
8
+ constructor(message, status, statusText, response) {
9
+ super(message);
10
+ this.status = status;
11
+ this.statusText = statusText;
12
+ this.response = response;
13
+ this.name = this.constructor.name;
14
+ Object.setPrototypeOf(this, BaseHttpError.prototype);
15
+ }
16
+ }
17
+ class ZephyrBadRequestError extends BaseHttpError {
18
+ constructor(message, statusText, response) {
19
+ super(message, 400, statusText || 'Bad Request', response);
20
+ Object.setPrototypeOf(this, ZephyrBadRequestError.prototype);
21
+ }
22
+ }
23
+ class ZephyrUnauthorizedError extends BaseHttpError {
24
+ constructor(message, statusText, response) {
25
+ super(message, 401, statusText || 'Unauthorized', response);
26
+ Object.setPrototypeOf(this, ZephyrUnauthorizedError.prototype);
27
+ }
28
+ }
29
+ class ZephyrForbiddenError extends BaseHttpError {
30
+ constructor(message, statusText, response) {
31
+ super(message, 403, statusText || 'Forbidden', response);
32
+ Object.setPrototypeOf(this, ZephyrForbiddenError.prototype);
33
+ }
34
+ }
35
+ class ZephyrNotFoundError extends BaseHttpError {
36
+ constructor(message, statusText, response) {
37
+ super(message, 404, statusText || 'Not Found', response);
38
+ Object.setPrototypeOf(this, ZephyrNotFoundError.prototype);
39
+ }
40
+ }
41
+ class ZephyrTooManyRequestsError extends BaseHttpError {
42
+ constructor(message, statusText, response) {
43
+ super(message, 429, statusText || 'Too Many Requests', response);
44
+ Object.setPrototypeOf(this, ZephyrTooManyRequestsError.prototype);
45
+ }
46
+ }
47
+ class ZephyrServerError extends BaseHttpError {
48
+ constructor(message, status, statusText, response) {
49
+ super(message, status, statusText || 'Server Error', response);
50
+ Object.setPrototypeOf(this, ZephyrServerError.prototype);
51
+ }
52
+ }
53
+ class ZephyrUnexpectedError extends Error {
54
+ constructor(message, cause) {
55
+ super(message);
56
+ this.cause = cause;
57
+ this.name = 'UnexpectedError';
58
+ Object.setPrototypeOf(this, ZephyrUnexpectedError.prototype);
59
+ }
60
+ }
61
+ // Export error classes that are compatible with Commons Core
62
+ // In ScriptRunner Connect runtime, these will be compatible with Commons Core types
63
+ // In local Node.js, these are our implementations
64
+ // These can be used with instanceof checks and provide the same API as Commons Core errors
65
+ export const HttpError = BaseHttpError;
66
+ export const BadRequestError = ZephyrBadRequestError;
67
+ export const UnauthorizedError = ZephyrUnauthorizedError;
68
+ export const ForbiddenError = ZephyrForbiddenError;
69
+ export const NotFoundError = ZephyrNotFoundError;
70
+ export const TooManyRequestsError = ZephyrTooManyRequestsError;
71
+ export const ServerError = ZephyrServerError;
72
+ export const UnexpectedError = ZephyrUnexpectedError;
73
+ /**
74
+ * Default retry configuration for rate limiting
75
+ */
76
+ const DEFAULT_RETRY_CONFIG = {
77
+ maxRetries: 5,
78
+ initialDelay: 1000, // 1 second
79
+ maxDelay: 60000, // 60 seconds
80
+ backoffMultiplier: 2,
81
+ };
82
+ /**
83
+ * Calculate delay for exponential backoff
84
+ */
85
+ function calculateDelay(retryCount, config) {
86
+ const delay = config.initialDelay * Math.pow(config.backoffMultiplier, retryCount);
87
+ return Math.min(delay, config.maxDelay);
88
+ }
89
+ /**
90
+ * Sleep utility for retry delays
91
+ */
92
+ function sleep(ms) {
93
+ return new Promise((resolve) => setTimeout(resolve, ms));
94
+ }
95
+ /**
96
+ * Extract retry-after value from response headers
97
+ * Returns delay in milliseconds, or null if not present
98
+ */
99
+ function getRetryAfter(response) {
100
+ const retryAfter = response.headers.get('Retry-After');
101
+ if (!retryAfter) {
102
+ return null;
103
+ }
104
+ // Retry-After can be either seconds (number) or HTTP date
105
+ const seconds = parseInt(retryAfter, 10);
106
+ if (!isNaN(seconds)) {
107
+ return seconds * 1000; // Convert to milliseconds
108
+ }
109
+ // Try parsing as HTTP date
110
+ const date = new Date(retryAfter);
111
+ if (!isNaN(date.getTime())) {
112
+ const delay = date.getTime() - Date.now();
113
+ return delay > 0 ? delay : null;
114
+ }
115
+ return null;
116
+ }
117
+ /**
118
+ * Check if an error response indicates "Unknown object" (resource doesn't exist)
119
+ * This is used for idempotent delete operations
120
+ */
121
+ export async function isUnknownObjectError(response) {
122
+ if (response.ok) {
123
+ return false;
124
+ }
125
+ // Check status code first (404 is always "not found")
126
+ if (response.status === 404) {
127
+ return true;
128
+ }
129
+ // Check error message for "Unknown object" pattern
130
+ try {
131
+ const errorData = (await response.json());
132
+ if (errorData.message) {
133
+ const errorMessage = errorData.message.toLowerCase();
134
+ // APIs return errors like "resource: Unknown object: 123456" or "resource: Not a recognized ID"
135
+ if (errorMessage.includes('unknown object') ||
136
+ errorMessage.includes('not a recognized id') ||
137
+ errorMessage.includes('does not exist') ||
138
+ errorMessage.includes('not found')) {
139
+ return true;
140
+ }
141
+ }
142
+ }
143
+ catch {
144
+ // If JSON parsing fails, check status code only
145
+ return response.status === 404;
146
+ }
147
+ return false;
148
+ }
149
+ /**
150
+ * Build query string from options
151
+ * Zephyr API uses different parameter names than Asana (projectKey, folderId, maxResults, startAt, etc.)
152
+ */
153
+ export function buildQueryString(options) {
154
+ if (!options) {
155
+ return '';
156
+ }
157
+ const params = [];
158
+ // Pagination parameters (offset-based)
159
+ if (options.maxResults !== undefined) {
160
+ params.push(`maxResults=${options.maxResults}`);
161
+ }
162
+ if (options.startAt !== undefined) {
163
+ params.push(`startAt=${options.startAt}`);
164
+ }
165
+ // Pagination parameters (cursor-based)
166
+ if (options.limit !== undefined) {
167
+ params.push(`limit=${options.limit}`);
168
+ }
169
+ if (options.startAtId !== undefined) {
170
+ params.push(`startAtId=${options.startAtId}`);
171
+ }
172
+ // Add any other query parameters (filters, etc.)
173
+ for (const [key, value] of Object.entries(options)) {
174
+ if (value !== undefined &&
175
+ value !== null &&
176
+ key !== 'maxResults' &&
177
+ key !== 'startAt' &&
178
+ key !== 'limit' &&
179
+ key !== 'startAtId') {
180
+ // Convert camelCase to query parameter format if needed
181
+ // Zephyr API uses camelCase in query params (projectKey, folderId, etc.)
182
+ params.push(`${key}=${encodeURIComponent(String(value))}`);
183
+ }
184
+ }
185
+ return params.length > 0 ? `?${params.join('&')}` : '';
186
+ }
187
+ /**
188
+ * Parse API response with error handling
189
+ * Zephyr API returns objects directly (not wrapped in { data: ... })
190
+ * Throws appropriate Commons Core error types
191
+ */
192
+ export async function parseResponse(response) {
193
+ if (!response.ok) {
194
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
195
+ let errorData = null;
196
+ try {
197
+ errorData = await response.json();
198
+ const error = errorData;
199
+ if (error.message) {
200
+ errorMessage = error.message;
201
+ }
202
+ }
203
+ catch {
204
+ // If JSON parsing fails, use the status text
205
+ }
206
+ // Throw appropriate error type based on status code
207
+ switch (response.status) {
208
+ case 400:
209
+ throw new BadRequestError(errorMessage, response.statusText, errorData);
210
+ case 401:
211
+ throw new UnauthorizedError(errorMessage, response.statusText, errorData);
212
+ case 403:
213
+ throw new ForbiddenError(errorMessage, response.statusText, errorData);
214
+ case 404:
215
+ throw new NotFoundError(errorMessage, response.statusText, errorData);
216
+ case 429:
217
+ throw new TooManyRequestsError(errorMessage, response.statusText, errorData);
218
+ default:
219
+ if (response.status >= 500) {
220
+ throw new ServerError(errorMessage, response.status, response.statusText, errorData);
221
+ }
222
+ throw new HttpError(errorMessage, response.status, response.statusText, errorData);
223
+ }
224
+ }
225
+ // Handle empty responses (204 No Content, etc.)
226
+ const contentType = response.headers.get('content-type') || '';
227
+ const contentLength = response.headers.get('content-length');
228
+ // If no content or content-length is 0, return empty object or null based on type
229
+ if (contentLength === '0' || (!contentType.includes('application/json') && !contentType.includes('text/json'))) {
230
+ // For void return types, return undefined cast as T
231
+ // For Link types, try to construct from headers or return minimal object
232
+ return {};
233
+ }
234
+ try {
235
+ const data = (await response.json());
236
+ return data;
237
+ }
238
+ catch (error) {
239
+ // If JSON parsing fails but response is OK, return empty object
240
+ if (response.ok) {
241
+ return {};
242
+ }
243
+ throw error;
244
+ }
245
+ }
246
+ /**
247
+ * Execute an API call with automatic retry on rate limiting (429)
248
+ * Uses exponential backoff with configurable retry settings
249
+ * Respects Retry-After header when present
250
+ * Supports optional error strategy for custom error handling
251
+ */
252
+ export async function executeWithRetry(apiCall, config = {}, errorStrategy) {
253
+ const retryConfig = {
254
+ ...DEFAULT_RETRY_CONFIG,
255
+ ...config,
256
+ };
257
+ let lastError;
258
+ let retryCount = 0;
259
+ while (retryCount <= retryConfig.maxRetries) {
260
+ try {
261
+ const response = await apiCall();
262
+ // If successful, parse and return
263
+ if (response.ok) {
264
+ return (await response.json());
265
+ }
266
+ // If rate limited (429), retry with backoff
267
+ if (response.status === 429) {
268
+ // Check for Retry-After header (takes precedence)
269
+ const retryAfter = getRetryAfter(response);
270
+ const delay = retryAfter ?? calculateDelay(retryCount, retryConfig);
271
+ if (retryCount < retryConfig.maxRetries) {
272
+ retryCount++;
273
+ await sleep(delay);
274
+ continue; // Retry the request
275
+ }
276
+ // Max retries exceeded, parse error and throw
277
+ let errorMessage = `Rate limit exceeded after ${retryCount} retries`;
278
+ let errorData = null;
279
+ try {
280
+ errorData = await response.json();
281
+ const error = errorData;
282
+ if (error.message) {
283
+ errorMessage = error.message;
284
+ }
285
+ }
286
+ catch {
287
+ // If JSON parsing fails, use default message
288
+ }
289
+ throw new TooManyRequestsError(errorMessage, response.statusText, errorData);
290
+ }
291
+ // For other errors, check error strategy first
292
+ if (errorStrategy) {
293
+ const { applyErrorStrategy } = await import('./error-strategy');
294
+ const action = await applyErrorStrategy(response, errorStrategy, retryCount);
295
+ if (action.type === 'retry') {
296
+ retryCount++;
297
+ await sleep(action.delay);
298
+ continue; // Retry the request
299
+ }
300
+ else if (action.type === 'return') {
301
+ return action.value;
302
+ }
303
+ // Otherwise, continue propagation (fall through to parseResponse)
304
+ }
305
+ // For other errors, parse and throw immediately
306
+ await parseResponse(response);
307
+ }
308
+ catch (error) {
309
+ lastError = error;
310
+ // If it's a TooManyRequestsError and we haven't exceeded max retries, retry
311
+ if (error instanceof TooManyRequestsError && retryCount < retryConfig.maxRetries) {
312
+ // Check error strategy for 429 handling
313
+ if (errorStrategy?.handleHttp429Error) {
314
+ const { applyErrorStrategy } = await import('./error-strategy');
315
+ // Create a mock response for error strategy
316
+ const mockResponse = {
317
+ ok: false,
318
+ status: 429,
319
+ statusText: 'Too Many Requests',
320
+ json: async () => error.response || null,
321
+ headers: { get: () => null },
322
+ };
323
+ const action = await applyErrorStrategy(mockResponse, errorStrategy, retryCount);
324
+ if (action.type === 'retry') {
325
+ retryCount++;
326
+ await sleep(action.delay);
327
+ continue; // Retry the request
328
+ }
329
+ else if (action.type === 'return') {
330
+ return action.value;
331
+ }
332
+ }
333
+ // Default retry behavior
334
+ const delay = calculateDelay(retryCount, retryConfig);
335
+ retryCount++;
336
+ await sleep(delay);
337
+ continue; // Retry the request
338
+ }
339
+ // For other errors or max retries exceeded, throw immediately
340
+ throw error;
341
+ }
342
+ }
343
+ // This should never be reached, but TypeScript needs it
344
+ throw lastError || new UnexpectedError('Unexpected error in retry logic');
345
+ }
346
+ /**
347
+ * Build request body for Zephyr API
348
+ * Zephyr API expects JSON objects directly (not wrapped in { data: ... })
349
+ */
350
+ export function buildRequestBody(data) {
351
+ return JSON.stringify(data);
352
+ }
353
+ /**
354
+ * Get all pages from a paginated endpoint (offset-based)
355
+ * Automatically fetches all pages until no more results are available
356
+ *
357
+ * @param apiCall - Function that makes the API call with startAt and maxResults
358
+ * @param options - Optional pagination options
359
+ * @returns Array of all items from all pages
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * import { getAllPages } from './zephyr/utils';
364
+ *
365
+ * const allTestCases = await getAllPages(
366
+ * (startAt, maxResults) => Zephyr.TestCase.listTestCases({
367
+ * projectKey: 'PROJ',
368
+ * startAt,
369
+ * maxResults
370
+ * }),
371
+ * { maxResults: 50 }
372
+ * );
373
+ * ```
374
+ */
375
+ export async function getAllPages(apiCall, options) {
376
+ const maxResults = options?.maxResults ?? 50;
377
+ let startAt = options?.startAt ?? 0;
378
+ const allItems = [];
379
+ while (true) {
380
+ const page = await apiCall(startAt, maxResults);
381
+ if (page.values) {
382
+ allItems.push(...page.values);
383
+ }
384
+ // Check if this is the last page
385
+ if (page.isLast || !page.values || page.values.length === 0) {
386
+ break;
387
+ }
388
+ // Move to next page
389
+ startAt = page.startAt + page.maxResults;
390
+ }
391
+ return allItems;
392
+ }
393
+ /**
394
+ * Get all pages from a cursor-paginated endpoint
395
+ * Automatically fetches all pages until no more results are available
396
+ *
397
+ * @param apiCall - Function that makes the API call with startAtId and limit
398
+ * @param options - Optional pagination options
399
+ * @returns Array of all items from all pages
400
+ *
401
+ * @example
402
+ * ```typescript
403
+ * import { getAllPagesCursor } from './zephyr/utils';
404
+ *
405
+ * const allTestCases = await getAllPagesCursor(
406
+ * (startAtId, limit) => Zephyr.TestCase.listTestCasesCursorPaginated({
407
+ * projectKey: 'PROJ',
408
+ * startAtId,
409
+ * limit
410
+ * }),
411
+ * { limit: 100 }
412
+ * );
413
+ * ```
414
+ */
415
+ export async function getAllPagesCursor(apiCall, options) {
416
+ const limit = options?.limit ?? 100;
417
+ let startAtId = options?.startAtId ?? null;
418
+ const allItems = [];
419
+ while (true) {
420
+ const page = await apiCall(startAtId, limit);
421
+ if (page.values) {
422
+ allItems.push(...page.values);
423
+ }
424
+ // Check if there's a next page
425
+ if (page.nextStartAtId === null || !page.values || page.values.length === 0) {
426
+ break;
427
+ }
428
+ // Move to next page
429
+ startAtId = page.nextStartAtId;
430
+ }
431
+ return allItems;
432
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@rbaileysr/zephyr-managed-api",
3
+ "version": "1.0.0",
4
+ "description": "Managed API wrapper for Zephyr Cloud REST API v2 - Comprehensive type-safe access to all Zephyr API endpoints for ScriptRunner Connect",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "zephyr",
20
+ "zephyr-scale",
21
+ "zephyr-cloud",
22
+ "test-management",
23
+ "api",
24
+ "managed-api",
25
+ "scriptrunner-connect",
26
+ "typescript",
27
+ "rest-api"
28
+ ],
29
+ "author": "rbailey@adaptavist.com",
30
+ "license": "MIT",
31
+ "dependencies": {},
32
+ "peerDependencies": {
33
+ "@managed-api/commons-core": "*"
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "^5.8.3"
37
+ },
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "files": [
42
+ "dist/",
43
+ "README.md"
44
+ ],
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/rbaileysr/zephyr-managed-api.git"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/rbaileysr/zephyr-managed-api/issues"
51
+ },
52
+ "homepage": "https://github.com/rbaileysr/zephyr-managed-api#readme"
53
+ }
54
+