@forge/events
Advanced tools
Comparing version 0.0.0-experimental-906222c to 0.0.0-experimental-d18f8dd
# @forge/events | ||
## 0.0.0-experimental-906222c | ||
## 0.0.0-experimental-d18f8dd | ||
### Patch Changes | ||
- d18f8dd: Force bump | ||
- Updated dependencies [d18f8dd] | ||
- @forge/api@0.0.0-experimental-d18f8dd | ||
## 0.3.0 | ||
### Minor Changes | ||
- 9fe841c: Explicitly add uuid dependency and whitelist WHP getStats endpoint | ||
- 66b6648: Add errors and validations | ||
- 9051f1d: Add @forge/api as a dependency of @forge/events | ||
- 6dbedd8: Add QueueResponse in forge-events package to allow retrying an async event. | ||
### Patch Changes | ||
- afa9ee0: Update the error messages | ||
## 0.3.0-next.4 | ||
### Patch Changes | ||
- afa9ee0: Update the error messages | ||
## 0.3.0-next.3 | ||
### Minor Changes | ||
- 9051f1d: Add @forge/api as a dependency of @forge/events | ||
## 0.3.0-next.2 | ||
### Minor Changes | ||
- 6dbedd8: Add QueueResponse in forge-events package to allow retrying an async event. | ||
## 0.3.0-next.1 | ||
### Minor Changes | ||
- 66b6648: Add errors and validations | ||
## 0.3.0-next.0 | ||
### Minor Changes | ||
- 767d40d: Explicitly add uuid dependency and whitelist WHP getStats endpoint | ||
## 0.2.0 | ||
### Minor Changes | ||
- a056b40: Add Queue.getStats() | ||
- 8dca98b: New configurable settings for Queue push API | ||
### Patch Changes | ||
- ddce3eb: Update the error message for delayInSettings error in push settings | ||
## 0.2.0-next.2 | ||
### Patch Changes | ||
- ddce3eb: Update the error message for delayInSettings error in push settings | ||
## 0.2.0-next.1 | ||
### Minor Changes | ||
- a056b40: Add Queue.getStats() | ||
## 0.2.0-next.0 | ||
### Minor Changes | ||
- 8dca98b: New configurable settings for Queue push API | ||
## 0.1.0 | ||
### Minor Changes | ||
- bfc9f8c: update stargate allowlist and new middleware for updating runtime variables in path for requestAtlassian | ||
- 0831a0a: export queue errors and rename queue param "queueName" to "key" | ||
@@ -13,2 +94,8 @@ ### Patch Changes | ||
## 0.1.0-next.2 | ||
### Minor Changes | ||
- 0831a0a: export queue errors and rename queue param "queueName" to "key" | ||
## 0.1.0-next.1 | ||
@@ -15,0 +102,0 @@ |
import { FailedEvent } from './types'; | ||
export declare class InvalidPushSettingsError extends Error { | ||
constructor(message: string); | ||
} | ||
export declare class InvalidQueueNameError extends Error { | ||
@@ -26,2 +29,8 @@ constructor(message: string); | ||
} | ||
export declare class JobDoesNotExistError extends Error { | ||
constructor(message: string); | ||
} | ||
export declare class InvocationLimitReachedError extends Error { | ||
constructor(); | ||
} | ||
//# sourceMappingURL=errors.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.InternalServerError = exports.PartialSuccessError = exports.RateLimitError = exports.NoEventsToPushError = exports.PayloadTooBigError = exports.TooManyEventsError = exports.InvalidQueueNameError = void 0; | ||
exports.InvocationLimitReachedError = exports.JobDoesNotExistError = exports.InternalServerError = exports.PartialSuccessError = exports.RateLimitError = exports.NoEventsToPushError = exports.PayloadTooBigError = exports.TooManyEventsError = exports.InvalidQueueNameError = exports.InvalidPushSettingsError = void 0; | ||
class InvalidPushSettingsError extends Error { | ||
constructor(message) { | ||
super(message); | ||
} | ||
} | ||
exports.InvalidPushSettingsError = InvalidPushSettingsError; | ||
class InvalidQueueNameError extends Error { | ||
@@ -49,1 +55,13 @@ constructor(message) { | ||
exports.InternalServerError = InternalServerError; | ||
class JobDoesNotExistError extends Error { | ||
constructor(message) { | ||
super(message); | ||
} | ||
} | ||
exports.JobDoesNotExistError = JobDoesNotExistError; | ||
class InvocationLimitReachedError extends Error { | ||
constructor() { | ||
super(); | ||
} | ||
} | ||
exports.InvocationLimitReachedError = InvocationLimitReachedError; |
export { Queue } from './queue'; | ||
export { InvalidQueueNameError, TooManyEventsError, PayloadTooBigError, NoEventsToPushError, RateLimitError, PartialSuccessError, InternalServerError, JobDoesNotExistError, InvalidPushSettingsError, InvocationLimitReachedError } from './errors'; | ||
export { JobProgress } from './jobProgress'; | ||
export { QueueResponse } from './queueResponse'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -5,1 +5,16 @@ "use strict"; | ||
Object.defineProperty(exports, "Queue", { enumerable: true, get: function () { return queue_1.Queue; } }); | ||
var errors_1 = require("./errors"); | ||
Object.defineProperty(exports, "InvalidQueueNameError", { enumerable: true, get: function () { return errors_1.InvalidQueueNameError; } }); | ||
Object.defineProperty(exports, "TooManyEventsError", { enumerable: true, get: function () { return errors_1.TooManyEventsError; } }); | ||
Object.defineProperty(exports, "PayloadTooBigError", { enumerable: true, get: function () { return errors_1.PayloadTooBigError; } }); | ||
Object.defineProperty(exports, "NoEventsToPushError", { enumerable: true, get: function () { return errors_1.NoEventsToPushError; } }); | ||
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_1.RateLimitError; } }); | ||
Object.defineProperty(exports, "PartialSuccessError", { enumerable: true, get: function () { return errors_1.PartialSuccessError; } }); | ||
Object.defineProperty(exports, "InternalServerError", { enumerable: true, get: function () { return errors_1.InternalServerError; } }); | ||
Object.defineProperty(exports, "JobDoesNotExistError", { enumerable: true, get: function () { return errors_1.JobDoesNotExistError; } }); | ||
Object.defineProperty(exports, "InvalidPushSettingsError", { enumerable: true, get: function () { return errors_1.InvalidPushSettingsError; } }); | ||
Object.defineProperty(exports, "InvocationLimitReachedError", { enumerable: true, get: function () { return errors_1.InvocationLimitReachedError; } }); | ||
var jobProgress_1 = require("./jobProgress"); | ||
Object.defineProperty(exports, "JobProgress", { enumerable: true, get: function () { return jobProgress_1.JobProgress; } }); | ||
var queueResponse_1 = require("./queueResponse"); | ||
Object.defineProperty(exports, "QueueResponse", { enumerable: true, get: function () { return queueResponse_1.QueueResponse; } }); |
@@ -1,4 +0,5 @@ | ||
import { PushBodyParams, APIRequest } from './types'; | ||
export declare const getPushBody: (params: PushBodyParams) => APIRequest; | ||
import { APIRequest, APIResponse, FetchMethod } from './types'; | ||
export declare const PUSH_PATH = "/webhook/queue/publish/{cloudId}/{environmentId}/{appId}/{appVersion}"; | ||
export declare const GET_STATS_PATH = "/webhook/queue/stats/{cloudId}/{environmentId}/{appId}/{appVersion}"; | ||
export declare const post: (endpoint: string, body: APIRequest, apiClient: FetchMethod) => Promise<APIResponse>; | ||
//# sourceMappingURL=queries.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.PUSH_PATH = exports.getPushBody = void 0; | ||
exports.getPushBody = (params) => ({ | ||
queueName: params.queueName, | ||
type: params.type, | ||
schema: params.schema, | ||
payload: params.payload, | ||
time: new Date().toISOString() | ||
}); | ||
exports.post = exports.GET_STATS_PATH = exports.PUSH_PATH = void 0; | ||
exports.PUSH_PATH = '/webhook/queue/publish/{cloudId}/{environmentId}/{appId}/{appVersion}'; | ||
exports.GET_STATS_PATH = '/webhook/queue/stats/{cloudId}/{environmentId}/{appId}/{appVersion}'; | ||
exports.post = async (endpoint, body, apiClient) => { | ||
const request = { | ||
method: 'POST', | ||
body: JSON.stringify(body), | ||
headers: { | ||
'content-type': 'application/json' | ||
} | ||
}; | ||
return await apiClient(endpoint, request); | ||
}; |
@@ -1,2 +0,3 @@ | ||
import { APIResponse, FetchMethod, Payload, QueueParams } from './types'; | ||
import { FetchMethod, Payload, QueueParams, PushSettings } from './types'; | ||
import { JobProgress } from './jobProgress'; | ||
export declare class Queue { | ||
@@ -6,6 +7,5 @@ private readonly apiClient; | ||
constructor(queueParams: QueueParams, apiClient?: FetchMethod); | ||
push(payloads: Payload | Payload[]): Promise<APIResponse>; | ||
private query; | ||
private buildRequest; | ||
push(payloads: Payload | Payload[], pushSettings?: PushSettings): Promise<string>; | ||
getJob(jobId: string): JobProgress; | ||
} | ||
//# sourceMappingURL=queue.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Queue = void 0; | ||
const tslib_1 = require("tslib"); | ||
const queries_1 = require("./queries"); | ||
const validators_1 = require("./validators"); | ||
async function getResponseBody(response, body) { | ||
await validators_1.validateAPIResponse(response, body); | ||
return response; | ||
} | ||
const v4_1 = tslib_1.__importDefault(require("uuid/v4")); | ||
const jobProgress_1 = require("./jobProgress"); | ||
class Queue { | ||
constructor(queueParams, apiClient) { | ||
validators_1.validateQueueName(queueParams.queueName); | ||
validators_1.validateQueueKey(queueParams.key); | ||
this.queueParams = queueParams; | ||
this.apiClient = apiClient || global.api.asApp().__requestAtlassian; | ||
} | ||
async push(payloads) { | ||
validators_1.validatePayloads(payloads); | ||
const queryParams = { | ||
async push(payloads, pushSettings) { | ||
validators_1.validatePushPayloads(payloads); | ||
const queueName = this.queueParams.key; | ||
const jobId = v4_1.default(); | ||
const pushRequest = { | ||
queueName: queueName, | ||
jobId: jobId, | ||
type: 'avi:forge:app:event', | ||
schema: 'ari:cloud:ecosystem::forge/app-event', | ||
payload: Array.isArray(payloads) ? payloads : [payloads], | ||
queueName: this.queueParams.queueName, | ||
schema: 'ari:cloud:ecosystem::forge/app-event', | ||
type: 'avi:forge:app:event' | ||
time: new Date().toISOString() | ||
}; | ||
const requestBody = queries_1.getPushBody(queryParams); | ||
return this.query(queries_1.PUSH_PATH, requestBody); | ||
if (pushSettings) { | ||
validators_1.validatePushSettings(pushSettings); | ||
if (pushSettings.delayInSeconds) { | ||
pushRequest.delayInSeconds = pushSettings.delayInSeconds; | ||
} | ||
} | ||
const response = await queries_1.post(queries_1.PUSH_PATH, pushRequest, this.apiClient); | ||
await validators_1.validatePushAPIResponse(response, pushRequest); | ||
return `${queueName}#${jobId}`; | ||
} | ||
async query(endpoint, body) { | ||
const response = await this.apiClient(endpoint, this.buildRequest(body)); | ||
return await getResponseBody(response, body); | ||
getJob(jobId) { | ||
return new jobProgress_1.JobProgress(jobId, this.apiClient); | ||
} | ||
buildRequest(requestBody) { | ||
return { | ||
method: 'POST', | ||
body: JSON.stringify(requestBody), | ||
headers: { | ||
'content-type': 'application/json' | ||
} | ||
}; | ||
} | ||
} | ||
exports.Queue = Queue; |
@@ -7,12 +7,17 @@ import { RequestInit, Response } from 'node-fetch'; | ||
}; | ||
export interface PushSettings { | ||
delayInSeconds: number; | ||
} | ||
export interface QueueParams { | ||
queueName: string; | ||
key: string; | ||
} | ||
export interface PushBodyParams { | ||
export interface PushRequest extends APIRequest { | ||
payload: Payload[]; | ||
queueName: string; | ||
schema: string; | ||
type: string; | ||
delayInSeconds?: number; | ||
} | ||
export interface APIRequest extends PushBodyParams { | ||
export interface APIRequest { | ||
queueName: string; | ||
jobId: string; | ||
time: string; | ||
@@ -24,2 +29,3 @@ } | ||
} | ||
export declare type GetStatsRequest = APIRequest; | ||
//# sourceMappingURL=types.d.ts.map |
@@ -1,5 +0,9 @@ | ||
import { APIResponse, Payload, APIRequest } from './types'; | ||
export declare const validateQueueName: (queueName: string) => void; | ||
export declare const validatePayloads: (payloads: Payload | Payload[]) => void; | ||
export declare const validateAPIResponse: (response: APIResponse, requestBody: APIRequest) => Promise<void>; | ||
import { APIResponse, GetStatsRequest, Payload, PushRequest, PushSettings } from './types'; | ||
export declare const validateQueueKey: (queueName: string) => void; | ||
export declare const validatePushSettings: (settings: PushSettings) => void; | ||
export declare const validatePushPayloads: (payloads: Payload | Payload[]) => void; | ||
export declare const validateGetStatsPayload: (getStatsRequest: GetStatsRequest) => void; | ||
export declare const validateAPIResponse: (response: APIResponse, expectedSuccessStatus: number) => Promise<void>; | ||
export declare const validatePushAPIResponse: (response: APIResponse, requestBody: PushRequest) => Promise<void>; | ||
export declare const validateGetStatsAPIResponse: (response: APIResponse, getStatsRequest: GetStatsRequest) => Promise<void>; | ||
//# sourceMappingURL=validators.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.validateAPIResponse = exports.validatePayloads = exports.validateQueueName = void 0; | ||
exports.validateGetStatsAPIResponse = exports.validatePushAPIResponse = exports.validateAPIResponse = exports.validateGetStatsPayload = exports.validatePushPayloads = exports.validatePushSettings = exports.validateQueueKey = void 0; | ||
const errors_1 = require("./errors"); | ||
const text_1 = require("./text"); | ||
const VALID_QUEUE_NAME_PATTERN = /^[a-zA-Z0-9-_]+$/; | ||
const MAXIMUM_EVENTS = 10; | ||
const MAXIMUM_PAYLOAD_SIZE_KB = 200; | ||
exports.validateQueueName = (queueName) => { | ||
exports.validateQueueKey = (queueName) => { | ||
if (!queueName || !VALID_QUEUE_NAME_PATTERN.test(queueName)) { | ||
throw new errors_1.InvalidQueueNameError('Queue name can only contain alphanumeric, dash and underscore characters'); | ||
throw new errors_1.InvalidQueueNameError(text_1.Text.error.invalidQueueName); | ||
} | ||
}; | ||
exports.validatePayloads = (payloads) => { | ||
exports.validatePushSettings = (settings) => { | ||
if ((settings.delayInSeconds && settings.delayInSeconds > 900) || settings.delayInSeconds < 0) { | ||
throw new errors_1.InvalidPushSettingsError(text_1.Text.error.invalidDelayInSecondsSetting); | ||
} | ||
}; | ||
exports.validatePushPayloads = (payloads) => { | ||
if (!payloads || (Array.isArray(payloads) && payloads.length === 0)) { | ||
throw new errors_1.NoEventsToPushError(`No events pushed`); | ||
throw new errors_1.NoEventsToPushError(text_1.Text.error.noEventsPushed); | ||
} | ||
if (Array.isArray(payloads) && payloads.length > MAXIMUM_EVENTS) { | ||
throw new errors_1.TooManyEventsError(`Maximum of ${MAXIMUM_EVENTS} events allowed in a single push`); | ||
throw new errors_1.TooManyEventsError(text_1.Text.error.maxEventsAllowed(MAXIMUM_EVENTS)); | ||
} | ||
const payloadSizeKB = Buffer.byteLength(JSON.stringify(payloads)) / 1024; | ||
if (payloadSizeKB > MAXIMUM_PAYLOAD_SIZE_KB) { | ||
throw new errors_1.PayloadTooBigError(`The maximum payload size allowed is ${MAXIMUM_PAYLOAD_SIZE_KB}KB`); | ||
throw new errors_1.PayloadTooBigError(text_1.Text.error.maxPayloadAllowed(MAXIMUM_PAYLOAD_SIZE_KB)); | ||
} | ||
}; | ||
exports.validateAPIResponse = async (response, requestBody) => { | ||
exports.validateGetStatsPayload = (getStatsRequest) => { | ||
if (!getStatsRequest.jobId) { | ||
throw new errors_1.JobDoesNotExistError(text_1.Text.error.jobIdEmpty); | ||
} | ||
exports.validateQueueKey(getStatsRequest.queueName); | ||
}; | ||
exports.validateAPIResponse = async (response, expectedSuccessStatus) => { | ||
if (response.status === 429) { | ||
throw new errors_1.RateLimitError(`Too many requests`); | ||
throw new errors_1.RateLimitError(text_1.Text.error.rateLimitError); | ||
} | ||
if (response.status === 405) { | ||
throw new errors_1.InvocationLimitReachedError(); | ||
} | ||
if (response.status != expectedSuccessStatus && response.status) { | ||
let internalServerError; | ||
try { | ||
const responseBody = await response.json(); | ||
const errorMessage = responseBody.message ? `: ${responseBody.message}` : ''; | ||
const errors = responseBody.errors ? `: ${responseBody.errors.join(', ')}` : ''; | ||
internalServerError = new errors_1.InternalServerError(`${response.status} ${response.statusText}${errorMessage}${errors}`, responseBody.code, responseBody.details); | ||
} | ||
catch (ignore) { | ||
internalServerError = new errors_1.InternalServerError(`${response.status} ${response.statusText}`, response.status); | ||
} | ||
throw internalServerError; | ||
} | ||
}; | ||
exports.validatePushAPIResponse = async (response, requestBody) => { | ||
if (response.status === 413) { | ||
@@ -35,4 +65,7 @@ const responseBody = await response.json(); | ||
const responseBody = await response.json(); | ||
const defaultErrorMessage = 'Failed to process some events.'; | ||
const partialSuccessError = new errors_1.PartialSuccessError(defaultErrorMessage, []); | ||
if (responseBody.failedEvents && responseBody.failedEvents.length > 0) { | ||
throw new errors_1.PartialSuccessError(`Failed to process ${responseBody.failedEvents.length} events`, responseBody.failedEvents.map((failedEvent) => { | ||
partialSuccessError.message = `Failed to process ${responseBody.failedEvents.length} event(s).`; | ||
partialSuccessError.failedEvents = responseBody.failedEvents.map((failedEvent) => { | ||
return { | ||
@@ -42,13 +75,19 @@ errorMessage: failedEvent.errorMessage, | ||
}; | ||
})); | ||
}); | ||
} | ||
else { | ||
throw new errors_1.PartialSuccessError(`Failed to process events`, []); | ||
if (responseBody.errorMessage) { | ||
partialSuccessError.message = | ||
partialSuccessError.message !== defaultErrorMessage | ||
? `${partialSuccessError.message} ${responseBody.errorMessage}` | ||
: responseBody.errorMessage; | ||
} | ||
throw partialSuccessError; | ||
} | ||
if (response.status != 201 && response.status) { | ||
const responseBody = await response.json(); | ||
const errorMessage = responseBody.message ? `: ${responseBody.message}` : ''; | ||
throw new errors_1.InternalServerError(`${response.status} ${response.statusText}${errorMessage}`, responseBody.code, responseBody.details); | ||
await exports.validateAPIResponse(response, 201); | ||
}; | ||
exports.validateGetStatsAPIResponse = async (response, getStatsRequest) => { | ||
if (response.status === 404) { | ||
throw new errors_1.JobDoesNotExistError(text_1.Text.error.jobDoesNotExit(getStatsRequest.jobId, getStatsRequest.queueName)); | ||
} | ||
await exports.validateAPIResponse(response, 200); | ||
}; |
{ | ||
"name": "@forge/events", | ||
"version": "0.0.0-experimental-906222c", | ||
"version": "0.0.0-experimental-d18f8dd", | ||
"description": "Forge Async Event methods", | ||
@@ -15,4 +15,9 @@ "author": "Atlassian", | ||
"devDependencies": { | ||
"@types/uuid": "^3.4.7", | ||
"@types/node": "^12.12.63" | ||
}, | ||
"dependencies": { | ||
"uuid": "^3.4.0", | ||
"@forge/api": "^0.0.0-experimental-d18f8dd" | ||
} | ||
} |
@@ -31,3 +31,3 @@ Library for asynchronous data processing. | ||
async function demo() { | ||
const queue = new Queue({queueName: "queue-name"}, apiClient); | ||
const queue = new Queue({key: "queue-name"}, apiClient); | ||
const payloads = { | ||
@@ -34,0 +34,0 @@ page: 1 |
import { FailedEvent } from './types'; | ||
export class InvalidPushSettingsError extends Error { | ||
constructor(message: string) { | ||
super(message); | ||
} | ||
} | ||
export class InvalidQueueNameError extends Error { | ||
@@ -52,1 +58,13 @@ constructor(message: string) { | ||
} | ||
export class JobDoesNotExistError extends Error { | ||
constructor(message: string) { | ||
super(message); | ||
} | ||
} | ||
export class InvocationLimitReachedError extends Error { | ||
constructor() { | ||
super(); | ||
} | ||
} |
export { Queue } from './queue'; | ||
export { | ||
InvalidQueueNameError, | ||
TooManyEventsError, | ||
PayloadTooBigError, | ||
NoEventsToPushError, | ||
RateLimitError, | ||
PartialSuccessError, | ||
InternalServerError, | ||
JobDoesNotExistError, | ||
InvalidPushSettingsError, | ||
InvocationLimitReachedError | ||
} from './errors'; | ||
export { JobProgress } from './jobProgress'; | ||
export { QueueResponse } from './queueResponse'; |
@@ -1,11 +0,16 @@ | ||
import { PushBodyParams, APIRequest } from './types'; | ||
import { APIRequest, APIResponse, FetchMethod } from './types'; | ||
export const getPushBody = (params: PushBodyParams): APIRequest => ({ | ||
queueName: params.queueName, | ||
type: params.type, | ||
schema: params.schema, | ||
payload: params.payload, | ||
time: new Date().toISOString() | ||
}); | ||
export const PUSH_PATH = '/webhook/queue/publish/{cloudId}/{environmentId}/{appId}/{appVersion}'; | ||
export const GET_STATS_PATH = '/webhook/queue/stats/{cloudId}/{environmentId}/{appId}/{appVersion}'; | ||
export const PUSH_PATH = '/webhook/queue/publish/{cloudId}/{environmentId}/{appId}/{appVersion}'; | ||
export const post = async (endpoint: string, body: APIRequest, apiClient: FetchMethod): Promise<APIResponse> => { | ||
const request = { | ||
method: 'POST', | ||
body: JSON.stringify(body), | ||
headers: { | ||
'content-type': 'application/json' | ||
} | ||
}; | ||
return await apiClient(endpoint, request); | ||
}; |
@@ -1,10 +0,7 @@ | ||
import { getPushBody, PUSH_PATH } from './queries'; | ||
import { validateAPIResponse, validatePayloads, validateQueueName } from './validators'; | ||
import { APIResponse, FetchMethod, Payload, PushBodyParams, QueueParams, APIRequest } from './types'; | ||
import { PUSH_PATH, post } from './queries'; | ||
import { validatePushAPIResponse, validatePushPayloads, validateQueueKey, validatePushSettings } from './validators'; | ||
import { FetchMethod, Payload, QueueParams, PushSettings, PushRequest } from './types'; | ||
import uuid from 'uuid/v4'; | ||
import { JobProgress } from './jobProgress'; | ||
async function getResponseBody(response: APIResponse, body: any): Promise<APIResponse> { | ||
await validateAPIResponse(response, body); | ||
return response; | ||
} | ||
export class Queue { | ||
@@ -15,3 +12,3 @@ private readonly apiClient: FetchMethod; | ||
constructor(queueParams: QueueParams, apiClient?: FetchMethod) { | ||
validateQueueName(queueParams.queueName); | ||
validateQueueKey(queueParams.key); | ||
this.queueParams = queueParams; | ||
@@ -21,29 +18,31 @@ this.apiClient = apiClient || (global as any).api.asApp().__requestAtlassian; | ||
async push(payloads: Payload | Payload[]): Promise<APIResponse> { | ||
validatePayloads(payloads); | ||
async push(payloads: Payload | Payload[], pushSettings?: PushSettings): Promise<string> { | ||
validatePushPayloads(payloads); | ||
const queueName = this.queueParams.key; | ||
const jobId = uuid(); | ||
const queryParams: PushBodyParams = { | ||
const pushRequest: PushRequest = { | ||
queueName: queueName, | ||
jobId: jobId, | ||
type: 'avi:forge:app:event', | ||
schema: 'ari:cloud:ecosystem::forge/app-event', | ||
payload: Array.isArray(payloads) ? payloads : [payloads], | ||
queueName: this.queueParams.queueName, | ||
schema: 'ari:cloud:ecosystem::forge/app-event', | ||
type: 'avi:forge:app:event' | ||
time: new Date().toISOString() | ||
}; | ||
const requestBody = getPushBody(queryParams); | ||
return this.query(PUSH_PATH, requestBody); | ||
} | ||
private async query(endpoint: string, body: APIRequest): Promise<APIResponse> { | ||
const response = await this.apiClient(endpoint, this.buildRequest(body)); | ||
return await getResponseBody(response, body); | ||
if (pushSettings) { | ||
validatePushSettings(pushSettings); | ||
if (pushSettings.delayInSeconds) { | ||
pushRequest.delayInSeconds = pushSettings.delayInSeconds; | ||
} | ||
} | ||
const response = await post(PUSH_PATH, pushRequest, this.apiClient); | ||
await validatePushAPIResponse(response, pushRequest); | ||
return `${queueName}#${jobId}`; | ||
} | ||
private buildRequest(requestBody: APIRequest) { | ||
return { | ||
method: 'POST', | ||
body: JSON.stringify(requestBody), | ||
headers: { | ||
'content-type': 'application/json' | ||
} | ||
}; | ||
getJob(jobId: string): JobProgress { | ||
return new JobProgress(jobId, this.apiClient); | ||
} | ||
} |
@@ -7,14 +7,19 @@ import { RequestInit, Response } from 'node-fetch'; | ||
export interface PushSettings { | ||
delayInSeconds: number; | ||
} | ||
export interface QueueParams { | ||
queueName: string; | ||
key: string; | ||
} | ||
export interface PushBodyParams { | ||
export interface PushRequest extends APIRequest { | ||
payload: Payload[]; | ||
queueName: string; | ||
schema: string; | ||
type: string; | ||
delayInSeconds?: number; | ||
} | ||
export interface APIRequest extends PushBodyParams { | ||
export interface APIRequest { | ||
queueName: string; | ||
jobId: string; | ||
time: string; | ||
@@ -27,1 +32,3 @@ } | ||
} | ||
export type GetStatsRequest = APIRequest; |
import { | ||
InternalServerError, | ||
InvalidQueueNameError, | ||
JobDoesNotExistError, | ||
NoEventsToPushError, | ||
@@ -8,5 +9,8 @@ PartialSuccessError, | ||
RateLimitError, | ||
TooManyEventsError | ||
TooManyEventsError, | ||
InvalidPushSettingsError, | ||
InvocationLimitReachedError | ||
} from './errors'; | ||
import { APIResponse, Payload, APIRequest } from './types'; | ||
import { Text } from './text'; | ||
import { APIResponse, GetStatsRequest, Payload, PushRequest, PushSettings } from './types'; | ||
@@ -17,15 +21,21 @@ const VALID_QUEUE_NAME_PATTERN = /^[a-zA-Z0-9-_]+$/; | ||
export const validateQueueName = (queueName: string) => { | ||
export const validateQueueKey = (queueName: string) => { | ||
if (!queueName || !VALID_QUEUE_NAME_PATTERN.test(queueName)) { | ||
throw new InvalidQueueNameError('Queue name can only contain alphanumeric, dash and underscore characters'); | ||
throw new InvalidQueueNameError(Text.error.invalidQueueName); | ||
} | ||
}; | ||
export const validatePayloads = (payloads: Payload | Payload[]) => { | ||
export const validatePushSettings = (settings: PushSettings) => { | ||
if ((settings.delayInSeconds && settings.delayInSeconds > 900) || settings.delayInSeconds < 0) { | ||
throw new InvalidPushSettingsError(Text.error.invalidDelayInSecondsSetting); | ||
} | ||
}; | ||
export const validatePushPayloads = (payloads: Payload | Payload[]) => { | ||
if (!payloads || (Array.isArray(payloads) && payloads.length === 0)) { | ||
throw new NoEventsToPushError(`No events pushed`); | ||
throw new NoEventsToPushError(Text.error.noEventsPushed); | ||
} | ||
if (Array.isArray(payloads) && payloads.length > MAXIMUM_EVENTS) { | ||
throw new TooManyEventsError(`Maximum of ${MAXIMUM_EVENTS} events allowed in a single push`); | ||
throw new TooManyEventsError(Text.error.maxEventsAllowed(MAXIMUM_EVENTS)); | ||
} | ||
@@ -35,11 +45,45 @@ | ||
if (payloadSizeKB > MAXIMUM_PAYLOAD_SIZE_KB) { | ||
throw new PayloadTooBigError(`The maximum payload size allowed is ${MAXIMUM_PAYLOAD_SIZE_KB}KB`); | ||
throw new PayloadTooBigError(Text.error.maxPayloadAllowed(MAXIMUM_PAYLOAD_SIZE_KB)); | ||
} | ||
}; | ||
export const validateAPIResponse = async (response: APIResponse, requestBody: APIRequest) => { | ||
export const validateGetStatsPayload = (getStatsRequest: GetStatsRequest) => { | ||
if (!getStatsRequest.jobId) { | ||
throw new JobDoesNotExistError(Text.error.jobIdEmpty); | ||
} | ||
validateQueueKey(getStatsRequest.queueName); | ||
}; | ||
export const validateAPIResponse = async (response: APIResponse, expectedSuccessStatus: number) => { | ||
if (response.status === 429) { | ||
throw new RateLimitError(`Too many requests`); | ||
throw new RateLimitError(Text.error.rateLimitError); | ||
} | ||
if (response.status === 405) { | ||
throw new InvocationLimitReachedError(); | ||
} | ||
if (response.status != expectedSuccessStatus && response.status) { | ||
//Catch all errors from server that we have not handled | ||
let internalServerError; | ||
try { | ||
const responseBody = await response.json(); | ||
const errorMessage = responseBody.message ? `: ${responseBody.message}` : ''; | ||
const errors = responseBody.errors ? `: ${responseBody.errors.join(', ')}` : ''; //Dropwizard returns an array of errors when request body validation failed | ||
internalServerError = new InternalServerError( | ||
`${response.status} ${response.statusText}${errorMessage}${errors}`, | ||
responseBody.code, | ||
responseBody.details | ||
); | ||
} catch (ignore) { | ||
//response body is not a json | ||
internalServerError = new InternalServerError(`${response.status} ${response.statusText}`, response.status); | ||
} | ||
throw internalServerError; | ||
} | ||
}; | ||
export const validatePushAPIResponse = async (response: APIResponse, requestBody: PushRequest) => { | ||
if (response.status === 413) { | ||
@@ -53,27 +97,35 @@ //Server can return this error response if it has a different max payload size and max number of events limits | ||
const responseBody = await response.json(); | ||
const defaultErrorMessage = 'Failed to process some events.'; | ||
const partialSuccessError = new PartialSuccessError(defaultErrorMessage, []); | ||
if (responseBody.failedEvents && responseBody.failedEvents.length > 0) { | ||
throw new PartialSuccessError( | ||
`Failed to process ${responseBody.failedEvents.length} events`, | ||
responseBody.failedEvents.map((failedEvent: any) => { | ||
return { | ||
errorMessage: failedEvent.errorMessage, | ||
payload: requestBody.payload[+failedEvent.index] | ||
}; | ||
}) | ||
); | ||
} else { | ||
throw new PartialSuccessError(`Failed to process events`, []); | ||
partialSuccessError.message = `Failed to process ${responseBody.failedEvents.length} event(s).`; | ||
partialSuccessError.failedEvents = responseBody.failedEvents.map((failedEvent: any) => { | ||
return { | ||
errorMessage: failedEvent.errorMessage, | ||
payload: requestBody.payload[+failedEvent.index] | ||
}; | ||
}); | ||
} | ||
if (responseBody.errorMessage) { | ||
//Append any extra error message from backend | ||
partialSuccessError.message = | ||
partialSuccessError.message !== defaultErrorMessage | ||
? `${partialSuccessError.message} ${responseBody.errorMessage}` | ||
: responseBody.errorMessage; | ||
} | ||
throw partialSuccessError; | ||
} | ||
if (response.status != 201 && response.status) { | ||
//Catch all errors from server that we have not handled | ||
const responseBody = await response.json(); | ||
const errorMessage = responseBody.message ? `: ${responseBody.message}` : ''; | ||
throw new InternalServerError( | ||
`${response.status} ${response.statusText}${errorMessage}`, | ||
responseBody.code, | ||
responseBody.details | ||
); | ||
await validateAPIResponse(response, 201); | ||
}; | ||
export const validateGetStatsAPIResponse = async (response: APIResponse, getStatsRequest: GetStatsRequest) => { | ||
if (response.status === 404) { | ||
throw new JobDoesNotExistError(Text.error.jobDoesNotExit(getStatsRequest.jobId, getStatsRequest.queueName)); | ||
} | ||
await validateAPIResponse(response, 200); | ||
}; |
{ | ||
"extends": "../../tsconfig-base.json", | ||
"compilerOptions": { | ||
"outDir": "./out", | ||
"rootDir": "src", | ||
"composite": true | ||
}, | ||
"references": [] | ||
} | ||
"extends": "../../tsconfig-base.json", | ||
"compilerOptions": { | ||
"outDir": "./out", | ||
"rootDir": "src", | ||
"composite": true | ||
}, | ||
"references": [ | ||
{ | ||
"path": "../forge-api" | ||
} | ||
] | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
122104
57
1414
2
2
1
+ Addeduuid@^3.4.0
+ Added@forge/api@0.0.0-experimental-f85f9b1(transitive)
+ Added@forge/auth@0.0.1(transitive)
+ Added@forge/egress@1.1.2(transitive)
+ Added@forge/storage@0.0.0-experimental-f85f9b1(transitive)
+ Added@types/node@22.10.2(transitive)
+ Added@types/node-fetch@2.6.12(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbrace-expansion@2.0.1(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addedform-data@4.0.1(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedminimatch@5.1.6(transitive)
+ Addednode-fetch@2.6.7(transitive)
+ Addedtr46@0.0.3(transitive)
+ Addedtslib@1.14.1(transitive)
+ Addedundici-types@6.20.0(transitive)
+ Addeduuid@3.4.0(transitive)
+ Addedwebidl-conversions@3.0.1(transitive)
+ Addedwhatwg-url@5.0.0(transitive)