Skip to content
78 changes: 78 additions & 0 deletions apps/sim/app/api/tools/athena/batch-get-query-execution/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { BatchGetQueryExecutionCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaBatchGetQueryExecutionContract } from '@/lib/api/contracts/tools/aws/athena-batch-get-query-execution'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'

const logger = createLogger('AthenaBatchGetQueryExecution')

export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const parsed = await parseToolRequest(awsAthenaBatchGetQueryExecutionContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body

const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})

try {
const command = new BatchGetQueryExecutionCommand({
QueryExecutionIds: data.queryExecutionIds,
})

const response = await client.send(command)

return NextResponse.json({
success: true,
output: {
queryExecutions: (response.QueryExecutions ?? []).map((execution) => ({
queryExecutionId: execution.QueryExecutionId ?? '',
query: execution.Query ?? null,
state: execution.Status?.State ?? null,
stateChangeReason: execution.Status?.StateChangeReason ?? null,
statementType: execution.StatementType ?? null,
database: execution.QueryExecutionContext?.Database ?? null,
catalog: execution.QueryExecutionContext?.Catalog ?? null,
workGroup: execution.WorkGroup ?? null,
submissionDateTime: execution.Status?.SubmissionDateTime?.getTime() ?? null,
completionDateTime: execution.Status?.CompletionDateTime?.getTime() ?? null,
dataScannedInBytes: execution.Statistics?.DataScannedInBytes ?? null,
engineExecutionTimeInMillis: execution.Statistics?.EngineExecutionTimeInMillis ?? null,
queryPlanningTimeInMillis: execution.Statistics?.QueryPlanningTimeInMillis ?? null,
queryQueueTimeInMillis: execution.Statistics?.QueryQueueTimeInMillis ?? null,
totalExecutionTimeInMillis: execution.Statistics?.TotalExecutionTimeInMillis ?? null,
outputLocation: execution.ResultConfiguration?.OutputLocation ?? null,
})),
unprocessedQueryExecutionIds: (response.UnprocessedQueryExecutionIds ?? []).map(
(item) => ({
queryExecutionId: item.QueryExecutionId ?? null,
errorCode: item.ErrorCode ?? null,
errorMessage: item.ErrorMessage ?? null,
})
),
},
})
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to batch get Athena query executions')
logger.error('BatchGetQueryExecution failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
54 changes: 54 additions & 0 deletions apps/sim/app/api/tools/athena/delete-named-query/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { DeleteNamedQueryCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaDeleteNamedQueryContract } from '@/lib/api/contracts/tools/aws/athena-delete-named-query'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'

const logger = createLogger('AthenaDeleteNamedQuery')

export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const parsed = await parseToolRequest(awsAthenaDeleteNamedQueryContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body

const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})

try {
const command = new DeleteNamedQueryCommand({
NamedQueryId: data.namedQueryId,
})

await client.send(command)

return NextResponse.json({
success: true,
output: {
success: true,
},
})
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to delete Athena named query')
logger.error('DeleteNamedQuery failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
61 changes: 61 additions & 0 deletions apps/sim/app/api/tools/athena/list-databases/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ListDatabasesCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaListDatabasesContract } from '@/lib/api/contracts/tools/aws/athena-list-databases'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'

const logger = createLogger('AthenaListDatabases')

export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const parsed = await parseToolRequest(awsAthenaListDatabasesContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body

const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})

try {
const command = new ListDatabasesCommand({
CatalogName: data.catalogName,
...(data.workGroup && { WorkGroup: data.workGroup }),
...(data.maxResults !== undefined && { MaxResults: data.maxResults }),
...(data.nextToken && { NextToken: data.nextToken }),
})

const response = await client.send(command)

return NextResponse.json({
success: true,
output: {
databases: (response.DatabaseList ?? []).map((db) => ({
name: db.Name ?? '',
description: db.Description ?? null,
})),
nextToken: response.NextToken ?? null,
},
})
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to list Athena databases')
logger.error('ListDatabases failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
75 changes: 75 additions & 0 deletions apps/sim/app/api/tools/athena/list-table-metadata/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ListTableMetadataCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaListTableMetadataContract } from '@/lib/api/contracts/tools/aws/athena-list-table-metadata'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'

const logger = createLogger('AthenaListTableMetadata')

export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const parsed = await parseToolRequest(awsAthenaListTableMetadataContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body

const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})

try {
const command = new ListTableMetadataCommand({
CatalogName: data.catalogName,
DatabaseName: data.databaseName,
...(data.expression && { Expression: data.expression }),
...(data.workGroup && { WorkGroup: data.workGroup }),
...(data.maxResults !== undefined && { MaxResults: data.maxResults }),
...(data.nextToken && { NextToken: data.nextToken }),
})

const response = await client.send(command)

return NextResponse.json({
success: true,
output: {
tables: (response.TableMetadataList ?? []).map((table) => ({
name: table.Name ?? '',
tableType: table.TableType ?? null,
createTime: table.CreateTime?.getTime() ?? null,
lastAccessTime: table.LastAccessTime?.getTime() ?? null,
columns: (table.Columns ?? []).map((col) => ({
name: col.Name ?? '',
type: col.Type ?? null,
comment: col.Comment ?? null,
})),
partitionKeys: (table.PartitionKeys ?? []).map((col) => ({
name: col.Name ?? '',
type: col.Type ?? null,
comment: col.Comment ?? null,
})),
})),
nextToken: response.NextToken ?? null,
},
})
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to list Athena table metadata')
logger.error('ListTableMetadata failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
57 changes: 57 additions & 0 deletions apps/sim/app/api/tools/cloudformation/cancel-update-stack/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { CancelUpdateStackCommand, CloudFormationClient } from '@aws-sdk/client-cloudformation'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudformationCancelUpdateStackContract } from '@/lib/api/contracts/tools/aws/cloudformation-cancel-update-stack'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

const logger = createLogger('CloudFormationCancelUpdateStack')

export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const parsed = await parseToolRequest(awsCloudformationCancelUpdateStackContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body

const client = new CloudFormationClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})

logger.info(`Cancelling update for CloudFormation stack "${validatedData.stackName}"`)

try {
const command = new CancelUpdateStackCommand({
StackName: validatedData.stackName,
})

await client.send(command)

return NextResponse.json({
success: true,
output: {
message: `Update for stack "${validatedData.stackName}" is being cancelled and rolled back`,
},
})
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to cancel CloudFormation stack update')
logger.error('CancelUpdateStack failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
68 changes: 68 additions & 0 deletions apps/sim/app/api/tools/cloudformation/create-change-set/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { CloudFormationClient, CreateChangeSetCommand } from '@aws-sdk/client-cloudformation'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsCloudformationCreateChangeSetContract } from '@/lib/api/contracts/tools/aws/cloudformation-create-change-set'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseCapabilities, toStackParameters } from '../utils'

const logger = createLogger('CloudFormationCreateChangeSet')

export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const parsed = await parseToolRequest(awsCloudformationCreateChangeSetContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body

const client = new CloudFormationClient({
region: validatedData.region,
credentials: {
accessKeyId: validatedData.accessKeyId,
secretAccessKey: validatedData.secretAccessKey,
},
})

logger.info(
`Creating change set "${validatedData.changeSetName}" for stack "${validatedData.stackName}"`
)

try {
const command = new CreateChangeSetCommand({
StackName: validatedData.stackName,
ChangeSetName: validatedData.changeSetName,
TemplateBody: validatedData.templateBody,
UsePreviousTemplate: validatedData.usePreviousTemplate,
Parameters: toStackParameters(validatedData.parameters),
Capabilities: parseCapabilities(validatedData.capabilities),
ChangeSetType: validatedData.changeSetType,
Description: validatedData.description,
})

const response = await client.send(command)

return NextResponse.json({
success: true,
output: {
changeSetId: response.Id ?? '',
stackId: response.StackId ?? '',
},
})
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to create CloudFormation change set')
logger.error('CreateChangeSet failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
Loading
Loading