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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user