544 lines
24 KiB
TypeScript
544 lines
24 KiB
TypeScript
import {
|
|
CreateDeploymentCommand,
|
|
CreateResourceCommand,
|
|
CreateRestApiCommand,
|
|
DeleteMethodCommand,
|
|
DeleteRestApiCommand,
|
|
GetRestApiCommand,
|
|
GetRestApisCommand,
|
|
GetMethodCommand,
|
|
GetResourcesCommand,
|
|
GetStagesCommand,
|
|
PutIntegrationCommand,
|
|
PutIntegrationResponseCommand,
|
|
PutMethodCommand,
|
|
PutMethodResponseCommand,
|
|
TestInvokeMethodCommand,
|
|
type APIGatewayClient,
|
|
type Integration,
|
|
type IntegrationResponse,
|
|
type MethodResponse,
|
|
type Resource,
|
|
type RestApi,
|
|
} from '@aws-sdk/client-api-gateway'
|
|
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 {ApiGatewayCorsInput, ApiGatewayCorsResult, ApiGatewayDeleteRouteInput, ApiGatewayLambdaRouteInput, ApiGatewayLambdaRouteResult, ApiGatewayMethodTestInput, ApiGatewayMethodTestResult, ApiGatewayRouteOverview, ApiGatewayRoutesResult, ApiGatewayTestRouteInput, ApiGatewayTestRouteResult, 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 method = input.method.toUpperCase()
|
|
const stage = stageName(input.stage)
|
|
const functionName = stringValue(input.functionName)
|
|
if (!functionName) throw new ValidationError('functionName is required')
|
|
if (!CONFIGURABLE_METHODS.includes(method)) {
|
|
throw new ValidationError('method must be GET, POST, PUT, DELETE, PATCH, ANY, or HEAD')
|
|
}
|
|
|
|
const resource = await this.resolveRouteResource(apiId, input)
|
|
const resourceId = resource.id
|
|
if (!resourceId) throw new ValidationError('API Gateway did not return a resource id')
|
|
const resourcePath = resource.path ?? routePath(input.path)
|
|
|
|
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`,
|
|
}))
|
|
|
|
const permission = {
|
|
FunctionName: functionName,
|
|
StatementId: apiGatewayPermissionStatementId(apiId, stage, method, resourcePath),
|
|
Action: 'lambda:InvokeFunction',
|
|
Principal: 'apigateway.amazonaws.com',
|
|
SourceArn: apiGatewaySourceArn(this.region, this.accountId, apiId, stage, method, resourcePath),
|
|
}
|
|
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,
|
|
stageName: stage,
|
|
description: `Lambda proxy route ${method} ${resourcePath}`,
|
|
}))
|
|
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}${resourcePath}`,
|
|
}
|
|
}
|
|
|
|
async testApiGatewayRoute(apiId: string, input: ApiGatewayTestRouteInput): Promise<ApiGatewayTestRouteResult> {
|
|
const path = testRoutePath(input.path)
|
|
const method = input.method.toUpperCase()
|
|
const stage = stageName(input.stage)
|
|
if (!DEPLOYED_HTTP_METHODS.includes(method)) {
|
|
throw new ValidationError('method must be GET, POST, PUT, DELETE, PATCH, or HEAD')
|
|
}
|
|
|
|
const encodedPath = path.split('/').map(encodeURIComponent).join('/')
|
|
const url = new URL(`${awsEndpoint().replace(/\/$/, '')}/execute-api/${encodeURIComponent(apiId)}/${encodeURIComponent(stage)}${encodedPath}`)
|
|
url.search = queryString(input.query)
|
|
const headers = new Headers(input.headers)
|
|
if (input.body && !headers.has('content-type')) headers.set('content-type', 'application/json')
|
|
|
|
const startedAt = performance.now()
|
|
const response = await globalThis.fetch(url, {
|
|
method,
|
|
headers,
|
|
body: ['GET', 'HEAD', 'OPTIONS'].includes(method) ? undefined : input.body || undefined,
|
|
})
|
|
return {
|
|
statusCode: response.status,
|
|
headers: Object.fromEntries(response.headers.entries()),
|
|
body: await response.text(),
|
|
durationMs: Math.round(performance.now() - startedAt),
|
|
}
|
|
}
|
|
|
|
async testApiGatewayMethod(apiId: string, input: ApiGatewayMethodTestInput): Promise<ApiGatewayMethodTestResult> {
|
|
const resourceId = stringValue(input.resourceId)
|
|
const httpMethod = input.httpMethod.toUpperCase()
|
|
const pathWithQueryString = stringValue(input.pathWithQueryString)
|
|
if (!resourceId) throw new ValidationError('resourceId is required')
|
|
if (!TEST_INVOKE_METHODS.includes(httpMethod)) {
|
|
throw new ValidationError('httpMethod must be a configured API Gateway method')
|
|
}
|
|
if (!pathWithQueryString.startsWith('/')) {
|
|
throw new ValidationError('pathWithQueryString must start with /')
|
|
}
|
|
|
|
const result = await this.apiGateway.send(new TestInvokeMethodCommand({
|
|
restApiId: apiId,
|
|
resourceId,
|
|
httpMethod,
|
|
pathWithQueryString,
|
|
body: input.body,
|
|
headers: input.headers,
|
|
multiValueHeaders: input.multiValueHeaders,
|
|
stageVariables: input.stageVariables,
|
|
}))
|
|
return {
|
|
statusCode: result.status ?? 0,
|
|
body: result.body ?? '',
|
|
headers: result.headers ?? {},
|
|
multiValueHeaders: result.multiValueHeaders ?? {},
|
|
log: result.log ?? null,
|
|
latency: result.latency ?? null,
|
|
}
|
|
}
|
|
|
|
async listApiGatewayRoutes(apiId: string): Promise<ApiGatewayRoutesResult> {
|
|
const resources = await this.resources(apiId, true)
|
|
|
|
const routes: ApiGatewayRouteOverview[] = []
|
|
for (const resource of resources) {
|
|
const methods = resource.resourceMethods
|
|
? Object.keys(resource.resourceMethods).sort()
|
|
: []
|
|
for (const method of methods) {
|
|
if (!resource.id || !resource.path) continue
|
|
let result
|
|
try {
|
|
result = await this.apiGateway.send(new GetMethodCommand({
|
|
restApiId: apiId,
|
|
resourceId: resource.id,
|
|
httpMethod: method,
|
|
}))
|
|
} catch (error) {
|
|
if (hasHttpStatus(error, 404)) continue
|
|
throw error
|
|
}
|
|
routes.push({
|
|
resourceId: resource.id,
|
|
path: resource.path,
|
|
method,
|
|
functionName: lambdaFunctionName(result.methodIntegration?.uri),
|
|
integrationType: result.methodIntegration?.type ?? null,
|
|
})
|
|
}
|
|
}
|
|
|
|
const stages = await this.apiGateway.send(new GetStagesCommand({restApiId: apiId}))
|
|
const resourcesOverview = resources
|
|
.filter((resource): resource is Resource & {id: string; path: string} => Boolean(resource.id && resource.path))
|
|
.map((resource) => ({
|
|
resourceId: resource.id,
|
|
parentId: resource.parentId ?? null,
|
|
path: resource.path,
|
|
pathPart: resource.pathPart ?? null,
|
|
methods: routes.filter((route) => route.resourceId === resource.id),
|
|
}))
|
|
.sort((left, right) => left.path.localeCompare(right.path))
|
|
return {
|
|
resources: resourcesOverview,
|
|
routes: routes.sort((left, right) => left.path.localeCompare(right.path) || left.method.localeCompare(right.method)),
|
|
stages: (stages.item ?? []).map((stage) => stage.stageName).filter((stage): stage is string => Boolean(stage)).sort(),
|
|
}
|
|
}
|
|
|
|
async deleteApiGatewayRoute(apiId: string, input: ApiGatewayDeleteRouteInput): Promise<void> {
|
|
const method = input.method.toUpperCase()
|
|
const stage = stageName(input.stage)
|
|
if (!DELETABLE_METHODS.includes(method)) throw new ValidationError('method is not supported')
|
|
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`,
|
|
}))
|
|
}
|
|
|
|
async enableApiGatewayCors(apiId: string, input: ApiGatewayCorsInput): Promise<ApiGatewayCorsResult> {
|
|
const resourceId = stringValue(input.resourceId)
|
|
const stage = stageName(input.stage)
|
|
const origin = stringValue(input.origin)
|
|
const headers = stringValue(input.headers)
|
|
const methods = [...new Set(input.methods.map((method) => method.toUpperCase()))]
|
|
if (!resourceId) throw new ValidationError('resourceId is required')
|
|
if (!origin) throw new ValidationError('origin is required')
|
|
if (!headers) throw new ValidationError('headers are required')
|
|
if (methods.length === 0 || methods.some((method) => !CORS_METHODS.includes(method))) {
|
|
throw new ValidationError('methods must contain one or more supported HTTP methods')
|
|
}
|
|
|
|
const corsParameters = corsResponseParameters(origin, headers, methods)
|
|
const configuredMethods: string[] = []
|
|
const proxyMethods: string[] = []
|
|
for (const method of methods) {
|
|
if (method === 'OPTIONS') continue
|
|
const route = await this.apiGateway.send(new GetMethodCommand({restApiId: apiId, resourceId, httpMethod: method}))
|
|
if (route.methodIntegration?.type === 'AWS_PROXY') {
|
|
proxyMethods.push(method)
|
|
continue
|
|
}
|
|
const responseCodes = Object.keys(route.methodResponses ?? {200: {}})
|
|
for (const statusCode of responseCodes) {
|
|
await this.apiGateway.send(new PutMethodResponseCommand({
|
|
restApiId: apiId, resourceId, httpMethod: method, statusCode,
|
|
responseParameters: {
|
|
...route.methodResponses?.[statusCode]?.responseParameters,
|
|
...corsMethodResponseParameters(),
|
|
},
|
|
responseModels: route.methodResponses?.[statusCode]?.responseModels,
|
|
}))
|
|
}
|
|
const integrationResponses = route.methodIntegration?.integrationResponses ?? {200: {}}
|
|
for (const [statusCode, response] of Object.entries(integrationResponses)) {
|
|
await this.apiGateway.send(new PutIntegrationResponseCommand({
|
|
restApiId: apiId, resourceId, httpMethod: method, statusCode,
|
|
selectionPattern: response.selectionPattern,
|
|
responseParameters: {...response.responseParameters, ...corsParameters},
|
|
responseTemplates: response.responseTemplates,
|
|
contentHandling: response.contentHandling,
|
|
}))
|
|
}
|
|
configuredMethods.push(method)
|
|
}
|
|
|
|
let existingOptionsMethodResponse: MethodResponse | undefined
|
|
let existingOptionsIntegration: Integration | undefined
|
|
let existingOptionsIntegrationResponse: IntegrationResponse | undefined
|
|
try {
|
|
const optionsRoute = await this.apiGateway.send(new GetMethodCommand({
|
|
restApiId: apiId, resourceId, httpMethod: 'OPTIONS',
|
|
}))
|
|
existingOptionsMethodResponse = optionsRoute.methodResponses?.['200']
|
|
existingOptionsIntegration = optionsRoute.methodIntegration
|
|
existingOptionsIntegrationResponse = optionsRoute.methodIntegration?.integrationResponses?.['200']
|
|
} catch (error) {
|
|
if (!hasHttpStatus(error, 404)) throw error
|
|
}
|
|
await this.apiGateway.send(new PutMethodCommand({restApiId: apiId, resourceId, httpMethod: 'OPTIONS', authorizationType: 'NONE'}))
|
|
await this.apiGateway.send(new PutMethodResponseCommand({
|
|
restApiId: apiId, resourceId, httpMethod: 'OPTIONS', statusCode: '200',
|
|
responseParameters: {
|
|
...corsMethodResponseParameters(),
|
|
...existingOptionsMethodResponse?.responseParameters,
|
|
},
|
|
responseModels: existingOptionsMethodResponse?.responseModels,
|
|
}))
|
|
await this.apiGateway.send(new PutIntegrationCommand({
|
|
restApiId: apiId, resourceId, httpMethod: 'OPTIONS', type: 'MOCK',
|
|
requestTemplates: {'application/json': '{"statusCode": 200}'},
|
|
timeoutInMillis: existingOptionsIntegration?.timeoutInMillis,
|
|
}))
|
|
await this.apiGateway.send(new PutIntegrationResponseCommand({
|
|
restApiId: apiId, resourceId, httpMethod: 'OPTIONS', statusCode: '200',
|
|
responseParameters: corsParameters,
|
|
responseTemplates: existingOptionsIntegrationResponse?.responseTemplates,
|
|
selectionPattern: existingOptionsIntegrationResponse?.selectionPattern,
|
|
contentHandling: existingOptionsIntegrationResponse?.contentHandling,
|
|
}))
|
|
await this.apiGateway.send(new CreateDeploymentCommand({
|
|
restApiId: apiId, stageName: stage, description: `Enable CORS for resource ${resourceId}`,
|
|
}))
|
|
return {configuredMethods, proxyMethods}
|
|
}
|
|
|
|
private async resources(apiId: string, embedMethods = false): Promise<Resource[]> {
|
|
const resources: Resource[] = []
|
|
let position: string | undefined
|
|
do {
|
|
const result = await this.apiGateway.send(new GetResourcesCommand({
|
|
restApiId: apiId,
|
|
position,
|
|
embed: embedMethods ? ['methods'] : undefined,
|
|
}))
|
|
resources.push(...(result.items ?? []))
|
|
position = result.position
|
|
} while (position)
|
|
return resources
|
|
}
|
|
|
|
private async resolveRouteResource(apiId: string, input: ApiGatewayLambdaRouteInput): Promise<Resource> {
|
|
const resources = await this.resources(apiId)
|
|
if (input.parentResourceId || input.pathPart) {
|
|
const parentId = stringValue(input.parentResourceId)
|
|
const pathPart = routePathPart(input.pathPart ?? '')
|
|
const parent = resources.find((resource) => resource.id === parentId)
|
|
if (!parent?.id || !parent.path) throw new ValidationError('parentResourceId must identify an existing API Gateway resource')
|
|
const existing = resources.find((resource) => resource.parentId === parent.id && resource.pathPart === pathPart)
|
|
if (existing?.id) return existing
|
|
const created = await this.apiGateway.send(new CreateResourceCommand({restApiId: apiId, parentId: parent.id, pathPart}))
|
|
return {...created, pathPart, parentId: parent.id, path: childPath(parent.path, pathPart)}
|
|
}
|
|
|
|
const parts = routePath(input.path).slice(1).split('/')
|
|
const root = resources.find((resource) => resource.path === '/')
|
|
if (!root?.id) throw new ValidationError('API Gateway root resource was not found')
|
|
let parent: Resource = root
|
|
for (const pathPart of parts) {
|
|
const existing = resources.find((resource) => resource.parentId === parent.id && resource.pathPart === pathPart)
|
|
if (existing?.id) {
|
|
parent = existing
|
|
continue
|
|
}
|
|
const created: Resource = await this.apiGateway.send(new CreateResourceCommand({restApiId: apiId, parentId: parent.id, pathPart}))
|
|
parent = {...created, pathPart, parentId: parent.id, path: childPath(parent.path ?? '/', pathPart)}
|
|
resources.push(parent)
|
|
}
|
|
return parent
|
|
}
|
|
}
|
|
|
|
const CONFIGURABLE_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'ANY', 'HEAD']
|
|
const DEPLOYED_HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']
|
|
const TEST_INVOKE_METHODS = [...CONFIGURABLE_METHODS, 'OPTIONS']
|
|
const CORS_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']
|
|
const DELETABLE_METHODS = [...CONFIGURABLE_METHODS, 'OPTIONS']
|
|
|
|
function corsMethodResponseParameters(): Record<string, boolean> {
|
|
return {
|
|
'method.response.header.Access-Control-Allow-Origin': false,
|
|
'method.response.header.Access-Control-Allow-Headers': false,
|
|
'method.response.header.Access-Control-Allow-Methods': false,
|
|
}
|
|
}
|
|
|
|
function corsResponseParameters(origin: string, headers: string, methods: string[]): Record<string, string> {
|
|
const requestedMethods = new Set([...methods, 'OPTIONS'])
|
|
const allowedMethods = CORS_METHODS.filter((method) => requestedMethods.has(method))
|
|
return {
|
|
'method.response.header.Access-Control-Allow-Origin': `'${origin}'`,
|
|
'method.response.header.Access-Control-Allow-Headers': `'${headers}'`,
|
|
'method.response.header.Access-Control-Allow-Methods': `'${allowedMethods.join(',')}'`,
|
|
}
|
|
}
|
|
|
|
function routePathPart(path: string): string {
|
|
const value = stringValue(path)
|
|
if (!/^(?:[A-Za-z0-9._-]+|\{[A-Za-z0-9._-]+\+?})$/.test(value)) {
|
|
throw new ValidationError('pathPart must be one API Gateway segment, for example orders, {id}, or {proxy+}')
|
|
}
|
|
return value
|
|
}
|
|
|
|
function routePath(path: string): string {
|
|
const value = stringValue(path)
|
|
if (!value.startsWith('/') || value === '/' || value.endsWith('/')) {
|
|
throw new ValidationError('path must start with / and contain at least one resource segment')
|
|
}
|
|
for (const part of value.slice(1).split('/')) routePathPart(part)
|
|
return value
|
|
}
|
|
|
|
function childPath(parentPath: string, pathPart: string): string {
|
|
return parentPath === '/' ? `/${pathPart}` : `${parentPath}/${pathPart}`
|
|
}
|
|
|
|
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 queryString(query: string): string {
|
|
const value = stringValue(query)
|
|
if (value.length > 8_192) throw new ValidationError('query must be at most 8192 characters')
|
|
return value.replace(/^\?/, '')
|
|
}
|
|
|
|
function testRoutePath(path: string): string {
|
|
const value = stringValue(path)
|
|
if (!/^\/(?:[A-Za-z0-9._~{}+%-]+\/)*[A-Za-z0-9._~{}+%-]+$/.test(value)) {
|
|
throw new ValidationError('path must be an API Gateway route path, for example /orders or /users/{id}')
|
|
}
|
|
return value
|
|
}
|
|
|
|
function lambdaFunctionName(uri?: string): string | null {
|
|
const match = uri?.match(/\/functions\/arn:aws:lambda:[^:]+:\d+:function:([^/:]+)/)
|
|
return match?.[1] ?? null
|
|
}
|
|
|
|
function apiGatewayPermissionStatementId(apiId: string, stage: string, method: string, path: string): string {
|
|
return `apigateway-${apiId}-${stage}-${method}-${path.replace(/[^A-Za-z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')}`.slice(0, 100)
|
|
}
|
|
|
|
function apiGatewaySourceArn(region: string, accountId: string, apiId: string, stage: string, method: string, path: string): string {
|
|
const arnMethod = method === 'ANY' ? '*' : method
|
|
const arnPath = path.slice(1).split('/').map((part) => part.startsWith('{') ? '*' : part).join('/')
|
|
return `arn:aws:execute-api:${region}:${accountId}:${apiId}/${stage}/${arnMethod}/${arnPath}`
|
|
}
|
|
|
|
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
|
|
}
|