UNPKG

@swarmion/serverless-contracts

Version:

Generate and use type-safe contracts between your Serverless services.

1,513 lines (1,468 loc) 53.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { ApiGatewayContract: () => ApiGatewayContract, CloudFormationContract: () => CloudFormationContract, EventBridgeContract: () => EventBridgeContract, HttpStatusCodes: () => HttpStatusCodes, InfiniteThroughput: () => InfiniteThroughput, SQSContract: () => SQSContract, SwarmionRouter: () => SwarmionRouter, buildPutEvent: () => buildPutEvent, buildPutEvents: () => buildPutEvents, buildSendMessage: () => buildSendMessage, buildSendMessages: () => buildSendMessages, getApiGatewayHandler: () => getApiGatewayHandler, getApiGatewayTrigger: () => getApiGatewayTrigger, getAxiosRequest: () => getAxiosRequest, getContractFullSchema: () => getContractFullSchema, getEventBridgeHandler: () => getEventBridgeHandler, getEventBridgeTrigger: () => getEventBridgeTrigger, getFetchRequest: () => getFetchRequest, getHandler: () => getHandler, getLambdaHandler: () => getLambdaHandler, getMultipleEventBridgeHandler: () => getMultipleEventBridgeHandler, getOpenApiDocumentation: () => getOpenApiDocumentation, getRequestParameters: () => getRequestParameters, getSQSHandler: () => getSQSHandler, getTrigger: () => getTrigger, handle: () => handle }); module.exports = __toCommonJS(index_exports); // src/contracts/cloudFormation/cloudFormationContract.ts var CloudFormationContract = class { contractType = "cloudFormation"; id; name; /** * Builds a new ApiGateway contract * * @param id a unique id to identify the contract among stacks. Beware uniqueness! * @param name the name of the export * See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html */ constructor({ id, name }) { this.id = id; this.name = name; } /** * @returns the CloudFormation { Fn::ImportValue } function corresponding to the name */ get importValue() { return { "Fn::ImportValue": this.name }; } /** * @returns the CloudFormation import name to use with Fn.importValue in CDK */ get cdkImportValue() { return this.name; } /** * @param description the description used in CloudFormation for this value * @param value the CloudFormation function passed to the export * For more information, see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html * @returns the CloudFormation Export function */ exportValue({ description, value }) { return { Description: description, Value: value, Export: { Name: this.name } }; } }; // src/contracts/apiGateway/apiGatewayContract.ts var import_isUndefined = __toESM(require("lodash/isUndefined.js"), 1); var import_omitBy = __toESM(require("lodash/omitBy.js"), 1); var ApiGatewayContract = class { contractType = "apiGateway"; id; path; method; integrationType; authorizerType; pathParametersSchema; queryStringParametersSchema; headersSchema; requestContextSchema; bodySchema; outputSchemas; inputSchema; /** * Builds a new ApiGateway contract * * @param props - the contract properties */ constructor(props) { this.id = props.id; this.path = props.path; this.method = props.method; this.integrationType = props.integrationType; this.authorizerType = props.authorizerType ?? void 0; this.pathParametersSchema = props.pathParametersSchema ?? void 0; this.queryStringParametersSchema = props.queryStringParametersSchema ?? void 0; this.headersSchema = props.headersSchema ?? void 0; this.requestContextSchema = props.requestContextSchema ?? void 0; this.bodySchema = props.bodySchema ?? void 0; this.outputSchemas = props.outputSchemas ?? {}; this.inputSchema = this.getInputSchema(); } getInputSchema() { const properties = (0, import_omitBy.default)( { pathParameters: this.pathParametersSchema, queryStringParameters: this.queryStringParametersSchema, headers: this.headersSchema, requestContext: this.requestContextSchema, body: this.bodySchema }, import_isUndefined.default ); return { type: "object", properties, required: Object.keys(properties), additionalProperties: true }; } }; // src/contracts/apiGateway/features/requestParameters.ts var import_isUndefined2 = __toESM(require("lodash/isUndefined.js"), 1); var import_omitBy2 = __toESM(require("lodash/omitBy.js"), 1); // src/utils/fillPathTemplate.ts var fillPathTemplate = (template, values) => values === void 0 ? template : Object.entries(values).reduce((accumulator, [key, value]) => { const re = new RegExp(`{${key}}`, "g"); return accumulator.replace(re, value); }, template); // src/utils/isDefined.ts var isDefined = (value) => value !== void 0; // src/utils/chunkEntriesBatch.ts var MaxSizeExceededError = class extends Error { constructor(message) { super(message); this.name = "MaxSizeExceededError"; } }; var chunkEntriesBatch = ({ entries, computeEntrySize, maxBatchSize, maxBatchLength }) => entries.reduce( (chunkedEntriesAccumulator, entry, index) => { const { chunkedEntries, lastChunkSize, lastChunkLength } = chunkedEntriesAccumulator; const entrySize = computeEntrySize(entry); if (entrySize > maxBatchSize) { throw new MaxSizeExceededError( `Entry ${index} size is ${entrySize}b exceeds the maximum batch size of ${maxBatchSize}b. The whole operation has been cancelled.` ); } if (lastChunkSize + entrySize > maxBatchSize || lastChunkLength === maxBatchLength) { return { chunkedEntries: [...chunkedEntries, [entry]], lastChunkSize: entrySize, lastChunkLength: 1 }; } const lastChunk = chunkedEntries.pop() ?? []; return { chunkedEntries: [...chunkedEntries, [...lastChunk, entry]], lastChunkSize: lastChunkSize + entrySize, lastChunkLength: lastChunkLength + 1 }; }, { chunkedEntries: [], lastChunkSize: 0, lastChunkLength: 0 } ).chunkedEntries; // src/contracts/apiGateway/features/requestParameters.ts var getRequestParameters = (contract, requestArguments) => { const { pathParameters, queryStringParameters, headers, body } = requestArguments; const path = typeof pathParameters !== "undefined" ? fillPathTemplate(contract.path, pathParameters) : contract.path; return (0, import_omitBy2.default)( { method: contract.method, path, body, queryStringParameters: (0, import_omitBy2.default)( queryStringParameters, import_isUndefined2.default ), headers: { ...headers, "Content-Type": "application/json" } }, import_isUndefined2.default ); }; // src/contracts/apiGateway/features/axiosRequest.ts var getAxiosRequest = async (contract, axiosClient, requestArguments) => { const { path, method, queryStringParameters, body, headers } = getRequestParameters(contract, requestArguments); return axiosClient.request({ method, url: path, headers, data: body, params: queryStringParameters }); }; // src/contracts/apiGateway/utils/apiGatewayProxyTransformers.ts var proxyEventToHandlerEvent = ({ requestContext, body: proxyEventBody = null, headers, pathParameters, queryStringParameters }) => { return { requestContext, body: proxyEventBody !== null ? JSON.parse(proxyEventBody) : void 0, headers, pathParameters, queryStringParameters: queryStringParameters ?? {} }; }; var handlerResponseToProxyResult = ({ statusCode, headers, body }) => { const contentTypeHeader = body !== void 0 ? { "Content-Type": "application/json" } : void 0; return { statusCode, body: body !== void 0 ? JSON.stringify(body) : "", headers: { ...contentTypeHeader, ...headers } }; }; // src/contracts/apiGateway/utils/convertJsonSchemaToValidOAS3.ts var import_cloneDeep = __toESM(require("lodash/cloneDeep.js"), 1); var import_mapValues = __toESM(require("lodash/mapValues.js"), 1); var isArrayOfString = (array) => Array.isArray(array) && array.every((element) => typeof element === "string"); var convertNullableSchema = (schema) => { if (schema.type?.includes("null")) { if (isArrayOfString(schema.type)) { const filteredSchemaType = schema.type.filter((type) => type !== "null"); if (filteredSchemaType && filteredSchemaType.length === 1) { schema.type = filteredSchemaType[0]; } } else { delete schema.type; } schema.nullable = true; } }; var convertArraySchemaPropertiesToValidOAS3 = (schema) => { if (schema.items) { if (Array.isArray(schema.items)) { schema.items = schema.items.map(convertJsonSchemaToValidOAS3); } else { schema.items = convertJsonSchemaToValidOAS3(schema.items); } } }; var convertObjectSchemaPropertiesToValidOAS3 = (schema) => { if (schema.properties) { schema.properties = (0, import_mapValues.default)( schema.properties, convertJsonSchemaToValidOAS3 ); } if (schema.additionalProperties) { schema.additionalProperties = convertJsonSchemaToValidOAS3( // @ts-expect-error problem with recursively typing, fully unit tested schema.additionalProperties ); } if (schema.patternProperties) { schema.patternProperties = (0, import_mapValues.default)( // @ts-expect-error problem with recursively typing, fully unit tested schema.patternProperties, convertJsonSchemaToValidOAS3 ); } }; var convertJsonSchemaToValidOAS3 = (initialSchema) => { if (typeof initialSchema === "boolean") { return initialSchema; } const schema = (0, import_cloneDeep.default)(initialSchema); if (schema.examples && schema.examples[0] !== void 0) { schema.example = schema.examples[0]; } delete schema.examples; if (schema.const) { schema.enum = [schema.const]; delete schema.const; } convertNullableSchema(schema); convertArraySchemaPropertiesToValidOAS3(schema); convertObjectSchemaPropertiesToValidOAS3(schema); if (schema.anyOf) { schema.anyOf = schema.anyOf.map(convertJsonSchemaToValidOAS3); } if (schema.oneOf) { schema.oneOf = schema.oneOf.map(convertJsonSchemaToValidOAS3); } if (schema.allOf) { schema.allOf = schema.allOf.map(convertJsonSchemaToValidOAS3); } if (schema.not) { schema.not = convertJsonSchemaToValidOAS3(schema.not); } if (schema.definitions) { schema.definitions = (0, import_mapValues.default)( // @ts-expect-error problem with recursively typing, fully unit tested schema.definitions, convertJsonSchemaToValidOAS3 ); } return schema; }; // src/contracts/apiGateway/utils/combineUrls.ts var combineUrls = (path, baseUrl) => { const stringBaseUrl = baseUrl instanceof URL ? baseUrl.toString() : baseUrl; const pathWithoutLeadingSlash = path.replace(/^\/+/, ""); const baseUrlWithTrailingSlash = stringBaseUrl.replace(/\/+$/, "") + "/"; return new URL(pathWithoutLeadingSlash, baseUrlWithTrailingSlash); }; // src/contracts/apiGateway/features/fetchRequest.ts var getFetchRequest = async (contract, fetchFunction, options) => { const { path, method, queryStringParameters, body, headers } = getRequestParameters(contract, options); let url; const searchString = new URLSearchParams(queryStringParameters).toString(); if (options.baseUrl !== void 0) { url = combineUrls(path, options.baseUrl); url.search = searchString; } else { url = `${path}?${searchString}`; } const response = await fetchFunction(url, { method, headers, body: JSON.stringify(body) }); return { statusCode: response.status, body: await response.json() }; }; // src/contracts/apiGateway/features/fullContractSchema.ts var import_isUndefined3 = __toESM(require("lodash/isUndefined.js"), 1); var import_omitBy3 = __toESM(require("lodash/omitBy.js"), 1); var getFullContractSchema = (contract) => { const properties = { contractId: { const: contract.id }, contractType: { const: contract.integrationType }, path: { const: contract.path }, method: { const: contract.method }, ...(0, import_omitBy3.default)( { pathParameters: contract.pathParametersSchema, queryStringParameters: contract.queryStringParametersSchema, headers: contract.headersSchema, body: contract.bodySchema, outputs: { type: "object", properties: contract.outputSchemas, required: Object.keys(contract.outputSchemas) } }, import_isUndefined3.default ) }; return { type: "object", // @ts-ignore type inference does not work here properties, // @ts-ignore type inference does not work here required: Object.keys(properties), additionalProperties: false }; }; // src/contracts/apiGateway/features/handleRouter.ts var handle = (router, options) => { return async (event, ...otherArgs) => { if (options?.logger === true) { console.log("event", event); } const matchedRoute = router.match(event); if (matchedRoute === null) { return Promise.resolve({ statusCode: 404, body: "Not found" }); } const [handler, newEvent] = matchedRoute; return handler(newEvent, ...otherArgs); }; }; // src/contracts/apiGateway/features/lambdaHandler.ts var import_http_errors = __toESM(require("http-errors"), 1); // src/types/http.ts var HttpStatusCodes = /* @__PURE__ */ ((HttpStatusCodes2) => { HttpStatusCodes2[HttpStatusCodes2["ACCEPTED"] = 202] = "ACCEPTED"; HttpStatusCodes2[HttpStatusCodes2["BAD_GATEWAY"] = 502] = "BAD_GATEWAY"; HttpStatusCodes2[HttpStatusCodes2["BAD_REQUEST"] = 400] = "BAD_REQUEST"; HttpStatusCodes2[HttpStatusCodes2["CONFLICT"] = 409] = "CONFLICT"; HttpStatusCodes2[HttpStatusCodes2["CONTINUE"] = 100] = "CONTINUE"; HttpStatusCodes2[HttpStatusCodes2["CREATED"] = 201] = "CREATED"; HttpStatusCodes2[HttpStatusCodes2["EXPECTATION_FAILED"] = 417] = "EXPECTATION_FAILED"; HttpStatusCodes2[HttpStatusCodes2["FAILED_DEPENDENCY"] = 424] = "FAILED_DEPENDENCY"; HttpStatusCodes2[HttpStatusCodes2["FORBIDDEN"] = 403] = "FORBIDDEN"; HttpStatusCodes2[HttpStatusCodes2["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT"; HttpStatusCodes2[HttpStatusCodes2["GONE"] = 410] = "GONE"; HttpStatusCodes2[HttpStatusCodes2["HTTP_VERSION_NOT_SUPPORTED"] = 505] = "HTTP_VERSION_NOT_SUPPORTED"; HttpStatusCodes2[HttpStatusCodes2["IM_A_TEAPOT"] = 418] = "IM_A_TEAPOT"; HttpStatusCodes2[HttpStatusCodes2["INSUFFICIENT_SPACE_ON_RESOURCE"] = 419] = "INSUFFICIENT_SPACE_ON_RESOURCE"; HttpStatusCodes2[HttpStatusCodes2["INSUFFICIENT_STORAGE"] = 507] = "INSUFFICIENT_STORAGE"; HttpStatusCodes2[HttpStatusCodes2["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR"; HttpStatusCodes2[HttpStatusCodes2["LENGTH_REQUIRED"] = 411] = "LENGTH_REQUIRED"; HttpStatusCodes2[HttpStatusCodes2["LOCKED"] = 423] = "LOCKED"; HttpStatusCodes2[HttpStatusCodes2["METHOD_FAILURE"] = 420] = "METHOD_FAILURE"; HttpStatusCodes2[HttpStatusCodes2["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED"; HttpStatusCodes2[HttpStatusCodes2["MOVED_PERMANENTLY"] = 301] = "MOVED_PERMANENTLY"; HttpStatusCodes2[HttpStatusCodes2["MOVED_TEMPORARILY"] = 302] = "MOVED_TEMPORARILY"; HttpStatusCodes2[HttpStatusCodes2["MULTI_STATUS"] = 207] = "MULTI_STATUS"; HttpStatusCodes2[HttpStatusCodes2["MULTIPLE_CHOICES"] = 300] = "MULTIPLE_CHOICES"; HttpStatusCodes2[HttpStatusCodes2["NETWORK_AUTHENTICATION_REQUIRED"] = 511] = "NETWORK_AUTHENTICATION_REQUIRED"; HttpStatusCodes2[HttpStatusCodes2["NO_CONTENT"] = 204] = "NO_CONTENT"; HttpStatusCodes2[HttpStatusCodes2["NON_AUTHORITATIVE_INFORMATION"] = 203] = "NON_AUTHORITATIVE_INFORMATION"; HttpStatusCodes2[HttpStatusCodes2["NOT_ACCEPTABLE"] = 406] = "NOT_ACCEPTABLE"; HttpStatusCodes2[HttpStatusCodes2["NOT_FOUND"] = 404] = "NOT_FOUND"; HttpStatusCodes2[HttpStatusCodes2["NOT_IMPLEMENTED"] = 501] = "NOT_IMPLEMENTED"; HttpStatusCodes2[HttpStatusCodes2["NOT_MODIFIED"] = 304] = "NOT_MODIFIED"; HttpStatusCodes2[HttpStatusCodes2["OK"] = 200] = "OK"; HttpStatusCodes2[HttpStatusCodes2["PARTIAL_CONTENT"] = 206] = "PARTIAL_CONTENT"; HttpStatusCodes2[HttpStatusCodes2["PAYMENT_REQUIRED"] = 402] = "PAYMENT_REQUIRED"; HttpStatusCodes2[HttpStatusCodes2["PERMANENT_REDIRECT"] = 308] = "PERMANENT_REDIRECT"; HttpStatusCodes2[HttpStatusCodes2["PRECONDITION_FAILED"] = 412] = "PRECONDITION_FAILED"; HttpStatusCodes2[HttpStatusCodes2["PRECONDITION_REQUIRED"] = 428] = "PRECONDITION_REQUIRED"; HttpStatusCodes2[HttpStatusCodes2["PROCESSING"] = 102] = "PROCESSING"; HttpStatusCodes2[HttpStatusCodes2["PROXY_AUTHENTICATION_REQUIRED"] = 407] = "PROXY_AUTHENTICATION_REQUIRED"; HttpStatusCodes2[HttpStatusCodes2["REQUEST_HEADER_FIELDS_TOO_LARGE"] = 431] = "REQUEST_HEADER_FIELDS_TOO_LARGE"; HttpStatusCodes2[HttpStatusCodes2["REQUEST_TIMEOUT"] = 408] = "REQUEST_TIMEOUT"; HttpStatusCodes2[HttpStatusCodes2["REQUEST_TOO_LONG"] = 413] = "REQUEST_TOO_LONG"; HttpStatusCodes2[HttpStatusCodes2["REQUEST_URI_TOO_LONG"] = 414] = "REQUEST_URI_TOO_LONG"; HttpStatusCodes2[HttpStatusCodes2["REQUESTED_RANGE_NOT_SATISFIABLE"] = 416] = "REQUESTED_RANGE_NOT_SATISFIABLE"; HttpStatusCodes2[HttpStatusCodes2["RESET_CONTENT"] = 205] = "RESET_CONTENT"; HttpStatusCodes2[HttpStatusCodes2["SEE_OTHER"] = 303] = "SEE_OTHER"; HttpStatusCodes2[HttpStatusCodes2["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE"; HttpStatusCodes2[HttpStatusCodes2["SWITCHING_PROTOCOLS"] = 101] = "SWITCHING_PROTOCOLS"; HttpStatusCodes2[HttpStatusCodes2["TEMPORARY_REDIRECT"] = 307] = "TEMPORARY_REDIRECT"; HttpStatusCodes2[HttpStatusCodes2["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS"; HttpStatusCodes2[HttpStatusCodes2["UNAUTHORIZED"] = 401] = "UNAUTHORIZED"; HttpStatusCodes2[HttpStatusCodes2["UNAVAILABLE_FOR_LEGAL_REASONS"] = 451] = "UNAVAILABLE_FOR_LEGAL_REASONS"; HttpStatusCodes2[HttpStatusCodes2["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY"; HttpStatusCodes2[HttpStatusCodes2["UNSUPPORTED_MEDIA_TYPE"] = 415] = "UNSUPPORTED_MEDIA_TYPE"; HttpStatusCodes2[HttpStatusCodes2["USE_PROXY"] = 305] = "USE_PROXY"; HttpStatusCodes2[HttpStatusCodes2["MISDIRECTED_REQUEST"] = 421] = "MISDIRECTED_REQUEST"; return HttpStatusCodes2; })(HttpStatusCodes || {}); // src/contracts/apiGateway/features/lambdaHandler.ts var defaultOptions = { validateInput: true, validateOutput: true }; var getApiGatewayHandler = (contract, options) => (handler) => { const { validateInput, validateOutput, ajv } = { ...defaultOptions, ...options }; const { inputSchema, outputSchemas } = contract; let inputValidator = void 0; const outputValidators = {}; if (validateInput || validateOutput) { if (validateInput) { inputValidator = ajv.compile(inputSchema); } if (validateOutput) { Object.keys(outputSchemas).forEach((statusCode) => { const outputSchema = outputSchemas[statusCode]; outputValidators[statusCode] = outputSchema !== void 0 ? ajv.compile(outputSchema) : void 0; }); } } return async (event, context, callback, ...additionalArgs) => { try { const parsedEvent = proxyEventToHandlerEvent(event); if (inputValidator !== void 0) { if (!inputValidator(parsedEvent)) { console.error("Error: Invalid input"); console.error(JSON.stringify(inputValidator.errors, null, 2)); if (options.returnValidationErrors === true) { throw (0, import_http_errors.default)( 400, JSON.stringify({ message: "Invalid input", errors: inputValidator.errors }) ); } throw (0, import_http_errors.default)(400, "Invalid input"); } } const handlerResponse = await handler( parsedEvent, context, callback, ...additionalArgs ); const outputValidator = outputValidators[handlerResponse.statusCode]; if (outputValidator !== void 0) { if (!outputValidator(handlerResponse.body)) { console.error("Error: Invalid output"); console.error(JSON.stringify(outputValidator.errors, null, 2)); throw (0, import_http_errors.default)(400, "Invalid output"); } } return handlerResponseToProxyResult( handlerResponse ); } catch (error) { console.error(error); if ((0, import_http_errors.isHttpError)(error) && error.expose) { return { headers: error.headers, statusCode: error.statusCode, body: error.message }; } return { statusCode: 500 /* INTERNAL_SERVER_ERROR */, body: "Internal server error" }; } }; }; var getLambdaHandler = (_contract) => (handler) => handler; // src/contracts/apiGateway/features/lambdaTrigger.ts var getApiGatewayTrigger = (contract, ...[additionalConfig]) => { const key = contract.integrationType === "httpApi" ? "httpApi" : "http"; return { [key]: { ...additionalConfig, path: contract.path, method: contract.method } }; }; // src/contracts/apiGateway/features/openApiDocumentation.ts var import_isUndefined4 = __toESM(require("lodash/isUndefined.js"), 1); var import_omitBy4 = __toESM(require("lodash/omitBy.js"), 1); var getContractDocumentation = (contract) => { const initialDocumentation = { responses: {} }; const definedOutputSchema = (0, import_omitBy4.default)(contract.outputSchemas, import_isUndefined4.default); const contractDocumentation = Object.keys(definedOutputSchema).reduce( (config, responseCode) => { const schema = definedOutputSchema[responseCode]; if (schema === void 0) { return config; } const openApiSchema = convertJsonSchemaToValidOAS3(schema); return { ...config, responses: { ...config.responses, [responseCode]: { description: `Response: ${responseCode}`, content: { "application/json": { schema: openApiSchema } } } } }; }, initialDocumentation ); if (contract.pathParametersSchema?.properties !== void 0) { contractDocumentation.parameters = [ ...Object.entries(contract.pathParametersSchema.properties).map( ([variableName, variableDefinition]) => ({ name: variableName, in: "path", schema: convertJsonSchemaToValidOAS3(variableDefinition), required: contract.pathParametersSchema?.required?.includes(variableName) ?? false }) ), ...contractDocumentation.parameters ?? [] ]; } if (contract.queryStringParametersSchema?.properties !== void 0) { contractDocumentation.parameters = [ ...Object.entries(contract.queryStringParametersSchema.properties).map( ([variableName, variableDefinition]) => ({ name: variableName, in: "query", schema: convertJsonSchemaToValidOAS3(variableDefinition), required: contract.queryStringParametersSchema?.required?.includes( variableName ) ?? false }) ), ...contractDocumentation.parameters ?? [] ]; } if (contract.headersSchema?.properties !== void 0) { contractDocumentation.parameters = [ ...Object.entries(contract.headersSchema.properties).map( ([variableName, variableDefinition]) => ({ name: variableName, in: "header", schema: convertJsonSchemaToValidOAS3(variableDefinition), required: contract.headersSchema?.required?.includes(variableName) ?? false }) ), ...contractDocumentation.parameters ?? [] ]; } if (contract.bodySchema !== void 0) { contractDocumentation.requestBody = { content: { "application/json": { schema: convertJsonSchemaToValidOAS3(contract.bodySchema) } } }; } return { path: contract.path, method: contract.method.toLowerCase(), documentation: contractDocumentation }; }; // src/contracts/apiGateway/features/router.ts var import_ajv = __toESM(require("ajv"), 1); var import_get = __toESM(require("lodash/get.js"), 1); var import_path_to_regexp = require("path-to-regexp"); var SwarmionRouter = class { consumers = []; ajv = new import_ajv.default(); constructor({ ajv } = {}) { if (ajv) { this.ajv = ajv; } } add(contract, options) { return (handler) => { this.consumers.push([ contract, // @ts-expect-error - This is a valid handler getApiGatewayHandler(contract, { ...options, ajv: this.ajv })(handler) ]); }; } match(event) { if (!isValidHttpApiGatewayEvent(event) && !isValidRestApiGatewayEvent(event)) { return null; } for (const [contract, handler] of this.consumers) { const routerMatch = matchApiGatewayContract(contract, event); if (routerMatch === false) { continue; } return [handler, routerMatch]; } return null; } }; var matchApiGatewayContract = (contract, event) => { if (isValidRestApiGatewayEvent(event)) { return matchRestApiGatewayContract(contract, event); } if (isValidHttpApiGatewayEvent(event)) { return matchHttpApiGatewayContract(contract, event); } return false; }; var matchHttpApiGatewayContract = (contract, event) => { if (contract.integrationType !== "httpApi") { return false; } return matchHttpRoute({ contract, method: event.requestContext.http.method, path: event.rawPath, event }); }; var matchRestApiGatewayContract = (contract, event) => { if (contract.integrationType !== "restApi") { return false; } return matchHttpRoute({ contract, method: event.requestContext.httpMethod, path: event.path, event }); }; var matchHttpRoute = ({ contract, method, path, event }) => { if (method !== contract.method) { return false; } const urlMatch = (0, import_path_to_regexp.match)( contract.path.replaceAll("{", ":").replaceAll("}", "") )(path); if (urlMatch === false) { return false; } event.pathParameters = urlMatch.params; return event; }; var isValidHttpApiGatewayEvent = (event) => { return (0, import_get.default)(event, "requestContext.http") !== void 0; }; var isValidRestApiGatewayEvent = (event) => { return (0, import_get.default)(event, "requestContext.httpMethod") !== void 0; }; // src/contracts/eventBridge/eventBridgeContract.ts var EventBridgeContract = class { id; contractType = "eventBridge"; sources; eventType; payloadSchema; /** * a native event pattern: * ```ts * { * source: string[]; * 'detail-type': string[]; * } * ``` */ nativePattern; /** * an event pattern accepted by the CDK: * ```ts * { * source: string[]; * detailType: string[]; * } * ``` */ pattern; /** * Builds a new EventBridgeContract contract */ constructor({ id, sources, eventType, payloadSchema }) { this.id = id; this.sources = sources; this.eventType = eventType; this.payloadSchema = payloadSchema; this.nativePattern = { // @ts-expect-error it does not matter that sources are readonly source: sources, "detail-type": [eventType] }; this.pattern = { // @ts-expect-error it does not matter that sources are readonly source: sources, detailType: [eventType] }; } }; // src/contracts/eventBridge/features/lambdaTrigger.ts var getEventBridgeTrigger = (contract, additionalTriggerArgs) => ({ eventBridge: { pattern: { source: contract.sources, "detail-type": [contract.eventType] }, ...additionalTriggerArgs } }); // src/contracts/eventBridge/features/fullContractSchema.ts var getFullContractSchema2 = (contract) => { const properties = { id: { const: contract.id }, contractType: { const: contract.contractType }, sources: { enum: contract.sources }, eventType: { const: contract.eventType }, payloadSchema: contract.payloadSchema }; return { type: "object", properties, required: ["id", "contractType", "sources", "eventType", "payloadSchema"], additionalProperties: false }; }; // src/contracts/eventBridge/features/lambdaHandler.ts var defaultOptions2 = { validatePayload: true }; var getEventBridgeHandler = (contract, options) => (handler) => { const { validatePayload, ajv } = { ...defaultOptions2, ...options }; let payloadValidator = void 0; if (validatePayload) { payloadValidator = ajv.compile(contract.payloadSchema); } return async (event, context, callback, ...additionalArgs) => { if (payloadValidator !== void 0) { if (!payloadValidator(event.detail)) { console.error("Error: Invalid payload"); console.error(JSON.stringify(payloadValidator.errors, null, 2)); throw new Error("Invalid payload"); } } const handlerResponse = await handler( event, context, callback, ...additionalArgs ); return handlerResponse; }; }; // src/contracts/eventBridge/features/putEvent.ts var import_client_eventbridge = require("@aws-sdk/client-eventbridge"); var buildPutEvent = (contract, { eventBusName, source, eventBridgeClient, throwOnFailure = true }) => async (payload) => { const event = { Detail: JSON.stringify(payload), DetailType: contract.eventType, Source: source, EventBusName: typeof eventBusName === "string" ? eventBusName : await eventBusName() }; const command = new import_client_eventbridge.PutEventsCommand({ Entries: [event] }); const { FailedEntryCount, Entries } = await eventBridgeClient.send(command); if ((FailedEntryCount ?? 0) > 0 && throwOnFailure) { const failedEntry = Entries?.[0]; console.error( `Failed to send this event: ${JSON.stringify(event, null, 2)}` ); throw new Error( `Failed to send event. Error: ${failedEntry?.ErrorMessage} (${failedEntry?.ErrorCode})` ); } return { failedEntryCount: FailedEntryCount, entry: Entries?.[0] }; }; // src/contracts/eventBridge/features/putEvents.ts var import_client_eventbridge2 = require("@aws-sdk/client-eventbridge"); var import_p_queue = __toESM(require("p-queue"), 1); // src/utils/fixESMInteropIssue.ts var fixESMInteropIssue = (object) => { if (object && object.default) { return object.default; } return object; }; // src/contracts/eventBridge/features/putEvents.ts var PQueue = fixESMInteropIssue(import_p_queue.default); var MAX_BATCH_SIZE = 256e3; var MAX_BATCH_LENGTH = 10; var defaultOptions3 = { throughputCallsPerSecond: 400, // Minimal default throughput. Some regions can have up to 10_000. See: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-quota.html throwOnFailure: true }; var OneSecondInMillis = 1e3; var getThroughputQueueConfig = (throughputCallsPerSecond) => ({ intervalCap: throughputCallsPerSecond, interval: OneSecondInMillis }); var computeEventSize = (event) => { let size = 0; if (event.Time) { size += 14; } if (event.Detail) { size += Buffer.byteLength(event.Detail, "utf8"); } if (event.DetailType) { size += Buffer.byteLength(event.DetailType, "utf8"); } if (event.Source) { size += Buffer.byteLength(event.Source, "utf8"); } if (event.Resources) { event.Resources.forEach((resource) => Buffer.byteLength(resource, "utf8")); } return size; }; var sendAllEventsBatchedWithControlledThroughput = async (events, options) => { const { eventBridgeClient, throughputCallsPerSecond } = options; const queue = new PQueue(getThroughputQueueConfig(throughputCallsPerSecond)); const batches = chunkEntriesBatch({ entries: events, computeEntrySize: computeEventSize, maxBatchSize: MAX_BATCH_SIZE, maxBatchLength: MAX_BATCH_LENGTH }); const results = await queue.addAll( batches.map( (batch) => async () => await eventBridgeClient.send( new import_client_eventbridge2.PutEventsCommand({ Entries: batch }) ) ) ); return { failedEntryCount: results.reduce( (sum, { FailedEntryCount }) => sum + (FailedEntryCount ?? 0), 0 ), entries: results.map(({ Entries }) => Entries ?? []).flat() }; }; var buildPutEvents = (contract, args) => async (payloads) => { const argsWithDefaults = { ...defaultOptions3, ...args }; const { source, eventBusName: eventBusNameGetter, throwOnFailure } = argsWithDefaults; const eventBusName = typeof eventBusNameGetter === "string" ? eventBusNameGetter : await eventBusNameGetter(); const events = payloads.map((payload) => ({ Detail: JSON.stringify(payload), DetailType: contract.eventType, Source: source, EventBusName: eventBusName })); const { failedEntryCount, entries } = await sendAllEventsBatchedWithControlledThroughput( events, argsWithDefaults ); if (failedEntryCount > 0 && throwOnFailure) { entries.forEach((entry) => { if (entry.ErrorCode) { console.error( `Failed to send event. Error: ${entry.ErrorMessage} (${entry.ErrorCode})` ); } }); throw new Error(`Failed to send ${failedEntryCount} events.`); } return { failedEntryCount, entries }; }; // src/contracts/eventBridge/features/multipleLambdaHandler.ts var defaultOptions4 = { validatePayload: true }; var getMultipleEventBridgeHandler = (contracts, options) => (handler) => { const { validatePayload, ajv } = { ...defaultOptions4, ...options }; let payloadValidator = void 0; if (validatePayload) { payloadValidator = ajv.compile({ anyOf: contracts.map((contract) => contract.payloadSchema) }); } return async (event, context, callback, ...additionalArgs) => { if (payloadValidator !== void 0) { if (!payloadValidator(event.detail)) { console.error("Error: Invalid payload"); console.error(JSON.stringify(payloadValidator.errors, null, 2)); throw new Error("Invalid payload"); } } const handlerResponse = await handler( event, context, callback, ...additionalArgs ); return handlerResponse; }; }; // src/contracts/SQS/sqsContract.ts var SQSContract = class { id; contractType = "SQS"; messageBodySchema; messageAttributesSchema; /** * Builds a new SQSContract contract */ constructor({ id, messageBodySchema, messageAttributesSchema }) { this.id = id; this.messageBodySchema = messageBodySchema; this.messageAttributesSchema = messageAttributesSchema ?? {}; } }; // src/contracts/SQS/features/fullContractSchema.ts var getFullContractSchema3 = (contract) => ({ type: "object", properties: { id: { const: contract.id }, contractType: { const: contract.contractType }, messageBodySchema: contract.messageBodySchema, ...Object.keys(contract.messageAttributesSchema).length > 0 ? { messageAttributesSchema: contract.messageAttributesSchema } : {} }, required: ["id", "contractType", "messageBodySchema"], additionalProperties: false }); // src/contracts/SQS/utils/parseMessageAttributes.ts var parseDataType = (dataType) => dataType.split(".")[0]; var parseMessageAttribute = (attribute) => { const dataType = parseDataType(attribute.dataType); switch (dataType) { case "Binary": return attribute.binaryValue; case "String": return attribute.stringValue; case "Number": return Number(attribute.stringValue); } }; var parseMessageAttributes = (messageAttributes) => Object.entries(messageAttributes).reduce( (acc, [attributeName, attribute]) => { return { ...acc, [attributeName]: parseMessageAttribute(attribute) }; }, {} ); // src/contracts/SQS/utils/parseRecord.ts var parseRecord = ({ bodyParser }) => ({ body, messageAttributes, ...rest }, index) => { try { return { ...rest, body: bodyParser !== void 0 ? bodyParser(body) : body, // Validation is done after messageAttributes: parseMessageAttributes( messageAttributes ) // Validation is done after }; } catch (e) { console.error( `Error while parsing SQS Record at index ${index}. Please use the \`logRawEvent\` option to debug` ); throw e; } }; // src/contracts/SQS/utils/getRecordsValidator.ts var getSchema = (contract, { validateBody, validateAttributes }) => ({ type: "array", items: { type: "object", properties: { ...validateBody === true && { body: contract.messageBodySchema }, ...validateAttributes === true && { messageAttributes: contract.messageAttributesSchema } }, required: [ ...validateBody !== void 0 ? ["body"] : [], ...validateAttributes !== void 0 ? ["messageAttributes"] : [] ], additionalProperties: true } }); var getRecordsValidationSchema = (contract, options) => { const { validateBody, validateAttributes } = options; if ((validateBody === void 0 || !validateBody) && (validateAttributes === void 0 || !validateAttributes)) { return void 0; } return getSchema(contract, options); }; var getRecordsValidator = (contract, options) => { const recordsValidationSchema = getRecordsValidationSchema(contract, options); return recordsValidationSchema !== void 0 ? options.ajv?.compile(recordsValidationSchema) : void 0; }; var getBodyValidator = (contract, options) => { if (options.validateMessage === false) { return; } return options.ajv.compile(contract.messageBodySchema); }; var getMessageAttributesValidator = (contract, options) => { if (options.validateMessage === false || Object.keys(contract.messageAttributesSchema).length === 0) { return; } return options.ajv.compile(contract.messageAttributesSchema); }; // src/contracts/SQS/utils/allRecordsHandler.ts var getAllRecordsHandler = (handleRecord, context, callback, additionalArgs) => async (event) => { const { records } = event; const handleRecordsResults = await Promise.allSettled( records.map( (record) => handleRecord(record, context, callback, ...additionalArgs) ) ); const recordsErrors = handleRecordsResults.map((recordResult, index) => ({ ...recordResult, record: records[index] })).filter( (record) => record.status === "rejected" ); recordsErrors.forEach(({ record, reason }) => { console.error( `Error during the processing of the message ${record.messageId}`, { record }, reason ); console.error( `Message ${record.messageId} will be marked as failed and be retried according to the SQS retry configuration` ); }); return { batchItemFailures: recordsErrors.map(({ record: { messageId } }) => ({ itemIdentifier: messageId })) }; }; // src/contracts/SQS/utils/serializeMessageAttributes.ts var serializeMessageAttribute = (attributeValue, attributeSchema) => { if (typeof attributeSchema === "boolean") { throw new Error("Invalid contract messageAttributesSchema"); } const { type } = attributeSchema; switch (type) { case "string": return { StringValue: attributeValue, DataType: "String" }; case "number": return { StringValue: attributeValue.toString(), DataType: "Number" }; default: throw new Error( "Invalid messageAttributesSchema. Only string and number are currently supported" ); } }; var serializeMessageAttributes = (messageAttributes, contract) => { const { properties } = contract.messageAttributesSchema; if (properties === void 0) { throw new Error("Invalid contract messageAttributesSchema"); } return Object.entries(properties).reduce( (acc, [attributeName, attributeSchema]) => { if (messageAttributes[attributeName] === void 0) { return acc; } return { ...acc, [attributeName]: serializeMessageAttribute( messageAttributes[attributeName], attributeSchema ) }; }, {} ); }; // src/contracts/SQS/utils/messagesValidation.ts var validateMessage = ({ contract, message, options, index }) => { const { body, messageAttributes } = message; const bodyValidator = getBodyValidator(contract, options); if (bodyValidator !== void 0 && !bodyValidator(body)) { console.error( `Error: Invalid message body ${index !== void 0 ? `at index ${index}` : ""}`, JSON.stringify(bodyValidator.errors, null, 2) ); throw new Error("Error: Invalid message body"); } if (messageAttributes === void 0) { return; } const messageAttributesValidator = getMessageAttributesValidator( contract, options ); if (messageAttributesValidator !== void 0 && !messageAttributesValidator(messageAttributes)) { console.error( `Error: Invalid message attributes ${index !== void 0 ? `at index ${index}` : ""}`, JSON.stringify(messageAttributesValidator.errors, null, 2) ); throw new Error("Error: Invalid message attributes"); } }; var validateMessages = ({ contract, messages, options }) => { const errors = []; messages.forEach((message, index) => { try { validateMessage({ contract, message, options, index }); } catch (error) { errors.push(error); } }); if (errors.length > 0) { throw new Error("Error: Invalid message"); } }; // src/contracts/SQS/utils/serializeMessage.ts var import_ulid = require("ulid"); var serializeMessage = ({ contract, bodySerializer }) => ({ body, messageAttributes, ...restMessage }) => ({ Id: (0, import_ulid.ulid)(), MessageBody: bodySerializer !== void 0 ? bodySerializer(body) : body, ...messageAttributes !== void 0 && messageAttributes !== null ? { MessageAttributes: serializeMessageAttributes( messageAttributes, // messageAttributes generic infered type has {} as type. I don't know why contract ) } : {}, ...restMessage }); // src/contracts/SQS/utils/delays.ts var wait = async (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms)); var getExponentialBackoffDelay = (retryCount, baseDelayMs = 1) => baseDelayMs * 10 ** retryCount; // src/contracts/SQS/features/lambdaHandler.ts var defaultOptions5 = { bodyParser: (body) => JSON.parse(body), validateBody: true, validateAttributes: true, handleBatchedRecords: true, logRawEvent: false }; var getSQSHandler = (contract, options) => (handler) => { const internalOptions = { ...defaultOptions5, ...options }; const recordsValidator = getRecordsValidator(contract, internalOptions); return async (event, context, callback, ...additionalArgs) => { if (internalOptions.logRawEvent) { console.debug("Raw event:", JSON.stringify(event, null, 2)); } const { Records } = event; if (Records === void 0) { throw new Error( "The provided event is not a valid SQS event. It has no Records attribute. Please use `logRawEvent` option to debug this" ); } const parsedRecords = Records.map( parseRecord(internalOptions) ); if (recordsValidator !== void 0 && !recordsValidator(parsedRecords)) { console.error("Error: Invalid records"); console.error(JSON.stringify(recordsValidator.errors, null, 2)); throw new Error("Invalid records"); } if (!internalOptions.handleBatchedRecords) { return handler({ records: parsedRecords }, context, callback, ...additionalArgs); } const allRecordsHandler = getAllRecordsHandler( // handleRecords is true, handler is SwarmionSQSHandler. Typescript is not able to infer this handler, context, callback, additionalArgs ); return allRecordsHandler({ records: parsedRecords }); }; }; // src/contracts/SQS/features/sendMessage.ts var import_client_sqs = require("@aws-sdk/client-sqs"); var import_omit = __toESM(require("lodash/omit.js"), 1); var defaultOptions6 = { validateMessage: true, bodySerializer: JSON.stringify }; var buildSendMessage = (contract, options) => async (message) => { const internalOptions = { ...defaultOptions6, ...options }; const { queueUrl, sqsClient, bodySerializer } = internalOptions; validateMessage({ contract, message, options: internalOptions }); const command = new import_client_sqs.SendMessageCommand({ QueueUrl: typeof queueUrl === "string" ? queueUrl : await queueUrl(), ...(0, import_omit.default)( serializeMessage({ contract, bodySerializer })(message), "Id" ) }); return sqsClient.send(command); }; // src/contracts/SQS/features/sendMessages.ts var import_client_sqs2 = require("@aws-sdk/client-sqs"); var import_p_queue2 = __toESM(require("p-queue"), 1); var PQueue2 = fixESMInteropIssue(import_p_queue2.default); var InfiniteThroughput = 0; var MAX_BATCH_SIZE2 = 256e3; var MAX_BATCH_LENGTH2 = 10; var defaultOptions7 = { validateMessage: true, bodySerializer: JSON.stringify, maxRetries: 3, baseDelay: 100, throwOnFailedBatch: true, throughputCallsPerSecond: InfiniteThroughput }; var getThroughputQueueConfig2 = (throughputCallsPerSecond) => throughputCallsPerSecond === InfiniteThroughput ? {} : { intervalCap: throughputCallsPerSecond, interval: 1e3 }; var computeMessageSize = (message) => Buffer.byteLength(JSON.stringify(message), "utf8"); var sendAllMessagesBatchedWithControlledThroughput = async (messages, options) => { const { sqsClient, queueUrl, throughputCallsPerSecond } = options; const queue = new PQueue2(getThroughputQueueConfig2(throughputCallsPerSecond)); const batches = chunkEntriesBatch({ entries: messages, computeEntrySize: computeMessageSize, maxBatchSize: MAX_BATCH_SIZE2, maxBatchLength: MAX_BATCH_LENGTH2 }); const results = await queue.addAll( batches.map( (batch) => async () => await sqsClient.send( new import_client_sqs2.SendMessageBatchCommand({ QueueUrl: queueUrl, Entries: batch }) ) ) ); return { failedItems: results.map(({ Failed }) => Failed ?? []).flat() }; }; var sendBatchedMessages = async ({ messages, options }) => { const { queueUrl: queueUrlOrGetter, baseDelay, maxRetries, throwOnFailedBatch } = options; const queueUrl = typeof queueUrlOrGetter === "string" ? queueUrlOrGetter : await queueUrlOrGetter(); let unprocessedMessages = messages; let failedItems = []; let attempts = 0; do { ({ failedItems } = await sendAllMessagesBatchedWithControlledThroughput( unprocessedMessages, { ...options, queueUrl } )); if (failedItems.length > 0) { const failedIds = failedItems.map(({ Id }) => Id); unprocessedMessages = messages.filter(({ Id }) => failedIds.includes(Id)); attempts++; console.warn( `Attempt ${attempts}: Failed to process ${failedItems.length} items. Retrying after delay...` ); if (attempts < maxRetries) { const delayDuration = getExponentialBackoffDelay( attempts - 1, baseDelay ); console.info(`Delaying for ${delayDuration} ms...`); await wait(delayDuration); } } } while (failedItems.length > 0 && attempts < maxRetries); if (failedItems.length > 0 && throwOnFailedBatch) { console.error("Failed items:", JSON.stringify(failedItems, null, 2)); throw new Error( `Failed to send ${failedItems.length} items to SQS after ${maxRetries} attempts` ); } return { failedItems }; }; var buildSendMessages = (contract, options) => async (messages) => { const internalOptions = { ...defaultOptions7, ...options }; const { bodySerializer } = internalOptions; validateMessages({ contract, messages, options: internalOptions }); return sendBatchedMessages({ messages: messages.map( serializeMessage({ contract, bodySerializer }) ), options: internalOptions }); }; // src/contracts/cloudFormation/features/fullContractSchema.ts var getFullContractSchema4 = (contract) => { return { type: "object", properties: { contractId: { const: contract.id }, contractType: { const: "cloudFormation" },