Skip to content

Commit 8fcce51

Browse files
fix(aws): align cloudwatch, cloudformation, athena, codepipeline with live API docs (#5483)
* fix(aws): align cloudwatch, cloudformation, athena, codepipeline with live API docs - cloudwatch: fix list_metrics pagination (wasn't draining pages past 500), add MaxRecords cap validation to describe_alarms; add describe_alarm_history, filter_log_events, put_log_group_retention - cloudformation: fix get_template missing TemplateStage param, fix invalid ModuleTag in block metadata; add full stack lifecycle tools (create/update/delete/cancel_update_stack, create/describe/execute_change_set, get_template_summary) - athena: fix missing .trim() on query/named-query ID fields; add delete_named_query, batch_get_query_execution, list_databases, list_table_metadata - codepipeline: fix missing rollbackMetadata field in list_pipeline_executions response; add get_pipeline, list_action_executions, disable/enable_stage_transition * fix(aws): require template on cloudformation change-sets, bump route-count baseline - cloudformation create_change_set now rejects requests missing both templateBody and usePreviousTemplate, matching update_stack (Cursor Bugbot finding) - bump check:api-validation route-count baseline 906->917 to reflect the 19 new fully contract-bound routes added in this PR (0 boundary violations) * fix(aws): address Greptile round-1 review findings - cloudwatch describe_alarm_history: always request both MetricAlarm and CompositeAlarm types, even when alarmName is provided (was silently returning empty history for composite alarms queried by name) - cloudformation: add validateAwsRegion refinement to region field on the 7 new write-path contracts (update/delete/cancel-update-stack, create/describe/execute-change-set, get-template-summary), matching the pattern already used elsewhere - cloudformation: destroy the AWS SDK client in a finally block on the same 7 new write routes, matching the pattern used by every other new route in this PR * fix(aws/cloudformation): add missing region validation and client cleanup to create-stack - Add validateAwsRegion refinement to create-stack contract (completes P2 fix from Greptile review) - Wrap AWS SDK call in try/finally with client.destroy() (completes P2 fix from Greptile review) - Aligns create-stack with the pattern used across all 7 other new CloudFormation routes Co-authored-by: Waleed <waleedlatif1@users.noreply.github.com> * fix(aws): final validation pass — bound missing limits, close consistency gaps - cloudformation create_stack: add missing validateAwsRegion refine and client.destroy() finally block, matching sibling write routes - cloudwatch get_metric_statistics: cap statistics array at AWS's 5-item limit - cloudwatch get_log_events: cap limit at AWS's 10,000-record max - athena batch_get_query_execution: surface engineExecutionTimeInMillis/queryPlanningTimeInMillis/queryQueueTimeInMillis, matching the sibling get_query_execution tool's Statistics mapping * fix(aws/athena): destroy AWS SDK client on the 4 new athena routes Cursor Bugbot finding: batch_get_query_execution, delete_named_query, list_databases, and list_table_metadata created an AthenaClient but never called client.destroy(), unlike every other new route in this PR. Wrapped each in try/finally to match. * fix(aws/athena): add validateAwsRegion refinement to the 4 new contracts Greptile finding: delete_named_query, batch_get_query_execution, list_databases, and list_table_metadata accepted any non-empty string for region, unlike every other new contract in this PR. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Waleed <waleedlatif1@users.noreply.github.com>
1 parent d32b966 commit 8fcce51

87 files changed

Lines changed: 6531 additions & 50 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { BatchGetQueryExecutionCommand } from '@aws-sdk/client-athena'
2+
import { createLogger } from '@sim/logger'
3+
import { getErrorMessage } from '@sim/utils/errors'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { awsAthenaBatchGetQueryExecutionContract } from '@/lib/api/contracts/tools/aws/athena-batch-get-query-execution'
6+
import { parseToolRequest } from '@/lib/api/server'
7+
import { checkInternalAuth } from '@/lib/auth/hybrid'
8+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9+
import { createAthenaClient } from '@/app/api/tools/athena/utils'
10+
11+
const logger = createLogger('AthenaBatchGetQueryExecution')
12+
13+
export const POST = withRouteHandler(async (request: NextRequest) => {
14+
try {
15+
const auth = await checkInternalAuth(request)
16+
if (!auth.success || !auth.userId) {
17+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
18+
}
19+
20+
const parsed = await parseToolRequest(awsAthenaBatchGetQueryExecutionContract, request, {
21+
errorFormat: 'details',
22+
logger,
23+
})
24+
if (!parsed.success) return parsed.response
25+
const data = parsed.data.body
26+
27+
const client = createAthenaClient({
28+
region: data.region,
29+
accessKeyId: data.accessKeyId,
30+
secretAccessKey: data.secretAccessKey,
31+
})
32+
33+
try {
34+
const command = new BatchGetQueryExecutionCommand({
35+
QueryExecutionIds: data.queryExecutionIds,
36+
})
37+
38+
const response = await client.send(command)
39+
40+
return NextResponse.json({
41+
success: true,
42+
output: {
43+
queryExecutions: (response.QueryExecutions ?? []).map((execution) => ({
44+
queryExecutionId: execution.QueryExecutionId ?? '',
45+
query: execution.Query ?? null,
46+
state: execution.Status?.State ?? null,
47+
stateChangeReason: execution.Status?.StateChangeReason ?? null,
48+
statementType: execution.StatementType ?? null,
49+
database: execution.QueryExecutionContext?.Database ?? null,
50+
catalog: execution.QueryExecutionContext?.Catalog ?? null,
51+
workGroup: execution.WorkGroup ?? null,
52+
submissionDateTime: execution.Status?.SubmissionDateTime?.getTime() ?? null,
53+
completionDateTime: execution.Status?.CompletionDateTime?.getTime() ?? null,
54+
dataScannedInBytes: execution.Statistics?.DataScannedInBytes ?? null,
55+
engineExecutionTimeInMillis: execution.Statistics?.EngineExecutionTimeInMillis ?? null,
56+
queryPlanningTimeInMillis: execution.Statistics?.QueryPlanningTimeInMillis ?? null,
57+
queryQueueTimeInMillis: execution.Statistics?.QueryQueueTimeInMillis ?? null,
58+
totalExecutionTimeInMillis: execution.Statistics?.TotalExecutionTimeInMillis ?? null,
59+
outputLocation: execution.ResultConfiguration?.OutputLocation ?? null,
60+
})),
61+
unprocessedQueryExecutionIds: (response.UnprocessedQueryExecutionIds ?? []).map(
62+
(item) => ({
63+
queryExecutionId: item.QueryExecutionId ?? null,
64+
errorCode: item.ErrorCode ?? null,
65+
errorMessage: item.ErrorMessage ?? null,
66+
})
67+
),
68+
},
69+
})
70+
} finally {
71+
client.destroy()
72+
}
73+
} catch (error) {
74+
const errorMessage = getErrorMessage(error, 'Failed to batch get Athena query executions')
75+
logger.error('BatchGetQueryExecution failed', { error: errorMessage })
76+
return NextResponse.json({ error: errorMessage }, { status: 500 })
77+
}
78+
})
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { DeleteNamedQueryCommand } from '@aws-sdk/client-athena'
2+
import { createLogger } from '@sim/logger'
3+
import { getErrorMessage } from '@sim/utils/errors'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { awsAthenaDeleteNamedQueryContract } from '@/lib/api/contracts/tools/aws/athena-delete-named-query'
6+
import { parseToolRequest } from '@/lib/api/server'
7+
import { checkInternalAuth } from '@/lib/auth/hybrid'
8+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9+
import { createAthenaClient } from '@/app/api/tools/athena/utils'
10+
11+
const logger = createLogger('AthenaDeleteNamedQuery')
12+
13+
export const POST = withRouteHandler(async (request: NextRequest) => {
14+
try {
15+
const auth = await checkInternalAuth(request)
16+
if (!auth.success || !auth.userId) {
17+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
18+
}
19+
20+
const parsed = await parseToolRequest(awsAthenaDeleteNamedQueryContract, request, {
21+
errorFormat: 'details',
22+
logger,
23+
})
24+
if (!parsed.success) return parsed.response
25+
const data = parsed.data.body
26+
27+
const client = createAthenaClient({
28+
region: data.region,
29+
accessKeyId: data.accessKeyId,
30+
secretAccessKey: data.secretAccessKey,
31+
})
32+
33+
try {
34+
const command = new DeleteNamedQueryCommand({
35+
NamedQueryId: data.namedQueryId,
36+
})
37+
38+
await client.send(command)
39+
40+
return NextResponse.json({
41+
success: true,
42+
output: {
43+
success: true,
44+
},
45+
})
46+
} finally {
47+
client.destroy()
48+
}
49+
} catch (error) {
50+
const errorMessage = getErrorMessage(error, 'Failed to delete Athena named query')
51+
logger.error('DeleteNamedQuery failed', { error: errorMessage })
52+
return NextResponse.json({ error: errorMessage }, { status: 500 })
53+
}
54+
})
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { ListDatabasesCommand } from '@aws-sdk/client-athena'
2+
import { createLogger } from '@sim/logger'
3+
import { getErrorMessage } from '@sim/utils/errors'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { awsAthenaListDatabasesContract } from '@/lib/api/contracts/tools/aws/athena-list-databases'
6+
import { parseToolRequest } from '@/lib/api/server'
7+
import { checkInternalAuth } from '@/lib/auth/hybrid'
8+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9+
import { createAthenaClient } from '@/app/api/tools/athena/utils'
10+
11+
const logger = createLogger('AthenaListDatabases')
12+
13+
export const POST = withRouteHandler(async (request: NextRequest) => {
14+
try {
15+
const auth = await checkInternalAuth(request)
16+
if (!auth.success || !auth.userId) {
17+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
18+
}
19+
20+
const parsed = await parseToolRequest(awsAthenaListDatabasesContract, request, {
21+
errorFormat: 'details',
22+
logger,
23+
})
24+
if (!parsed.success) return parsed.response
25+
const data = parsed.data.body
26+
27+
const client = createAthenaClient({
28+
region: data.region,
29+
accessKeyId: data.accessKeyId,
30+
secretAccessKey: data.secretAccessKey,
31+
})
32+
33+
try {
34+
const command = new ListDatabasesCommand({
35+
CatalogName: data.catalogName,
36+
...(data.workGroup && { WorkGroup: data.workGroup }),
37+
...(data.maxResults !== undefined && { MaxResults: data.maxResults }),
38+
...(data.nextToken && { NextToken: data.nextToken }),
39+
})
40+
41+
const response = await client.send(command)
42+
43+
return NextResponse.json({
44+
success: true,
45+
output: {
46+
databases: (response.DatabaseList ?? []).map((db) => ({
47+
name: db.Name ?? '',
48+
description: db.Description ?? null,
49+
})),
50+
nextToken: response.NextToken ?? null,
51+
},
52+
})
53+
} finally {
54+
client.destroy()
55+
}
56+
} catch (error) {
57+
const errorMessage = getErrorMessage(error, 'Failed to list Athena databases')
58+
logger.error('ListDatabases failed', { error: errorMessage })
59+
return NextResponse.json({ error: errorMessage }, { status: 500 })
60+
}
61+
})
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ListTableMetadataCommand } from '@aws-sdk/client-athena'
2+
import { createLogger } from '@sim/logger'
3+
import { getErrorMessage } from '@sim/utils/errors'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { awsAthenaListTableMetadataContract } from '@/lib/api/contracts/tools/aws/athena-list-table-metadata'
6+
import { parseToolRequest } from '@/lib/api/server'
7+
import { checkInternalAuth } from '@/lib/auth/hybrid'
8+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9+
import { createAthenaClient } from '@/app/api/tools/athena/utils'
10+
11+
const logger = createLogger('AthenaListTableMetadata')
12+
13+
export const POST = withRouteHandler(async (request: NextRequest) => {
14+
try {
15+
const auth = await checkInternalAuth(request)
16+
if (!auth.success || !auth.userId) {
17+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
18+
}
19+
20+
const parsed = await parseToolRequest(awsAthenaListTableMetadataContract, request, {
21+
errorFormat: 'details',
22+
logger,
23+
})
24+
if (!parsed.success) return parsed.response
25+
const data = parsed.data.body
26+
27+
const client = createAthenaClient({
28+
region: data.region,
29+
accessKeyId: data.accessKeyId,
30+
secretAccessKey: data.secretAccessKey,
31+
})
32+
33+
try {
34+
const command = new ListTableMetadataCommand({
35+
CatalogName: data.catalogName,
36+
DatabaseName: data.databaseName,
37+
...(data.expression && { Expression: data.expression }),
38+
...(data.workGroup && { WorkGroup: data.workGroup }),
39+
...(data.maxResults !== undefined && { MaxResults: data.maxResults }),
40+
...(data.nextToken && { NextToken: data.nextToken }),
41+
})
42+
43+
const response = await client.send(command)
44+
45+
return NextResponse.json({
46+
success: true,
47+
output: {
48+
tables: (response.TableMetadataList ?? []).map((table) => ({
49+
name: table.Name ?? '',
50+
tableType: table.TableType ?? null,
51+
createTime: table.CreateTime?.getTime() ?? null,
52+
lastAccessTime: table.LastAccessTime?.getTime() ?? null,
53+
columns: (table.Columns ?? []).map((col) => ({
54+
name: col.Name ?? '',
55+
type: col.Type ?? null,
56+
comment: col.Comment ?? null,
57+
})),
58+
partitionKeys: (table.PartitionKeys ?? []).map((col) => ({
59+
name: col.Name ?? '',
60+
type: col.Type ?? null,
61+
comment: col.Comment ?? null,
62+
})),
63+
})),
64+
nextToken: response.NextToken ?? null,
65+
},
66+
})
67+
} finally {
68+
client.destroy()
69+
}
70+
} catch (error) {
71+
const errorMessage = getErrorMessage(error, 'Failed to list Athena table metadata')
72+
logger.error('ListTableMetadata failed', { error: errorMessage })
73+
return NextResponse.json({ error: errorMessage }, { status: 500 })
74+
}
75+
})
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { CancelUpdateStackCommand, CloudFormationClient } from '@aws-sdk/client-cloudformation'
2+
import { createLogger } from '@sim/logger'
3+
import { getErrorMessage } from '@sim/utils/errors'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { awsCloudformationCancelUpdateStackContract } from '@/lib/api/contracts/tools/aws/cloudformation-cancel-update-stack'
6+
import { parseToolRequest } from '@/lib/api/server'
7+
import { checkInternalAuth } from '@/lib/auth/hybrid'
8+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9+
10+
const logger = createLogger('CloudFormationCancelUpdateStack')
11+
12+
export const POST = withRouteHandler(async (request: NextRequest) => {
13+
try {
14+
const auth = await checkInternalAuth(request)
15+
if (!auth.success || !auth.userId) {
16+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
17+
}
18+
19+
const parsed = await parseToolRequest(awsCloudformationCancelUpdateStackContract, request, {
20+
errorFormat: 'details',
21+
logger,
22+
})
23+
if (!parsed.success) return parsed.response
24+
const validatedData = parsed.data.body
25+
26+
const client = new CloudFormationClient({
27+
region: validatedData.region,
28+
credentials: {
29+
accessKeyId: validatedData.accessKeyId,
30+
secretAccessKey: validatedData.secretAccessKey,
31+
},
32+
})
33+
34+
logger.info(`Cancelling update for CloudFormation stack "${validatedData.stackName}"`)
35+
36+
try {
37+
const command = new CancelUpdateStackCommand({
38+
StackName: validatedData.stackName,
39+
})
40+
41+
await client.send(command)
42+
43+
return NextResponse.json({
44+
success: true,
45+
output: {
46+
message: `Update for stack "${validatedData.stackName}" is being cancelled and rolled back`,
47+
},
48+
})
49+
} finally {
50+
client.destroy()
51+
}
52+
} catch (error) {
53+
const errorMessage = getErrorMessage(error, 'Failed to cancel CloudFormation stack update')
54+
logger.error('CancelUpdateStack failed', { error: errorMessage })
55+
return NextResponse.json({ error: errorMessage }, { status: 500 })
56+
}
57+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { CloudFormationClient, CreateChangeSetCommand } from '@aws-sdk/client-cloudformation'
2+
import { createLogger } from '@sim/logger'
3+
import { getErrorMessage } from '@sim/utils/errors'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { awsCloudformationCreateChangeSetContract } from '@/lib/api/contracts/tools/aws/cloudformation-create-change-set'
6+
import { parseToolRequest } from '@/lib/api/server'
7+
import { checkInternalAuth } from '@/lib/auth/hybrid'
8+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9+
import { parseCapabilities, toStackParameters } from '../utils'
10+
11+
const logger = createLogger('CloudFormationCreateChangeSet')
12+
13+
export const POST = withRouteHandler(async (request: NextRequest) => {
14+
try {
15+
const auth = await checkInternalAuth(request)
16+
if (!auth.success || !auth.userId) {
17+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
18+
}
19+
20+
const parsed = await parseToolRequest(awsCloudformationCreateChangeSetContract, request, {
21+
errorFormat: 'details',
22+
logger,
23+
})
24+
if (!parsed.success) return parsed.response
25+
const validatedData = parsed.data.body
26+
27+
const client = new CloudFormationClient({
28+
region: validatedData.region,
29+
credentials: {
30+
accessKeyId: validatedData.accessKeyId,
31+
secretAccessKey: validatedData.secretAccessKey,
32+
},
33+
})
34+
35+
logger.info(
36+
`Creating change set "${validatedData.changeSetName}" for stack "${validatedData.stackName}"`
37+
)
38+
39+
try {
40+
const command = new CreateChangeSetCommand({
41+
StackName: validatedData.stackName,
42+
ChangeSetName: validatedData.changeSetName,
43+
TemplateBody: validatedData.templateBody,
44+
UsePreviousTemplate: validatedData.usePreviousTemplate,
45+
Parameters: toStackParameters(validatedData.parameters),
46+
Capabilities: parseCapabilities(validatedData.capabilities),
47+
ChangeSetType: validatedData.changeSetType,
48+
Description: validatedData.description,
49+
})
50+
51+
const response = await client.send(command)
52+
53+
return NextResponse.json({
54+
success: true,
55+
output: {
56+
changeSetId: response.Id ?? '',
57+
stackId: response.StackId ?? '',
58+
},
59+
})
60+
} finally {
61+
client.destroy()
62+
}
63+
} catch (error) {
64+
const errorMessage = getErrorMessage(error, 'Failed to create CloudFormation change set')
65+
logger.error('CreateChangeSet failed', { error: errorMessage })
66+
return NextResponse.json({ error: errorMessage }, { status: 500 })
67+
}
68+
})

0 commit comments

Comments
 (0)