feat: add SQS + EventBridge

This commit is contained in:
Zakaria
2026-09-24 22:34:04 -04:00
parent 9bbe80b5f2
commit 1b129e9dcf
37 changed files with 2902 additions and 66 deletions
@@ -2,6 +2,7 @@ import {
CreateDeploymentCommand,
CreateResourceCommand,
CreateRestApiCommand,
DeleteMethodCommand,
DeleteRestApiCommand,
GetRestApiCommand,
GetRestApisCommand,
@@ -13,12 +14,12 @@ import {
type APIGatewayClient,
type RestApi,
} from '@aws-sdk/client-api-gateway'
import {AddPermissionCommand, GetFunctionCommand, type LambdaClient} from '@aws-sdk/client-lambda'
import {AddPermissionCommand, GetFunctionCommand, RemovePermissionCommand, type LambdaClient} from '@aws-sdk/client-lambda'
import {apiGateway as defaultApiGateway, defaultAccountId, lambda as defaultLambda} from '../aws'
import {awsApiGatewaySchema} from '../cloud-spi/apigatewaySchema'
import {ValidationError} from '../cloud-spi/errors'
import {awsEndpoint} from '../service/runtimeProbe'
import type {ApiGatewayLambdaRouteInput, ApiGatewayLambdaRouteResult, ApiGatewayRouteOverview, ApiGatewayRoutesResult, ApiGatewayTestRouteInput, ApiGatewayTestRouteResult, CloudResource, CloudServiceAdapter, CreateResourceInput, ResourceQuery, ServiceSchema} from '../cloud-spi/types'
import type {ApiGatewayDeleteRouteInput, ApiGatewayLambdaRouteInput, ApiGatewayLambdaRouteResult, ApiGatewayRouteOverview, ApiGatewayRoutesResult, ApiGatewayTestRouteInput, ApiGatewayTestRouteResult, CloudResource, CloudServiceAdapter, CreateResourceInput, ResourceQuery, ServiceSchema} from '../cloud-spi/types'
export class AwsApiGatewayAdapter implements CloudServiceAdapter {
readonly cloud = 'aws' as const
@@ -84,12 +85,7 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter {
throw new ValidationError('method must be GET, POST, PUT, or DELETE')
}
const root = await this.rootResource(apiId)
const resource = await this.apiGateway.send(new CreateResourceCommand({
restApiId: apiId,
parentId: root.id,
pathPart,
}))
const resource = await this.resourceForPath(apiId, `/${pathPart}`) ?? await this.createTopLevelResource(apiId, pathPart)
const resourceId = resource.id
if (!resourceId) throw new ValidationError('API Gateway did not return a resource id')
@@ -112,13 +108,23 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter {
uri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${functionArn}/invocations`,
}))
await this.lambda.send(new AddPermissionCommand({
const permission = {
FunctionName: functionName,
StatementId: `apigateway-${apiId}-${stage}-${method}-${pathPart}`,
StatementId: apiGatewayPermissionStatementId(apiId, stage, method, `/${pathPart}`),
Action: 'lambda:InvokeFunction',
Principal: 'apigateway.amazonaws.com',
SourceArn: `arn:aws:execute-api:${this.region}:${this.accountId}:${apiId}/${stage}/${method}/${pathPart}`,
}))
}
try {
await this.lambda.send(new AddPermissionCommand(permission))
} catch (error) {
if (!hasHttpStatus(error, 409)) throw error
await this.lambda.send(new RemovePermissionCommand({
FunctionName: functionName,
StatementId: permission.StatementId,
}))
await this.lambda.send(new AddPermissionCommand(permission))
}
const deployment = await this.apiGateway.send(new CreateDeploymentCommand({
restApiId: apiId,
@@ -205,6 +211,40 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter {
}
}
async deleteApiGatewayRoute(apiId: string, input: ApiGatewayDeleteRouteInput): Promise<void> {
const method = input.method.toUpperCase()
const stage = stageName(input.stage)
if (!['GET', 'POST', 'PUT', 'DELETE'].includes(method)) throw new ValidationError('method must be GET, POST, PUT, or DELETE')
if (!stringValue(input.resourceId)) throw new ValidationError('resourceId is required')
const route = await this.apiGateway.send(new GetMethodCommand({
restApiId: apiId,
resourceId: input.resourceId,
httpMethod: method,
}))
await this.apiGateway.send(new DeleteMethodCommand({
restApiId: apiId,
resourceId: input.resourceId,
httpMethod: method,
}))
const functionName = lambdaFunctionName(route.methodIntegration?.uri)
if (functionName) {
try {
await this.lambda.send(new RemovePermissionCommand({
FunctionName: functionName,
StatementId: apiGatewayPermissionStatementId(apiId, stage, method, input.path),
}))
} catch (error) {
if (!hasHttpStatus(error, 404)) throw error
}
}
await this.apiGateway.send(new CreateDeploymentCommand({
restApiId: apiId,
stageName: stage,
description: `Delete ${method} method`,
}))
}
private async rootResource(apiId: string) {
let position: string | undefined
do {
@@ -215,6 +255,26 @@ export class AwsApiGatewayAdapter implements CloudServiceAdapter {
} while (position)
throw new ValidationError('API Gateway root resource was not found')
}
private async resourceForPath(apiId: string, path: string) {
let position: string | undefined
do {
const result = await this.apiGateway.send(new GetResourcesCommand({restApiId: apiId, position}))
const resource = result.items?.find((item) => item.path === path)
if (resource?.id) return resource
position = result.position
} while (position)
return undefined
}
private async createTopLevelResource(apiId: string, pathPart: string) {
const root = await this.rootResource(apiId)
return this.apiGateway.send(new CreateResourceCommand({
restApiId: apiId,
parentId: root.id,
pathPart,
}))
}
}
function routePathPart(path: string): string {
@@ -252,6 +312,10 @@ function lambdaFunctionName(uri?: string): string | null {
return match?.[1] ?? null
}
function apiGatewayPermissionStatementId(apiId: string, stage: string, method: string, path: string): string {
return `apigateway-${apiId}-${stage}-${method}-${routePathPart(path)}`
}
function toResource(api: RestApi): CloudResource {
const id = api.id ?? ''
return {