first commit

This commit is contained in:
Zakaria
2026-09-22 16:13:21 -04:00
commit 7e49be27f1
297 changed files with 63162 additions and 0 deletions
@@ -0,0 +1,198 @@
import {
CreateDeploymentCommand,
CreateResourceCommand,
CreateRestApiCommand,
DeleteRestApiCommand,
GetRestApiCommand,
GetRestApisCommand,
GetResourcesCommand,
PutIntegrationCommand,
PutMethodCommand,
type APIGatewayClient,
type RestApi,
} from '@aws-sdk/client-api-gateway'
import {AddPermissionCommand, GetFunctionCommand, 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, CloudResource, CloudServiceAdapter, CreateResourceInput, ResourceQuery, ServiceSchema} from '../cloud-spi/types'
export class AwsApiGatewayAdapter implements CloudServiceAdapter {
readonly cloud = 'aws' as const
readonly service = 'apigateway' as const
constructor(
private readonly apiGateway: APIGatewayClient = defaultApiGateway,
private readonly lambda: LambdaClient = defaultLambda,
private readonly accountId = defaultAccountId(),
private readonly region = process.env.AWS_REGION ?? 'us-east-1',
) {}
schema(): ServiceSchema {
return awsApiGatewaySchema()
}
async list(query: ResourceQuery = {}): Promise<CloudResource[]> {
const apis: RestApi[] = []
let position: string | undefined
do {
const res = await this.apiGateway.send(new GetRestApisCommand({position}))
apis.push(...(res.items ?? []))
position = res.position
} while (position)
return filterBySearch(apis.map(toResource), query.search)
}
async get(id: string): Promise<CloudResource | null> {
try {
const res = await this.apiGateway.send(new GetRestApiCommand({restApiId: id}))
return toResource(res)
} catch (error) {
if (hasHttpStatus(error, 404)) return null
throw error
}
}
async create(input: CreateResourceInput): Promise<CloudResource> {
const name = stringValue(input.values.name)
const description = stringValue(input.values.description)
if (!name) throw new ValidationError('name is required')
const res = await this.apiGateway.send(new CreateRestApiCommand({
name,
description: description || undefined,
}))
return toResource(res)
}
async delete(id: string): Promise<void> {
await this.apiGateway.send(new DeleteRestApiCommand({restApiId: id}))
}
async createApiGatewayLambdaRoute(apiId: string, input: ApiGatewayLambdaRouteInput): Promise<ApiGatewayLambdaRouteResult> {
const pathPart = routePathPart(input.path)
const method = input.method.toUpperCase()
const stage = stageName(input.stage)
const functionName = stringValue(input.functionName)
if (!functionName) throw new ValidationError('functionName is required')
if (!['GET', 'POST', 'PUT', 'DELETE'].includes(method)) {
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 resourceId = resource.id
if (!resourceId) throw new ValidationError('API Gateway did not return a resource id')
await this.apiGateway.send(new PutMethodCommand({
restApiId: apiId,
resourceId,
httpMethod: method,
authorizationType: 'NONE',
}))
const functionResult = await this.lambda.send(new GetFunctionCommand({FunctionName: functionName}))
const functionArn = functionResult.Configuration?.FunctionArn
if (!functionArn) throw new ValidationError(`Lambda function ${functionName} did not return an ARN`)
await this.apiGateway.send(new PutIntegrationCommand({
restApiId: apiId,
resourceId,
httpMethod: method,
type: 'AWS_PROXY',
integrationHttpMethod: 'POST',
uri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${functionArn}/invocations`,
}))
await this.lambda.send(new AddPermissionCommand({
FunctionName: functionName,
StatementId: `apigateway-${apiId}-${stage}-${method}-${pathPart}`,
Action: 'lambda:InvokeFunction',
Principal: 'apigateway.amazonaws.com',
SourceArn: `arn:aws:execute-api:${this.region}:${this.accountId}:${apiId}/${stage}/${method}/${pathPart}`,
}))
const deployment = await this.apiGateway.send(new CreateDeploymentCommand({
restApiId: apiId,
stageName: stage,
description: `Lambda proxy route ${method} /${pathPart}`,
}))
if (!deployment.id) throw new ValidationError('API Gateway did not return a deployment id')
return {
resourceId,
deploymentId: deployment.id,
invokeUrl: `${awsEndpoint().replace(/\/$/, '')}/execute-api/${apiId}/${stage}/${pathPart}`,
}
}
private async rootResource(apiId: string) {
let position: string | undefined
do {
const result = await this.apiGateway.send(new GetResourcesCommand({restApiId: apiId, position}))
const root = result.items?.find((resource) => resource.path === '/')
if (root?.id) return root
position = result.position
} while (position)
throw new ValidationError('API Gateway root resource was not found')
}
}
function routePathPart(path: string): string {
const value = stringValue(path).replace(/^\//, '')
if (!/^[A-Za-z0-9._~-]+$/.test(value)) {
throw new ValidationError('path must be one URL segment, for example /orders')
}
return value
}
function stageName(stage: string): string {
const value = stringValue(stage)
if (!/^[A-Za-z0-9_-]+$/.test(value)) {
throw new ValidationError('stage must contain only letters, numbers, underscores, or hyphens')
}
return value
}
function toResource(api: RestApi): CloudResource {
const id = api.id ?? ''
return {
id,
name: api.name ?? '',
cloud: 'aws',
service: 'apigateway',
type: 'rest-api',
region: null,
createdAt: api.createdDate?.toISOString() ?? null,
metadata: {
provider: 'aws',
apigatewayService: 'rest',
description: api.description,
version: api.version,
endpointTypes: api.endpointConfiguration?.types,
rootResourceId: api.rootResourceId,
},
}
}
function stringValue(value: unknown): string {
return typeof value === 'string' ? value.trim() : ''
}
function filterBySearch(resources: CloudResource[], search?: string): CloudResource[] {
const normalized = search?.trim().toLowerCase()
if (!normalized) return resources
return resources.filter((resource) => resource.name.toLowerCase().includes(normalized))
}
function hasHttpStatus(error: unknown, status: number): boolean {
if (typeof error !== 'object' || error === null) return false
const metadata = (error as {$metadata?: {httpStatusCode?: number}}).$metadata
return metadata?.httpStatusCode === status
}