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:
@@ -81,7 +81,7 @@ Each task references specific requirements from the requirements document and in
|
|||||||
- Implement batch operations (BatchGet, BatchWrite)
|
- Implement batch operations (BatchGet, BatchWrite)
|
||||||
- _Requirements: 8.1, 8.8_
|
- _Requirements: 8.1, 8.8_
|
||||||
|
|
||||||
- [~] 3.2 Define all data models
|
- [x] 3.2 Define all data models
|
||||||
- Create Page model struct
|
- Create Page model struct
|
||||||
- Create Widget model struct with type enum
|
- Create Widget model struct with type enum
|
||||||
- Create Bookmark model struct with version field
|
- Create Bookmark model struct with version field
|
||||||
@@ -92,7 +92,7 @@ Each task references specific requirements from the requirements document and in
|
|||||||
- Create Preferences model struct
|
- Create Preferences model struct
|
||||||
- _Requirements: 2.1, 4.2, 5.2, 8.3, 8.4, 8.5_
|
- _Requirements: 2.1, 4.2, 5.2, 8.3, 8.4, 8.5_
|
||||||
|
|
||||||
- [~] 3.3 Create DynamoDB table schemas
|
- [x] 3.3 Create DynamoDB table schemas
|
||||||
- Implement Pages table creation with composite key
|
- Implement Pages table creation with composite key
|
||||||
- Implement Widgets table creation
|
- Implement Widgets table creation
|
||||||
- Implement Bookmarks table creation with UserBookmarksIndex GSI
|
- Implement Bookmarks table creation with UserBookmarksIndex GSI
|
||||||
|
|||||||
@@ -22,12 +22,34 @@ func main() {
|
|||||||
log.Fatalf("Failed to create DynamoDB client: %v", err)
|
log.Fatalf("Failed to create DynamoDB client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Creating Users table...")
|
// Create all tables
|
||||||
if err := db.CreateUsersTable(ctx); err != nil {
|
tables := []struct {
|
||||||
log.Fatalf("Failed to create Users table: %v", err)
|
name string
|
||||||
|
create func(context.Context) error
|
||||||
|
}{
|
||||||
|
{"Users", db.CreateUsersTable},
|
||||||
|
{"Pages", db.CreatePagesTable},
|
||||||
|
{"Widgets", db.CreateWidgetsTable},
|
||||||
|
{"Bookmarks", db.CreateBookmarksTable},
|
||||||
|
{"Notes", db.CreateNotesTable},
|
||||||
|
{"TagAssociations", db.CreateTagAssociationsTable},
|
||||||
|
{"Groups", db.CreateGroupsTable},
|
||||||
|
{"SharedItems", db.CreateSharedItemsTable},
|
||||||
|
{"Preferences", db.CreatePreferencesTable},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
log.Printf("Creating %s table...", table.name)
|
||||||
|
if err := table.create(ctx); err != nil {
|
||||||
|
log.Fatalf("Failed to create %s table: %v", table.name, err)
|
||||||
|
}
|
||||||
|
log.Printf("✓ %s table created successfully", table.name)
|
||||||
}
|
}
|
||||||
log.Println("✓ Users table created successfully")
|
|
||||||
|
|
||||||
fmt.Println("\nDatabase initialization complete!")
|
fmt.Println("\nDatabase initialization complete!")
|
||||||
|
fmt.Println("All tables created:")
|
||||||
|
for _, table := range tables {
|
||||||
|
fmt.Printf(" - %s\n", table.name)
|
||||||
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|||||||
175
docs/task-3.3-implementation.md
Normal file
175
docs/task-3.3-implementation.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# Task 3.3: Create DynamoDB Table Schemas
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This task implements all DynamoDB table creation methods for the Custom Start Page application. The implementation follows the schema design specified in the design document and addresses Requirements 8.3, 8.4, 8.5, and 11.6.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Tables Created
|
||||||
|
|
||||||
|
1. **Users Table**
|
||||||
|
- Partition Key: `user_id` (String)
|
||||||
|
- No GSI
|
||||||
|
- Stores user authentication and profile data
|
||||||
|
|
||||||
|
2. **Pages Table**
|
||||||
|
- Partition Key: `user_id` (String)
|
||||||
|
- Sort Key: `page_id` (String)
|
||||||
|
- No GSI
|
||||||
|
- Stores page configurations for each user
|
||||||
|
|
||||||
|
3. **Widgets Table**
|
||||||
|
- Partition Key: `page_id` (String)
|
||||||
|
- Sort Key: `widget_id` (String)
|
||||||
|
- No GSI
|
||||||
|
- Stores widget configurations for each page
|
||||||
|
|
||||||
|
4. **Bookmarks Table**
|
||||||
|
- Partition Key: `widget_id` (String)
|
||||||
|
- Sort Key: `bookmark_id` (String)
|
||||||
|
- GSI: **UserBookmarksIndex**
|
||||||
|
- Partition Key: `user_id` (String)
|
||||||
|
- Sort Key: `created_at` (Number)
|
||||||
|
- Projection: ALL
|
||||||
|
- Stores bookmark data with support for cross-widget queries
|
||||||
|
|
||||||
|
5. **Notes Table**
|
||||||
|
- Partition Key: `widget_id` (String)
|
||||||
|
- Sort Key: `note_id` (String)
|
||||||
|
- GSI: **UserNotesIndex**
|
||||||
|
- Partition Key: `user_id` (String)
|
||||||
|
- Sort Key: `created_at` (Number)
|
||||||
|
- Projection: ALL
|
||||||
|
- Stores note content with rich format support
|
||||||
|
|
||||||
|
6. **TagAssociations Table**
|
||||||
|
- Partition Key: `item_id` (String)
|
||||||
|
- Sort Key: `tag_name` (String)
|
||||||
|
- GSI: **TagItemsIndex**
|
||||||
|
- Partition Key: `tag_name` (String)
|
||||||
|
- Sort Key: `user_id` (String)
|
||||||
|
- Projection: INCLUDE (item_id, item_type, created_at)
|
||||||
|
- Enables efficient tag-based filtering and queries
|
||||||
|
|
||||||
|
7. **Groups Table**
|
||||||
|
- Partition Key: `widget_id` (String)
|
||||||
|
- Sort Key: `group_id` (String)
|
||||||
|
- No GSI
|
||||||
|
- Stores bookmark group configurations
|
||||||
|
|
||||||
|
8. **SharedItems Table**
|
||||||
|
- Partition Key: `share_id` (String)
|
||||||
|
- GSI: **UserSharesIndex**
|
||||||
|
- Partition Key: `user_id` (String)
|
||||||
|
- Sort Key: `created_at` (Number)
|
||||||
|
- Projection: INCLUDE (share_id, item_id, item_type, access_count)
|
||||||
|
- Stores shareable links for bookmarks and notes
|
||||||
|
|
||||||
|
9. **Preferences Table**
|
||||||
|
- Partition Key: `user_id` (String)
|
||||||
|
- No GSI
|
||||||
|
- Stores user preferences and settings
|
||||||
|
|
||||||
|
### Key Design Features
|
||||||
|
|
||||||
|
- **Billing Mode**: All tables use Pay-Per-Request (on-demand) billing for cost efficiency
|
||||||
|
- **Idempotency**: All table creation methods check if the table exists before creating
|
||||||
|
- **Wait for Active**: All methods wait for tables to become active before returning
|
||||||
|
- **Error Handling**: Comprehensive error handling with descriptive error messages
|
||||||
|
- **Composite Keys**: Used where needed for efficient querying (Pages, Widgets, Bookmarks, Notes, etc.)
|
||||||
|
- **GSI Strategy**: Global Secondary Indexes enable efficient cross-partition queries for tags and user-scoped data
|
||||||
|
|
||||||
|
### Access Patterns Supported
|
||||||
|
|
||||||
|
1. Get all pages for a user (Query Pages by user_id)
|
||||||
|
2. Get all widgets for a page (Query Widgets by page_id)
|
||||||
|
3. Get all bookmarks for a widget (Query Bookmarks by widget_id)
|
||||||
|
4. Get all bookmarks for a user (Query UserBookmarksIndex by user_id)
|
||||||
|
5. Get all notes for a user (Query UserNotesIndex by user_id)
|
||||||
|
6. Get all items with a specific tag (Query TagItemsIndex by tag_name + user_id)
|
||||||
|
7. Get all tags for an item (Query TagAssociations by item_id)
|
||||||
|
8. Get all groups for a widget (Query Groups by widget_id)
|
||||||
|
9. Get shared item by share ID (GetItem SharedItems by share_id)
|
||||||
|
10. Get all shares for a user (Query UserSharesIndex by user_id)
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. **internal/storage/dynamodb.go**
|
||||||
|
- Added `CreatePagesTable()` method
|
||||||
|
- Added `CreateWidgetsTable()` method
|
||||||
|
- Added `CreateBookmarksTable()` method with UserBookmarksIndex GSI
|
||||||
|
- Added `CreateNotesTable()` method with UserNotesIndex GSI
|
||||||
|
- Added `CreateTagAssociationsTable()` method with TagItemsIndex GSI
|
||||||
|
- Added `CreateGroupsTable()` method
|
||||||
|
- Added `CreateSharedItemsTable()` method with UserSharesIndex GSI
|
||||||
|
- Added `CreatePreferencesTable()` method
|
||||||
|
|
||||||
|
2. **cmd/init-db/main.go**
|
||||||
|
- Updated to call all table creation methods
|
||||||
|
- Added loop to create all tables with status reporting
|
||||||
|
- Added summary output showing all created tables
|
||||||
|
|
||||||
|
3. **internal/storage/dynamodb_test.go**
|
||||||
|
- Added `TestCreateUsersTable()` - verifies Users table schema
|
||||||
|
- Added `TestCreatePagesTable()` - verifies composite key schema
|
||||||
|
- Added `TestCreateWidgetsTable()` - verifies composite key schema
|
||||||
|
- Added `TestCreateBookmarksTable()` - verifies GSI configuration
|
||||||
|
- Added `TestCreateNotesTable()` - verifies GSI configuration
|
||||||
|
- Added `TestCreateTagAssociationsTable()` - verifies GSI with INCLUDE projection
|
||||||
|
- Added `TestCreateGroupsTable()` - verifies composite key schema
|
||||||
|
- Added `TestCreateSharedItemsTable()` - verifies GSI configuration
|
||||||
|
- Added `TestCreatePreferencesTable()` - verifies simple key schema
|
||||||
|
- Added `TestCreateAllTables()` - integration test for all tables
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
All tests are designed to:
|
||||||
|
- Skip gracefully when DynamoDB local is not available
|
||||||
|
- Verify table existence and schema correctness
|
||||||
|
- Test idempotency (calling create twice should not error)
|
||||||
|
- Verify key schemas (partition and sort keys)
|
||||||
|
- Verify GSI configurations (key schema, projection type)
|
||||||
|
|
||||||
|
To run tests with DynamoDB local:
|
||||||
|
```bash
|
||||||
|
# Start DynamoDB local
|
||||||
|
make db-start
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Or run specific tests
|
||||||
|
/usr/local/go/bin/go test -v ./internal/storage -run TestCreate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To initialize all tables:
|
||||||
|
```bash
|
||||||
|
# Build the init-db command
|
||||||
|
/usr/local/go/bin/go build -o bin/init-db cmd/init-db/main.go
|
||||||
|
|
||||||
|
# Run against local DynamoDB
|
||||||
|
./bin/init-db -endpoint http://localhost:8000
|
||||||
|
|
||||||
|
# Run against AWS DynamoDB (uses default AWS credentials)
|
||||||
|
./bin/init-db -endpoint ""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements Validation
|
||||||
|
|
||||||
|
- ✅ **Requirement 8.3**: Storage service stores page configurations
|
||||||
|
- ✅ **Requirement 8.4**: Storage service stores widget configurations
|
||||||
|
- ✅ **Requirement 8.5**: Storage service stores user preferences
|
||||||
|
- ✅ **Requirement 11.6**: Data model and indexing strategy for tags defined
|
||||||
|
- TagAssociations table with TagItemsIndex GSI
|
||||||
|
- Supports one-to-many relationships
|
||||||
|
- Enables efficient tag-based queries
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
The table schemas are now ready for use. The next tasks will implement:
|
||||||
|
- Storage layer CRUD operations for each table
|
||||||
|
- Service layer business logic
|
||||||
|
- HTTP handlers for API endpoints
|
||||||
20
internal/models/bookmark.go
Normal file
20
internal/models/bookmark.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bookmark represents a link with title, URL, and optional grouping
|
||||||
|
type Bookmark struct {
|
||||||
|
ID string `dynamodbav:"bookmark_id" json:"id"`
|
||||||
|
WidgetID string `dynamodbav:"widget_id" json:"widget_id"`
|
||||||
|
UserID string `dynamodbav:"user_id" json:"user_id"`
|
||||||
|
Title string `dynamodbav:"title" json:"title"`
|
||||||
|
URL string `dynamodbav:"url" json:"url"`
|
||||||
|
GroupID *string `dynamodbav:"group_id,omitempty" json:"group_id,omitempty"`
|
||||||
|
Order int `dynamodbav:"order" json:"order"`
|
||||||
|
FaviconURL *string `dynamodbav:"favicon_url,omitempty" json:"favicon_url,omitempty"`
|
||||||
|
CreatedAt time.Time `dynamodbav:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `dynamodbav:"updated_at" json:"updated_at"`
|
||||||
|
Version int `dynamodbav:"version" json:"version"`
|
||||||
|
}
|
||||||
16
internal/models/group.go
Normal file
16
internal/models/group.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Group represents a named collection of bookmarks within a widget
|
||||||
|
type Group struct {
|
||||||
|
ID string `dynamodbav:"group_id" json:"id"`
|
||||||
|
WidgetID string `dynamodbav:"widget_id" json:"widget_id"`
|
||||||
|
UserID string `dynamodbav:"user_id" json:"user_id"`
|
||||||
|
Name string `dynamodbav:"name" json:"name"`
|
||||||
|
Order int `dynamodbav:"order" json:"order"`
|
||||||
|
CreatedAt time.Time `dynamodbav:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `dynamodbav:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
31
internal/models/note.go
Normal file
31
internal/models/note.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContentFormat represents the format mode of note content
|
||||||
|
type ContentFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContentFormatPlain ContentFormat = "plain"
|
||||||
|
ContentFormatRTF ContentFormat = "rtf"
|
||||||
|
ContentFormatCode ContentFormat = "code"
|
||||||
|
ContentFormatYAML ContentFormat = "yaml"
|
||||||
|
ContentFormatMarkdown ContentFormat = "markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Note represents a text note with rich content support
|
||||||
|
type Note struct {
|
||||||
|
ID string `dynamodbav:"note_id" json:"id"`
|
||||||
|
WidgetID string `dynamodbav:"widget_id" json:"widget_id"`
|
||||||
|
UserID string `dynamodbav:"user_id" json:"user_id"`
|
||||||
|
Title *string `dynamodbav:"title,omitempty" json:"title,omitempty"`
|
||||||
|
Content string `dynamodbav:"content" json:"content"`
|
||||||
|
ContentLocation string `dynamodbav:"content_location" json:"content_location"`
|
||||||
|
Format ContentFormat `dynamodbav:"format" json:"format"`
|
||||||
|
Language *string `dynamodbav:"language,omitempty" json:"language,omitempty"`
|
||||||
|
CreatedAt time.Time `dynamodbav:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `dynamodbav:"updated_at" json:"updated_at"`
|
||||||
|
Version int `dynamodbav:"version" json:"version"`
|
||||||
|
}
|
||||||
15
internal/models/page.go
Normal file
15
internal/models/page.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Page represents a tab-based container that holds multiple widgets
|
||||||
|
type Page struct {
|
||||||
|
ID string `dynamodbav:"page_id" json:"id"`
|
||||||
|
UserID string `dynamodbav:"user_id" json:"user_id"`
|
||||||
|
Name string `dynamodbav:"name" json:"name"`
|
||||||
|
Order int `dynamodbav:"order" json:"order"`
|
||||||
|
CreatedAt time.Time `dynamodbav:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `dynamodbav:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
22
internal/models/preferences.go
Normal file
22
internal/models/preferences.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SearchProvider represents the configured search engine
|
||||||
|
type SearchProvider string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SearchProviderGoogle SearchProvider = "google"
|
||||||
|
SearchProviderDuckDuckGo SearchProvider = "duckduckgo"
|
||||||
|
SearchProviderBing SearchProvider = "bing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Preferences represents user preferences and settings
|
||||||
|
type Preferences struct {
|
||||||
|
UserID string `dynamodbav:"user_id" json:"user_id"`
|
||||||
|
SearchProvider SearchProvider `dynamodbav:"search_provider" json:"search_provider"`
|
||||||
|
Theme *string `dynamodbav:"theme,omitempty" json:"theme,omitempty"`
|
||||||
|
UpdatedAt time.Time `dynamodbav:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
31
internal/models/share.go
Normal file
31
internal/models/share.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Share represents a shareable link for bookmarks or notes
|
||||||
|
type Share struct {
|
||||||
|
ID string `dynamodbav:"share_id" json:"id"`
|
||||||
|
UserID string `dynamodbav:"user_id" json:"user_id"`
|
||||||
|
ItemID string `dynamodbav:"item_id" json:"item_id"`
|
||||||
|
ItemType ItemType `dynamodbav:"item_type" json:"item_type"`
|
||||||
|
CreatedAt time.Time `dynamodbav:"created_at" json:"created_at"`
|
||||||
|
ExpiresAt *time.Time `dynamodbav:"expires_at,omitempty" json:"expires_at,omitempty"`
|
||||||
|
AccessCount int `dynamodbav:"access_count" json:"access_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShareLink represents a shareable link with full URL
|
||||||
|
type ShareLink struct {
|
||||||
|
ShareID string `json:"share_id"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharedItem represents the data returned when accessing a shared link
|
||||||
|
type SharedItem struct {
|
||||||
|
ItemType ItemType `json:"item_type"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
OwnerEmail *string `json:"owner_email,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
29
internal/models/tag.go
Normal file
29
internal/models/tag.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ItemType represents the type of item that can be tagged
|
||||||
|
type ItemType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ItemTypeBookmark ItemType = "bookmark"
|
||||||
|
ItemTypeNote ItemType = "note"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TagAssociation represents a many-to-many relationship between tags and items
|
||||||
|
type TagAssociation struct {
|
||||||
|
ItemID string `dynamodbav:"item_id" json:"item_id"`
|
||||||
|
TagName string `dynamodbav:"tag_name" json:"tag_name"`
|
||||||
|
UserID string `dynamodbav:"user_id" json:"user_id"`
|
||||||
|
ItemType ItemType `dynamodbav:"item_type" json:"item_type"`
|
||||||
|
CreatedAt time.Time `dynamodbav:"created_at" json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagInfo represents tag metadata with usage statistics
|
||||||
|
type TagInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
ItemTypes []ItemType `json:"item_types"`
|
||||||
|
}
|
||||||
40
internal/models/widget.go
Normal file
40
internal/models/widget.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WidgetType represents the type of widget
|
||||||
|
type WidgetType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WidgetTypeBookmark WidgetType = "bookmark"
|
||||||
|
WidgetTypeNotes WidgetType = "notes"
|
||||||
|
WidgetTypeWeather WidgetType = "weather"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Position represents the x,y coordinates of a widget
|
||||||
|
type Position struct {
|
||||||
|
X int `dynamodbav:"x" json:"x"`
|
||||||
|
Y int `dynamodbav:"y" json:"y"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size represents the width and height of a widget
|
||||||
|
type Size struct {
|
||||||
|
Width int `dynamodbav:"width" json:"width"`
|
||||||
|
Height int `dynamodbav:"height" json:"height"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widget represents a modular component that displays specific content
|
||||||
|
type Widget struct {
|
||||||
|
ID string `dynamodbav:"widget_id" json:"id"`
|
||||||
|
PageID string `dynamodbav:"page_id" json:"page_id"`
|
||||||
|
UserID string `dynamodbav:"user_id" json:"user_id"`
|
||||||
|
Type WidgetType `dynamodbav:"type" json:"type"`
|
||||||
|
Title *string `dynamodbav:"title,omitempty" json:"title,omitempty"`
|
||||||
|
Position Position `dynamodbav:"position" json:"position"`
|
||||||
|
Size Size `dynamodbav:"size" json:"size"`
|
||||||
|
Config map[string]interface{} `dynamodbav:"config" json:"config"`
|
||||||
|
CreatedAt time.Time `dynamodbav:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `dynamodbav:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
@@ -97,6 +97,516 @@ func (db *DynamoDBClient) CreateUsersTable(ctx context.Context) error {
|
|||||||
return nil
|
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
|
// GetClient returns the underlying DynamoDB client
|
||||||
func (db *DynamoDBClient) GetClient() *dynamodb.Client {
|
func (db *DynamoDBClient) GetClient() *dynamodb.Client {
|
||||||
return db.client
|
return db.client
|
||||||
|
|||||||
@@ -450,3 +450,449 @@ func deleteTestTable(t *testing.T, ctx context.Context, client *DynamoDBClient,
|
|||||||
t.Logf("Warning: Failed to delete test table: %v", err)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user