Complete tasks 3.2-3.3: Data models and DynamoDB table schemas

- Defined all 8 data models (Page, Widget, Bookmark, Note, TagAssociation, Group, Share, Preferences)
- Implemented DynamoDB table creation for all tables with proper schemas
- Added GSIs for efficient querying (UserBookmarksIndex, UserNotesIndex, TagItemsIndex, UserSharesIndex)
- Comprehensive test coverage for all table schemas
- Updated init-db command to create all tables
This commit is contained in:
2026-02-18 22:55:06 -05:00
parent 7175ff14ba
commit 9f07b0c6f9
13 changed files with 1363 additions and 6 deletions

View File

@@ -97,6 +97,516 @@ func (db *DynamoDBClient) CreateUsersTable(ctx context.Context) error {
return nil
}
// CreatePagesTable creates the Pages table with composite key (user_id, page_id)
func (db *DynamoDBClient) CreatePagesTable(ctx context.Context) error {
tableName := "Pages"
// Check if table already exists
_, err := db.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
})
if err == nil {
return nil
}
// Create table
_, err = db.client.CreateTable(ctx, &dynamodb.CreateTableInput{
TableName: aws.String(tableName),
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("user_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("page_id"),
AttributeType: types.ScalarAttributeTypeS,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("user_id"),
KeyType: types.KeyTypeHash, // Partition key
},
{
AttributeName: aws.String("page_id"),
KeyType: types.KeyTypeRange, // Sort key
},
},
BillingMode: types.BillingModePayPerRequest,
})
if err != nil {
return fmt.Errorf("failed to create Pages table: %w", err)
}
// Wait for table to be active
waiter := dynamodb.NewTableExistsWaiter(db.client)
err = waiter.Wait(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}, 5*60)
if err != nil {
return fmt.Errorf("failed waiting for Pages table to be active: %w", err)
}
return nil
}
// CreateWidgetsTable creates the Widgets table with composite key (page_id, widget_id)
func (db *DynamoDBClient) CreateWidgetsTable(ctx context.Context) error {
tableName := "Widgets"
// Check if table already exists
_, err := db.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
})
if err == nil {
return nil
}
// Create table
_, err = db.client.CreateTable(ctx, &dynamodb.CreateTableInput{
TableName: aws.String(tableName),
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("page_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("widget_id"),
AttributeType: types.ScalarAttributeTypeS,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("page_id"),
KeyType: types.KeyTypeHash, // Partition key
},
{
AttributeName: aws.String("widget_id"),
KeyType: types.KeyTypeRange, // Sort key
},
},
BillingMode: types.BillingModePayPerRequest,
})
if err != nil {
return fmt.Errorf("failed to create Widgets table: %w", err)
}
// Wait for table to be active
waiter := dynamodb.NewTableExistsWaiter(db.client)
err = waiter.Wait(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}, 5*60)
if err != nil {
return fmt.Errorf("failed waiting for Widgets table to be active: %w", err)
}
return nil
}
// CreateBookmarksTable creates the Bookmarks table with UserBookmarksIndex GSI
func (db *DynamoDBClient) CreateBookmarksTable(ctx context.Context) error {
tableName := "Bookmarks"
// Check if table already exists
_, err := db.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
})
if err == nil {
return nil
}
// Create table
_, err = db.client.CreateTable(ctx, &dynamodb.CreateTableInput{
TableName: aws.String(tableName),
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("widget_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("bookmark_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("user_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("created_at"),
AttributeType: types.ScalarAttributeTypeN,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("widget_id"),
KeyType: types.KeyTypeHash, // Partition key
},
{
AttributeName: aws.String("bookmark_id"),
KeyType: types.KeyTypeRange, // Sort key
},
},
GlobalSecondaryIndexes: []types.GlobalSecondaryIndex{
{
IndexName: aws.String("UserBookmarksIndex"),
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("user_id"),
KeyType: types.KeyTypeHash, // Partition key
},
{
AttributeName: aws.String("created_at"),
KeyType: types.KeyTypeRange, // Sort key
},
},
Projection: &types.Projection{
ProjectionType: types.ProjectionTypeAll,
},
},
},
BillingMode: types.BillingModePayPerRequest,
})
if err != nil {
return fmt.Errorf("failed to create Bookmarks table: %w", err)
}
// Wait for table to be active
waiter := dynamodb.NewTableExistsWaiter(db.client)
err = waiter.Wait(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}, 5*60)
if err != nil {
return fmt.Errorf("failed waiting for Bookmarks table to be active: %w", err)
}
return nil
}
// CreateNotesTable creates the Notes table with UserNotesIndex GSI
func (db *DynamoDBClient) CreateNotesTable(ctx context.Context) error {
tableName := "Notes"
// Check if table already exists
_, err := db.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
})
if err == nil {
return nil
}
// Create table
_, err = db.client.CreateTable(ctx, &dynamodb.CreateTableInput{
TableName: aws.String(tableName),
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("widget_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("note_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("user_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("created_at"),
AttributeType: types.ScalarAttributeTypeN,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("widget_id"),
KeyType: types.KeyTypeHash, // Partition key
},
{
AttributeName: aws.String("note_id"),
KeyType: types.KeyTypeRange, // Sort key
},
},
GlobalSecondaryIndexes: []types.GlobalSecondaryIndex{
{
IndexName: aws.String("UserNotesIndex"),
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("user_id"),
KeyType: types.KeyTypeHash, // Partition key
},
{
AttributeName: aws.String("created_at"),
KeyType: types.KeyTypeRange, // Sort key
},
},
Projection: &types.Projection{
ProjectionType: types.ProjectionTypeAll,
},
},
},
BillingMode: types.BillingModePayPerRequest,
})
if err != nil {
return fmt.Errorf("failed to create Notes table: %w", err)
}
// Wait for table to be active
waiter := dynamodb.NewTableExistsWaiter(db.client)
err = waiter.Wait(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}, 5*60)
if err != nil {
return fmt.Errorf("failed waiting for Notes table to be active: %w", err)
}
return nil
}
// CreateTagAssociationsTable creates the TagAssociations table with TagItemsIndex GSI
func (db *DynamoDBClient) CreateTagAssociationsTable(ctx context.Context) error {
tableName := "TagAssociations"
// Check if table already exists
_, err := db.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
})
if err == nil {
return nil
}
// Create table
_, err = db.client.CreateTable(ctx, &dynamodb.CreateTableInput{
TableName: aws.String(tableName),
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("item_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("tag_name"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("user_id"),
AttributeType: types.ScalarAttributeTypeS,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("item_id"),
KeyType: types.KeyTypeHash, // Partition key
},
{
AttributeName: aws.String("tag_name"),
KeyType: types.KeyTypeRange, // Sort key
},
},
GlobalSecondaryIndexes: []types.GlobalSecondaryIndex{
{
IndexName: aws.String("TagItemsIndex"),
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("tag_name"),
KeyType: types.KeyTypeHash, // Partition key
},
{
AttributeName: aws.String("user_id"),
KeyType: types.KeyTypeRange, // Sort key
},
},
Projection: &types.Projection{
ProjectionType: types.ProjectionTypeInclude,
NonKeyAttributes: []string{"item_id", "item_type", "created_at"},
},
},
},
BillingMode: types.BillingModePayPerRequest,
})
if err != nil {
return fmt.Errorf("failed to create TagAssociations table: %w", err)
}
// Wait for table to be active
waiter := dynamodb.NewTableExistsWaiter(db.client)
err = waiter.Wait(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}, 5*60)
if err != nil {
return fmt.Errorf("failed waiting for TagAssociations table to be active: %w", err)
}
return nil
}
// CreateGroupsTable creates the Groups table
func (db *DynamoDBClient) CreateGroupsTable(ctx context.Context) error {
tableName := "Groups"
// Check if table already exists
_, err := db.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
})
if err == nil {
return nil
}
// Create table
_, err = db.client.CreateTable(ctx, &dynamodb.CreateTableInput{
TableName: aws.String(tableName),
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("widget_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("group_id"),
AttributeType: types.ScalarAttributeTypeS,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("widget_id"),
KeyType: types.KeyTypeHash, // Partition key
},
{
AttributeName: aws.String("group_id"),
KeyType: types.KeyTypeRange, // Sort key
},
},
BillingMode: types.BillingModePayPerRequest,
})
if err != nil {
return fmt.Errorf("failed to create Groups table: %w", err)
}
// Wait for table to be active
waiter := dynamodb.NewTableExistsWaiter(db.client)
err = waiter.Wait(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}, 5*60)
if err != nil {
return fmt.Errorf("failed waiting for Groups table to be active: %w", err)
}
return nil
}
// CreateSharedItemsTable creates the SharedItems table with UserSharesIndex GSI
func (db *DynamoDBClient) CreateSharedItemsTable(ctx context.Context) error {
tableName := "SharedItems"
// Check if table already exists
_, err := db.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
})
if err == nil {
return nil
}
// Create table
_, err = db.client.CreateTable(ctx, &dynamodb.CreateTableInput{
TableName: aws.String(tableName),
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("share_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("user_id"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("created_at"),
AttributeType: types.ScalarAttributeTypeN,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("share_id"),
KeyType: types.KeyTypeHash, // Partition key
},
},
GlobalSecondaryIndexes: []types.GlobalSecondaryIndex{
{
IndexName: aws.String("UserSharesIndex"),
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("user_id"),
KeyType: types.KeyTypeHash, // Partition key
},
{
AttributeName: aws.String("created_at"),
KeyType: types.KeyTypeRange, // Sort key
},
},
Projection: &types.Projection{
ProjectionType: types.ProjectionTypeInclude,
NonKeyAttributes: []string{"share_id", "item_id", "item_type", "access_count"},
},
},
},
BillingMode: types.BillingModePayPerRequest,
})
if err != nil {
return fmt.Errorf("failed to create SharedItems table: %w", err)
}
// Wait for table to be active
waiter := dynamodb.NewTableExistsWaiter(db.client)
err = waiter.Wait(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}, 5*60)
if err != nil {
return fmt.Errorf("failed waiting for SharedItems table to be active: %w", err)
}
return nil
}
// CreatePreferencesTable creates the Preferences table
func (db *DynamoDBClient) CreatePreferencesTable(ctx context.Context) error {
tableName := "Preferences"
// Check if table already exists
_, err := db.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
})
if err == nil {
return nil
}
// Create table
_, err = db.client.CreateTable(ctx, &dynamodb.CreateTableInput{
TableName: aws.String(tableName),
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("user_id"),
AttributeType: types.ScalarAttributeTypeS,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("user_id"),
KeyType: types.KeyTypeHash, // Partition key
},
},
BillingMode: types.BillingModePayPerRequest,
})
if err != nil {
return fmt.Errorf("failed to create Preferences table: %w", err)
}
// Wait for table to be active
waiter := dynamodb.NewTableExistsWaiter(db.client)
err = waiter.Wait(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}, 5*60)
if err != nil {
return fmt.Errorf("failed waiting for Preferences table to be active: %w", err)
}
return nil
}
// GetClient returns the underlying DynamoDB client
func (db *DynamoDBClient) GetClient() *dynamodb.Client {
return db.client

View File

@@ -450,3 +450,449 @@ func deleteTestTable(t *testing.T, ctx context.Context, client *DynamoDBClient,
t.Logf("Warning: Failed to delete test table: %v", err)
}
}
// TestCreateUsersTable tests the Users table creation
func TestCreateUsersTable(t *testing.T) {
client, ctx := setupTestClient(t)
// Delete table if it exists
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
TableName: aws.String("Users"),
})
time.Sleep(1 * time.Second)
// Create table
err := client.CreateUsersTable(ctx)
if err != nil {
t.Fatalf("CreateUsersTable failed: %v", err)
}
// Verify table exists and has correct schema
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String("Users"),
})
if err != nil {
t.Fatalf("DescribeTable failed: %v", err)
}
// Verify key schema
if len(output.Table.KeySchema) != 1 {
t.Fatalf("Expected 1 key, got %d", len(output.Table.KeySchema))
}
if *output.Table.KeySchema[0].AttributeName != "user_id" {
t.Errorf("Expected partition key 'user_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
}
// Test idempotency - calling again should not error
err = client.CreateUsersTable(ctx)
if err != nil {
t.Fatalf("CreateUsersTable should be idempotent: %v", err)
}
}
// TestCreatePagesTable tests the Pages table creation with composite key
func TestCreatePagesTable(t *testing.T) {
client, ctx := setupTestClient(t)
// Delete table if it exists
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
TableName: aws.String("Pages"),
})
time.Sleep(1 * time.Second)
// Create table
err := client.CreatePagesTable(ctx)
if err != nil {
t.Fatalf("CreatePagesTable failed: %v", err)
}
// Verify table exists and has correct schema
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String("Pages"),
})
if err != nil {
t.Fatalf("DescribeTable failed: %v", err)
}
// Verify composite key schema
if len(output.Table.KeySchema) != 2 {
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
}
if *output.Table.KeySchema[0].AttributeName != "user_id" {
t.Errorf("Expected partition key 'user_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
}
if *output.Table.KeySchema[1].AttributeName != "page_id" {
t.Errorf("Expected sort key 'page_id', got '%s'", *output.Table.KeySchema[1].AttributeName)
}
}
// TestCreateWidgetsTable tests the Widgets table creation
func TestCreateWidgetsTable(t *testing.T) {
client, ctx := setupTestClient(t)
// Delete table if it exists
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
TableName: aws.String("Widgets"),
})
time.Sleep(1 * time.Second)
// Create table
err := client.CreateWidgetsTable(ctx)
if err != nil {
t.Fatalf("CreateWidgetsTable failed: %v", err)
}
// Verify table exists and has correct schema
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String("Widgets"),
})
if err != nil {
t.Fatalf("DescribeTable failed: %v", err)
}
// Verify composite key schema
if len(output.Table.KeySchema) != 2 {
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
}
if *output.Table.KeySchema[0].AttributeName != "page_id" {
t.Errorf("Expected partition key 'page_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
}
if *output.Table.KeySchema[1].AttributeName != "widget_id" {
t.Errorf("Expected sort key 'widget_id', got '%s'", *output.Table.KeySchema[1].AttributeName)
}
}
// TestCreateBookmarksTable tests the Bookmarks table creation with GSI
func TestCreateBookmarksTable(t *testing.T) {
client, ctx := setupTestClient(t)
// Delete table if it exists
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
TableName: aws.String("Bookmarks"),
})
time.Sleep(1 * time.Second)
// Create table
err := client.CreateBookmarksTable(ctx)
if err != nil {
t.Fatalf("CreateBookmarksTable failed: %v", err)
}
// Verify table exists and has correct schema
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String("Bookmarks"),
})
if err != nil {
t.Fatalf("DescribeTable failed: %v", err)
}
// Verify composite key schema
if len(output.Table.KeySchema) != 2 {
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
}
if *output.Table.KeySchema[0].AttributeName != "widget_id" {
t.Errorf("Expected partition key 'widget_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
}
if *output.Table.KeySchema[1].AttributeName != "bookmark_id" {
t.Errorf("Expected sort key 'bookmark_id', got '%s'", *output.Table.KeySchema[1].AttributeName)
}
// Verify GSI exists
if len(output.Table.GlobalSecondaryIndexes) != 1 {
t.Fatalf("Expected 1 GSI, got %d", len(output.Table.GlobalSecondaryIndexes))
}
gsi := output.Table.GlobalSecondaryIndexes[0]
if *gsi.IndexName != "UserBookmarksIndex" {
t.Errorf("Expected GSI name 'UserBookmarksIndex', got '%s'", *gsi.IndexName)
}
if len(gsi.KeySchema) != 2 {
t.Fatalf("Expected 2 GSI keys, got %d", len(gsi.KeySchema))
}
if *gsi.KeySchema[0].AttributeName != "user_id" {
t.Errorf("Expected GSI partition key 'user_id', got '%s'", *gsi.KeySchema[0].AttributeName)
}
if *gsi.KeySchema[1].AttributeName != "created_at" {
t.Errorf("Expected GSI sort key 'created_at', got '%s'", *gsi.KeySchema[1].AttributeName)
}
}
// TestCreateNotesTable tests the Notes table creation with GSI
func TestCreateNotesTable(t *testing.T) {
client, ctx := setupTestClient(t)
// Delete table if it exists
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
TableName: aws.String("Notes"),
})
time.Sleep(1 * time.Second)
// Create table
err := client.CreateNotesTable(ctx)
if err != nil {
t.Fatalf("CreateNotesTable failed: %v", err)
}
// Verify table exists and has correct schema
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String("Notes"),
})
if err != nil {
t.Fatalf("DescribeTable failed: %v", err)
}
// Verify composite key schema
if len(output.Table.KeySchema) != 2 {
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
}
if *output.Table.KeySchema[0].AttributeName != "widget_id" {
t.Errorf("Expected partition key 'widget_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
}
if *output.Table.KeySchema[1].AttributeName != "note_id" {
t.Errorf("Expected sort key 'note_id', got '%s'", *output.Table.KeySchema[1].AttributeName)
}
// Verify GSI exists
if len(output.Table.GlobalSecondaryIndexes) != 1 {
t.Fatalf("Expected 1 GSI, got %d", len(output.Table.GlobalSecondaryIndexes))
}
gsi := output.Table.GlobalSecondaryIndexes[0]
if *gsi.IndexName != "UserNotesIndex" {
t.Errorf("Expected GSI name 'UserNotesIndex', got '%s'", *gsi.IndexName)
}
}
// TestCreateTagAssociationsTable tests the TagAssociations table creation with GSI
func TestCreateTagAssociationsTable(t *testing.T) {
client, ctx := setupTestClient(t)
// Delete table if it exists
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
TableName: aws.String("TagAssociations"),
})
time.Sleep(1 * time.Second)
// Create table
err := client.CreateTagAssociationsTable(ctx)
if err != nil {
t.Fatalf("CreateTagAssociationsTable failed: %v", err)
}
// Verify table exists and has correct schema
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String("TagAssociations"),
})
if err != nil {
t.Fatalf("DescribeTable failed: %v", err)
}
// Verify composite key schema
if len(output.Table.KeySchema) != 2 {
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
}
if *output.Table.KeySchema[0].AttributeName != "item_id" {
t.Errorf("Expected partition key 'item_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
}
if *output.Table.KeySchema[1].AttributeName != "tag_name" {
t.Errorf("Expected sort key 'tag_name', got '%s'", *output.Table.KeySchema[1].AttributeName)
}
// Verify GSI exists
if len(output.Table.GlobalSecondaryIndexes) != 1 {
t.Fatalf("Expected 1 GSI, got %d", len(output.Table.GlobalSecondaryIndexes))
}
gsi := output.Table.GlobalSecondaryIndexes[0]
if *gsi.IndexName != "TagItemsIndex" {
t.Errorf("Expected GSI name 'TagItemsIndex', got '%s'", *gsi.IndexName)
}
if len(gsi.KeySchema) != 2 {
t.Fatalf("Expected 2 GSI keys, got %d", len(gsi.KeySchema))
}
if *gsi.KeySchema[0].AttributeName != "tag_name" {
t.Errorf("Expected GSI partition key 'tag_name', got '%s'", *gsi.KeySchema[0].AttributeName)
}
if *gsi.KeySchema[1].AttributeName != "user_id" {
t.Errorf("Expected GSI sort key 'user_id', got '%s'", *gsi.KeySchema[1].AttributeName)
}
// Verify projection type is INCLUDE
if gsi.Projection.ProjectionType != types.ProjectionTypeInclude {
t.Errorf("Expected projection type INCLUDE, got %v", gsi.Projection.ProjectionType)
}
}
// TestCreateGroupsTable tests the Groups table creation
func TestCreateGroupsTable(t *testing.T) {
client, ctx := setupTestClient(t)
// Delete table if it exists
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
TableName: aws.String("Groups"),
})
time.Sleep(1 * time.Second)
// Create table
err := client.CreateGroupsTable(ctx)
if err != nil {
t.Fatalf("CreateGroupsTable failed: %v", err)
}
// Verify table exists and has correct schema
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String("Groups"),
})
if err != nil {
t.Fatalf("DescribeTable failed: %v", err)
}
// Verify composite key schema
if len(output.Table.KeySchema) != 2 {
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
}
if *output.Table.KeySchema[0].AttributeName != "widget_id" {
t.Errorf("Expected partition key 'widget_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
}
if *output.Table.KeySchema[1].AttributeName != "group_id" {
t.Errorf("Expected sort key 'group_id', got '%s'", *output.Table.KeySchema[1].AttributeName)
}
}
// TestCreateSharedItemsTable tests the SharedItems table creation with GSI
func TestCreateSharedItemsTable(t *testing.T) {
client, ctx := setupTestClient(t)
// Delete table if it exists
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
TableName: aws.String("SharedItems"),
})
time.Sleep(1 * time.Second)
// Create table
err := client.CreateSharedItemsTable(ctx)
if err != nil {
t.Fatalf("CreateSharedItemsTable failed: %v", err)
}
// Verify table exists and has correct schema
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String("SharedItems"),
})
if err != nil {
t.Fatalf("DescribeTable failed: %v", err)
}
// Verify key schema
if len(output.Table.KeySchema) != 1 {
t.Fatalf("Expected 1 key, got %d", len(output.Table.KeySchema))
}
if *output.Table.KeySchema[0].AttributeName != "share_id" {
t.Errorf("Expected partition key 'share_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
}
// Verify GSI exists
if len(output.Table.GlobalSecondaryIndexes) != 1 {
t.Fatalf("Expected 1 GSI, got %d", len(output.Table.GlobalSecondaryIndexes))
}
gsi := output.Table.GlobalSecondaryIndexes[0]
if *gsi.IndexName != "UserSharesIndex" {
t.Errorf("Expected GSI name 'UserSharesIndex', got '%s'", *gsi.IndexName)
}
if len(gsi.KeySchema) != 2 {
t.Fatalf("Expected 2 GSI keys, got %d", len(gsi.KeySchema))
}
if *gsi.KeySchema[0].AttributeName != "user_id" {
t.Errorf("Expected GSI partition key 'user_id', got '%s'", *gsi.KeySchema[0].AttributeName)
}
if *gsi.KeySchema[1].AttributeName != "created_at" {
t.Errorf("Expected GSI sort key 'created_at', got '%s'", *gsi.KeySchema[1].AttributeName)
}
}
// TestCreatePreferencesTable tests the Preferences table creation
func TestCreatePreferencesTable(t *testing.T) {
client, ctx := setupTestClient(t)
// Delete table if it exists
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
TableName: aws.String("Preferences"),
})
time.Sleep(1 * time.Second)
// Create table
err := client.CreatePreferencesTable(ctx)
if err != nil {
t.Fatalf("CreatePreferencesTable failed: %v", err)
}
// Verify table exists and has correct schema
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String("Preferences"),
})
if err != nil {
t.Fatalf("DescribeTable failed: %v", err)
}
// Verify key schema
if len(output.Table.KeySchema) != 1 {
t.Fatalf("Expected 1 key, got %d", len(output.Table.KeySchema))
}
if *output.Table.KeySchema[0].AttributeName != "user_id" {
t.Errorf("Expected partition key 'user_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
}
}
// TestCreateAllTables tests creating all tables at once
func TestCreateAllTables(t *testing.T) {
client, ctx := setupTestClient(t)
// Delete all tables if they exist
tables := []string{
"Users", "Pages", "Widgets", "Bookmarks", "Notes",
"TagAssociations", "Groups", "SharedItems", "Preferences",
}
for _, table := range tables {
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
TableName: aws.String(table),
})
}
time.Sleep(2 * time.Second)
// Create all tables
createFuncs := []struct {
name string
create func(context.Context) error
}{
{"Users", client.CreateUsersTable},
{"Pages", client.CreatePagesTable},
{"Widgets", client.CreateWidgetsTable},
{"Bookmarks", client.CreateBookmarksTable},
{"Notes", client.CreateNotesTable},
{"TagAssociations", client.CreateTagAssociationsTable},
{"Groups", client.CreateGroupsTable},
{"SharedItems", client.CreateSharedItemsTable},
{"Preferences", client.CreatePreferencesTable},
}
for _, fn := range createFuncs {
err := fn.create(ctx)
if err != nil {
t.Fatalf("Failed to create %s table: %v", fn.name, err)
}
}
// Verify all tables exist
listOutput, err := client.client.ListTables(ctx, &dynamodb.ListTablesInput{})
if err != nil {
t.Fatalf("ListTables failed: %v", err)
}
tableMap := make(map[string]bool)
for _, tableName := range listOutput.TableNames {
tableMap[tableName] = true
}
for _, table := range tables {
if !tableMap[table] {
t.Errorf("Expected table %s to exist", table)
}
}
}