feat: Cloudformation + RDS
This commit is contained in:
@@ -35,7 +35,6 @@ import type {
|
||||
CreateResourceInput,
|
||||
DatabaseSnapshot,
|
||||
LogsInsightsQueryResult,
|
||||
NoSqlItem,
|
||||
} from '../cloud-spi/types'
|
||||
import {CloudAdapterRegistry} from '../registry/CloudAdapterRegistry'
|
||||
import {CloudProxyService} from '../service/CloudProxyService'
|
||||
@@ -197,6 +196,44 @@ describe('API Gateway method test routes', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('API Gateway resource routes', () => {
|
||||
test('dispatches parent-scoped nested route creation and returns the resource tree', async () => {
|
||||
const calls: unknown[] = []
|
||||
const tree = {
|
||||
resources: [
|
||||
{resourceId: 'root-123', parentId: null, path: '/', pathPart: null, methods: []},
|
||||
{resourceId: 'orders-123', parentId: 'root-123', path: '/orders', pathPart: 'orders', methods: []},
|
||||
],
|
||||
routes: [],
|
||||
stages: ['dev'],
|
||||
}
|
||||
const app = appWithRoutes([mockAdapter('aws', {
|
||||
service: 'apigateway',
|
||||
createApiGatewayLambdaRoute: async (apiId, input) => {
|
||||
calls.push({apiId, input})
|
||||
return {resourceId: 'order-123', deploymentId: 'deployment-123', invokeUrl: '/orders/{id}'}
|
||||
},
|
||||
listApiGatewayRoutes: async () => tree,
|
||||
})])
|
||||
|
||||
const create = await app.request('/api/clouds/aws/services/apigateway/resources/api-123/routes', {
|
||||
method: 'POST', headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
path: '/orders/{id}', parentResourceId: 'orders-123', pathPart: '{id}',
|
||||
method: 'PATCH', stage: 'dev', functionName: 'orders-handler',
|
||||
}),
|
||||
})
|
||||
const list = await app.request('/api/clouds/aws/services/apigateway/resources/api-123/routes')
|
||||
|
||||
expect(create.status).toBe(201)
|
||||
expect(await list.json()).toEqual(tree)
|
||||
expect(calls).toEqual([{apiId: 'api-123', input: {
|
||||
path: '/orders/{id}', parentResourceId: 'orders-123', pathPart: '{id}',
|
||||
method: 'PATCH', stage: 'dev', functionName: 'orders-handler',
|
||||
}}])
|
||||
})
|
||||
})
|
||||
|
||||
describe('S3 website routes', () => {
|
||||
test('routes normalized website configuration lifecycle requests through the storage adapter', async () => {
|
||||
const calls: Array<{operation: string; args: unknown[]}> = []
|
||||
@@ -579,46 +616,61 @@ describe('cloud schema routes', () => {
|
||||
expect(body[0].name).toBe('items')
|
||||
})
|
||||
|
||||
test('lists DynamoDB records through the nosql adapter', async () => {
|
||||
test('lists DynamoDB records through the generic item adapter', async () => {
|
||||
const app = appWithRoutes([mockAdapter('aws', {
|
||||
service: 'nosql',
|
||||
schema: awsDynamoDbSchema,
|
||||
listNoSqlItems: async (resourceId: string): Promise<NoSqlItem[]> => [{
|
||||
id: '{"pk":"item-1"}',
|
||||
key: {pk: 'item-1'},
|
||||
document: {pk: 'item-1', name: 'First item', table: resourceId},
|
||||
}],
|
||||
items: {
|
||||
listItems: async (resourceId) => ({items: [{
|
||||
id: '{"pk":"item-1"}', collectionId: null, timestamp: null,
|
||||
body: {pk: 'item-1', name: 'First item', table: resourceId}, metadata: {key: {pk: 'item-1'}},
|
||||
}], nextCursor: null}),
|
||||
},
|
||||
})])
|
||||
|
||||
const res = await app.request('/api/clouds/aws/services/nosql/resources/orders/items')
|
||||
const body = await res.json()
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(body[0].key).toEqual({pk: 'item-1'})
|
||||
expect(body[0].document.table).toBe('orders')
|
||||
expect(body.items[0].metadata.key).toEqual({pk: 'item-1'})
|
||||
expect(body.items[0].body.table).toBe('orders')
|
||||
})
|
||||
|
||||
test('puts a DynamoDB record through the nosql adapter', async () => {
|
||||
test('puts and deletes DynamoDB records through the generic item adapter with the complete key', async () => {
|
||||
const calls: Array<{resourceId: string; document: Record<string, unknown>}> = []
|
||||
const app = appWithRoutes([mockAdapter('aws', {
|
||||
service: 'nosql',
|
||||
schema: awsDynamoDbSchema,
|
||||
putNoSqlItem: async (resourceId, document): Promise<NoSqlItem> => {
|
||||
calls.push({resourceId, document})
|
||||
return {id: JSON.stringify({pk: document.pk}), key: {pk: document.pk}, document}
|
||||
items: {
|
||||
listItems: async () => ({items: [], nextCursor: null}),
|
||||
putItem: async (resourceId, document) => {
|
||||
calls.push({resourceId, document})
|
||||
return {id: JSON.stringify({pk: document.pk, sk: document.sk}), collectionId: null, timestamp: null, body: document, metadata: {key: {pk: document.pk, sk: document.sk}}}
|
||||
},
|
||||
deleteItem: async (resourceId, itemId) => {
|
||||
calls.push({resourceId, document: {itemId}})
|
||||
},
|
||||
},
|
||||
})])
|
||||
|
||||
const res = await app.request('/api/clouds/aws/services/nosql/resources/orders/items', {
|
||||
method: 'POST',
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify({pk: 'item-1', name: 'First item'}),
|
||||
body: JSON.stringify({pk: 'item-1', sk: '2026-09-29', name: 'First item'}),
|
||||
})
|
||||
const body = await res.json()
|
||||
|
||||
expect(res.status).toBe(201)
|
||||
expect(body.key).toEqual({pk: 'item-1'})
|
||||
expect(calls).toEqual([{resourceId: 'orders', document: {pk: 'item-1', name: 'First item'}}])
|
||||
expect(body.metadata.key).toEqual({pk: 'item-1', sk: '2026-09-29'})
|
||||
expect(calls).toEqual([{resourceId: 'orders', document: {pk: 'item-1', sk: '2026-09-29', name: 'First item'}}])
|
||||
|
||||
const deleteRes = await app.request('/api/clouds/aws/services/nosql/resources/orders/items/%7B%22pk%22%3A%22item-1%22%2C%22sk%22%3A%222026-09-29%22%7D', {method: 'DELETE'})
|
||||
|
||||
expect(deleteRes.status).toBe(200)
|
||||
expect(calls).toEqual([
|
||||
{resourceId: 'orders', document: {pk: 'item-1', sk: '2026-09-29', name: 'First item'}},
|
||||
{resourceId: 'orders', document: {itemId: '{"pk":"item-1","sk":"2026-09-29"}'}},
|
||||
])
|
||||
})
|
||||
|
||||
test('clears the SES mailbox through the email adapter', async () => {
|
||||
@@ -804,6 +856,51 @@ describe('cloud schema routes', () => {
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
test('filters log events with pagination through the route', async () => {
|
||||
let received: Record<string, unknown> = {}
|
||||
const app = appWithRoutes([mockAdapter('aws', {
|
||||
service: 'logs',
|
||||
schema: awsLogsSchema,
|
||||
filterLogEvents: async (logGroupName, input, page) => {
|
||||
received = {logGroupName, input, page}
|
||||
return {items: [{id: 'event-1', collectionId: 'stream-a', timestamp: null, body: {message: 'ERROR'}, metadata: {}}], nextCursor: 'next'}
|
||||
},
|
||||
})])
|
||||
|
||||
const res = await app.request('/api/clouds/aws/services/logs/resources/%2Ffloci%2Fprobe/filter?cursor=previous&limit=25', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({filterPattern: 'ERROR', logStreamNames: ['stream-a'], startTime: 1000, endTime: 2000}),
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toMatchObject({nextCursor: 'next'})
|
||||
expect(received).toEqual({
|
||||
logGroupName: '/floci/probe',
|
||||
input: {filterPattern: 'ERROR', logStreamNames: ['stream-a'], startTime: 1000, endTime: 2000},
|
||||
page: {cursor: 'previous', limit: 25},
|
||||
})
|
||||
})
|
||||
|
||||
test('rejects invalid log event filters before calling the adapter', async () => {
|
||||
let called = false
|
||||
const app = appWithRoutes([mockAdapter('aws', {
|
||||
service: 'logs',
|
||||
schema: awsLogsSchema,
|
||||
filterLogEvents: async () => {
|
||||
called = true
|
||||
return {items: [], nextCursor: null}
|
||||
},
|
||||
})])
|
||||
|
||||
const res = await app.request('/api/clouds/aws/services/logs/resources/%2Ffloci%2Fprobe/filter', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({logStreamNames: 'stream-a'}),
|
||||
})
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
expect(called).toBe(false)
|
||||
})
|
||||
|
||||
test('normalizes runtime unavailable errors', async () => {
|
||||
const app = appWithRoutes([
|
||||
mockAdapter('aws', {
|
||||
@@ -1621,6 +1718,30 @@ describe('child collection routes', () => {
|
||||
expect(await res.json()).toEqual({items: stubItems.items, nextCursor: null})
|
||||
})
|
||||
|
||||
test('passes a structured flat-item query and pagination through the existing query route', async () => {
|
||||
let seen: unknown
|
||||
const app = appWithRoutes([mockAdapter('aws', {items: {
|
||||
...itemsStub,
|
||||
queryItems: async (resourceId, query, page) => {
|
||||
seen = {resourceId, query, page}
|
||||
return {...stubItems, count: 1, scannedCount: 4, consumedCapacity: 0.5}
|
||||
},
|
||||
}})])
|
||||
const res = await app.request('/api/clouds/aws/services/storage/resources/table-1/query?cursor=opaque&limit=25', {
|
||||
method: 'POST',
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify({query: {operation: 'scan', filter: {attribute: 'status', operator: 'EQ', type: 'S', value: 'OPEN'}}}),
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(seen).toEqual({
|
||||
resourceId: 'table-1',
|
||||
query: {operation: 'scan', filter: {attribute: 'status', operator: 'EQ', type: 'S', value: 'OPEN'}},
|
||||
page: {cursor: 'opaque', limit: 25},
|
||||
})
|
||||
expect(await res.json()).toMatchObject({count: 1, scannedCount: 4, consumedCapacity: 0.5})
|
||||
})
|
||||
|
||||
// A documents-only adapter has no flat shape, and vice versa.
|
||||
test('501s when the adapter has the other shape', async () => {
|
||||
const app = appWithRoutes([mockAdapter('aws', {documents: documentsStub})])
|
||||
|
||||
Reference in New Issue
Block a user