292 lines
7.9 KiB
TypeScript
292 lines
7.9 KiB
TypeScript
/**
|
|
* DynamoDB integration tests.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import {
|
|
DynamoDBClient,
|
|
CreateTableCommand,
|
|
PutItemCommand,
|
|
GetItemCommand,
|
|
DeleteItemCommand,
|
|
ScanCommand,
|
|
QueryCommand,
|
|
UpdateItemCommand,
|
|
DeleteTableCommand,
|
|
ListTablesCommand,
|
|
DescribeTableCommand,
|
|
} from '@aws-sdk/client-dynamodb';
|
|
import { makeClient, uniqueName } from './setup';
|
|
|
|
describe('DynamoDB', () => {
|
|
let dynamo: DynamoDBClient;
|
|
let tableName: string;
|
|
|
|
beforeAll(() => {
|
|
dynamo = makeClient(DynamoDBClient);
|
|
tableName = `test-table-${uniqueName()}`;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
await dynamo.send(new DeleteTableCommand({ TableName: tableName }));
|
|
} catch {
|
|
// ignore
|
|
}
|
|
});
|
|
|
|
it('should create table', async () => {
|
|
await dynamo.send(
|
|
new CreateTableCommand({
|
|
TableName: tableName,
|
|
KeySchema: [
|
|
{ AttributeName: 'pk', KeyType: 'HASH' },
|
|
{ AttributeName: 'sk', KeyType: 'RANGE' },
|
|
],
|
|
AttributeDefinitions: [
|
|
{ AttributeName: 'pk', AttributeType: 'S' },
|
|
{ AttributeName: 'sk', AttributeType: 'S' },
|
|
],
|
|
BillingMode: 'PAY_PER_REQUEST',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should describe table', async () => {
|
|
const response = await dynamo.send(new DescribeTableCommand({ TableName: tableName }));
|
|
expect(response.Table?.TableStatus).toBe('ACTIVE');
|
|
});
|
|
|
|
it('should list tables', async () => {
|
|
const response = await dynamo.send(new ListTablesCommand({}));
|
|
expect(response.TableNames?.includes(tableName)).toBe(true);
|
|
});
|
|
|
|
it('should put item', async () => {
|
|
await dynamo.send(
|
|
new PutItemCommand({
|
|
TableName: tableName,
|
|
Item: {
|
|
pk: { S: 'user#1' },
|
|
sk: { S: 'profile' },
|
|
name: { S: 'Alice' },
|
|
age: { N: '30' },
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should get item', async () => {
|
|
const response = await dynamo.send(
|
|
new GetItemCommand({
|
|
TableName: tableName,
|
|
Key: { pk: { S: 'user#1' }, sk: { S: 'profile' } },
|
|
})
|
|
);
|
|
expect(response.Item?.name?.S).toBe('Alice');
|
|
});
|
|
|
|
it('should update item', async () => {
|
|
await dynamo.send(
|
|
new UpdateItemCommand({
|
|
TableName: tableName,
|
|
Key: { pk: { S: 'user#1' }, sk: { S: 'profile' } },
|
|
UpdateExpression: 'SET #n = :v',
|
|
ExpressionAttributeNames: { '#n': 'name' },
|
|
ExpressionAttributeValues: { ':v': { S: 'Bob' } },
|
|
})
|
|
);
|
|
|
|
const response = await dynamo.send(
|
|
new GetItemCommand({
|
|
TableName: tableName,
|
|
Key: { pk: { S: 'user#1' }, sk: { S: 'profile' } },
|
|
})
|
|
);
|
|
expect(response.Item?.name?.S).toBe('Bob');
|
|
});
|
|
|
|
it('should put second item', async () => {
|
|
await dynamo.send(
|
|
new PutItemCommand({
|
|
TableName: tableName,
|
|
Item: {
|
|
pk: { S: 'user#2' },
|
|
sk: { S: 'profile' },
|
|
name: { S: 'Carol' },
|
|
age: { N: '25' },
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should scan table', async () => {
|
|
const response = await dynamo.send(new ScanCommand({ TableName: tableName }));
|
|
expect(response.Count).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it('should query table', async () => {
|
|
const response = await dynamo.send(
|
|
new QueryCommand({
|
|
TableName: tableName,
|
|
KeyConditionExpression: 'pk = :pk',
|
|
ExpressionAttributeValues: { ':pk': { S: 'user#1' } },
|
|
})
|
|
);
|
|
expect(response.Count).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('should delete items', async () => {
|
|
await dynamo.send(
|
|
new DeleteItemCommand({
|
|
TableName: tableName,
|
|
Key: { pk: { S: 'user#1' }, sk: { S: 'profile' } },
|
|
})
|
|
);
|
|
await dynamo.send(
|
|
new DeleteItemCommand({
|
|
TableName: tableName,
|
|
Key: { pk: { S: 'user#2' }, sk: { S: 'profile' } },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should delete table', async () => {
|
|
await dynamo.send(new DeleteTableCommand({ TableName: tableName }));
|
|
tableName = '';
|
|
});
|
|
});
|
|
|
|
describe('DynamoDB GSI/LSI', () => {
|
|
let dynamo: DynamoDBClient;
|
|
let tableName: string;
|
|
|
|
beforeAll(() => {
|
|
dynamo = makeClient(DynamoDBClient);
|
|
tableName = `test-gsi-table-${uniqueName()}`;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
await dynamo.send(new DeleteTableCommand({ TableName: tableName }));
|
|
} catch {
|
|
// ignore
|
|
}
|
|
});
|
|
|
|
it('should create table with GSI and LSI', async () => {
|
|
await dynamo.send(
|
|
new CreateTableCommand({
|
|
TableName: tableName,
|
|
KeySchema: [
|
|
{ AttributeName: 'pk', KeyType: 'HASH' },
|
|
{ AttributeName: 'sk', KeyType: 'RANGE' },
|
|
],
|
|
AttributeDefinitions: [
|
|
{ AttributeName: 'pk', AttributeType: 'S' },
|
|
{ AttributeName: 'sk', AttributeType: 'S' },
|
|
{ AttributeName: 'gsiPk', AttributeType: 'S' },
|
|
{ AttributeName: 'lsiSk', AttributeType: 'S' },
|
|
],
|
|
GlobalSecondaryIndexes: [
|
|
{
|
|
IndexName: 'gsi-1',
|
|
KeySchema: [
|
|
{ AttributeName: 'gsiPk', KeyType: 'HASH' },
|
|
{ AttributeName: 'sk', KeyType: 'RANGE' },
|
|
],
|
|
Projection: { ProjectionType: 'ALL' },
|
|
ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 },
|
|
},
|
|
],
|
|
LocalSecondaryIndexes: [
|
|
{
|
|
IndexName: 'lsi-1',
|
|
KeySchema: [
|
|
{ AttributeName: 'pk', KeyType: 'HASH' },
|
|
{ AttributeName: 'lsiSk', KeyType: 'RANGE' },
|
|
],
|
|
Projection: { ProjectionType: 'KEYS_ONLY' },
|
|
},
|
|
],
|
|
ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should verify indexes via DescribeTable', async () => {
|
|
const desc = await dynamo.send(new DescribeTableCommand({ TableName: tableName }));
|
|
const gsis = desc.Table?.GlobalSecondaryIndexes || [];
|
|
const lsis = desc.Table?.LocalSecondaryIndexes || [];
|
|
|
|
expect(gsis.length).toBe(1);
|
|
expect(gsis[0]?.IndexName).toBe('gsi-1');
|
|
expect(gsis[0]?.Projection?.ProjectionType).toBe('ALL');
|
|
expect(lsis.length).toBe(1);
|
|
expect(lsis[0]?.IndexName).toBe('lsi-1');
|
|
expect(lsis[0]?.Projection?.ProjectionType).toBe('KEYS_ONLY');
|
|
});
|
|
|
|
it('should put items with GSI attributes', async () => {
|
|
await dynamo.send(
|
|
new PutItemCommand({
|
|
TableName: tableName,
|
|
Item: {
|
|
pk: { S: 'item-1' },
|
|
sk: { S: 'rev-1' },
|
|
gsiPk: { S: 'group-A' },
|
|
lsiSk: { S: '2024-01-01' },
|
|
},
|
|
})
|
|
);
|
|
await dynamo.send(
|
|
new PutItemCommand({
|
|
TableName: tableName,
|
|
Item: {
|
|
pk: { S: 'item-2' },
|
|
sk: { S: 'rev-1' },
|
|
gsiPk: { S: 'group-A' },
|
|
lsiSk: { S: '2024-01-02' },
|
|
},
|
|
})
|
|
);
|
|
await dynamo.send(
|
|
new PutItemCommand({
|
|
TableName: tableName,
|
|
Item: { pk: { S: 'item-3' }, sk: { S: 'rev-1' }, data: { S: 'no-gsi-attrs' } },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should query GSI', async () => {
|
|
const resp = await dynamo.send(
|
|
new QueryCommand({
|
|
TableName: tableName,
|
|
IndexName: 'gsi-1',
|
|
KeyConditionExpression: 'gsiPk = :gpk',
|
|
ExpressionAttributeValues: { ':gpk': { S: 'group-A' } },
|
|
})
|
|
);
|
|
expect(resp.Count).toBe(2);
|
|
const pks = new Set(resp.Items?.map((i) => i.pk.S));
|
|
expect(pks.has('item-3')).toBe(false);
|
|
});
|
|
|
|
it('should query LSI', async () => {
|
|
const resp = await dynamo.send(
|
|
new QueryCommand({
|
|
TableName: tableName,
|
|
IndexName: 'lsi-1',
|
|
KeyConditionExpression: 'pk = :pk AND lsiSk > :d',
|
|
ExpressionAttributeValues: { ':pk': { S: 'item-1' }, ':d': { S: '2024-01-00' } },
|
|
})
|
|
);
|
|
expect(resp.Count).toBe(1);
|
|
});
|
|
|
|
it('should delete table', async () => {
|
|
await dynamo.send(new DeleteTableCommand({ TableName: tableName }));
|
|
tableName = '';
|
|
});
|
|
});
|