611 lines
19 KiB
Markdown
611 lines
19 KiB
Markdown
# Data Model: Reference Board Viewer
|
|
|
|
**Created:** 2025-11-02
|
|
**Status:** Active
|
|
**Version:** 1.0.0
|
|
|
|
## Overview
|
|
|
|
This document defines the data model for the Reference Board Viewer application, including entities, relationships, validation rules, and state transitions.
|
|
|
|
---
|
|
|
|
## Entity Relationship Diagram
|
|
|
|
```
|
|
┌─────────┐ ┌──────────┐ ┌────────────┐
|
|
│ User │────1:N──│ Board │────M:N──│ Image │
|
|
└─────────┘ └──────────┘ └────────────┘
|
|
│ │
|
|
│ │
|
|
1:N 1:N
|
|
│ │
|
|
┌──────────┐ ┌─────────────┐
|
|
│ Group │ │ BoardImage │
|
|
└──────────┘ └─────────────┘
|
|
│
|
|
│
|
|
┌─────────────┐
|
|
│ ShareLink │
|
|
└─────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Core Entities
|
|
|
|
### User
|
|
|
|
**Purpose:** Represents an authenticated user of the system
|
|
|
|
**Fields:**
|
|
| Field | Type | Constraints | Description |
|
|
|-------|------|-------------|-------------|
|
|
| id | UUID | PK, NOT NULL | Unique identifier |
|
|
| email | VARCHAR(255) | UNIQUE, NOT NULL | User email (login) |
|
|
| password_hash | VARCHAR(255) | NOT NULL | Bcrypt hashed password |
|
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Account creation time |
|
|
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Last update time |
|
|
| is_active | BOOLEAN | NOT NULL, DEFAULT TRUE | Account active status |
|
|
|
|
**Validation Rules:**
|
|
- Email must be valid format (RFC 5322)
|
|
- Email must be lowercase
|
|
- Password minimum 8 characters before hashing
|
|
- Password must contain: 1 uppercase, 1 lowercase, 1 number
|
|
|
|
**Indexes:**
|
|
- PRIMARY KEY (id)
|
|
- UNIQUE INDEX (email)
|
|
- INDEX (created_at)
|
|
|
|
**Relationships:**
|
|
- User → Board (1:N)
|
|
- User → Image (1:N, images they own)
|
|
|
|
---
|
|
|
|
### Board
|
|
|
|
**Purpose:** Represents a reference board (canvas) containing images
|
|
|
|
**Fields:**
|
|
| Field | Type | Constraints | Description |
|
|
|-------|------|-------------|-------------|
|
|
| id | UUID | PK, NOT NULL | Unique identifier |
|
|
| user_id | UUID | FK(users.id), NOT NULL | Owner reference |
|
|
| title | VARCHAR(255) | NOT NULL | Board title |
|
|
| description | TEXT | NULL | Optional description |
|
|
| viewport_state | JSONB | NOT NULL | Canvas viewport (zoom, pan, rotation) |
|
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Creation time |
|
|
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Last modification |
|
|
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | Soft delete flag |
|
|
|
|
**Validation Rules:**
|
|
- Title: 1-255 characters, non-empty
|
|
- viewport_state must contain: `{x: number, y: number, zoom: number, rotation: number}`
|
|
- Zoom: 0.1 to 5.0
|
|
- Rotation: 0 to 360 degrees
|
|
|
|
**Indexes:**
|
|
- PRIMARY KEY (id)
|
|
- INDEX (user_id, created_at)
|
|
- INDEX (updated_at)
|
|
- GIN INDEX (viewport_state) - for JSONB queries
|
|
|
|
**Relationships:**
|
|
- Board → User (N:1)
|
|
- Board → BoardImage (1:N)
|
|
- Board → Group (1:N)
|
|
- Board → ShareLink (1:N)
|
|
|
|
**Example viewport_state:**
|
|
```json
|
|
{
|
|
"x": 0,
|
|
"y": 0,
|
|
"zoom": 1.0,
|
|
"rotation": 0
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Image
|
|
|
|
**Purpose:** Represents an uploaded image file
|
|
|
|
**Fields:**
|
|
| Field | Type | Constraints | Description |
|
|
|-------|------|-------------|-------------|
|
|
| id | UUID | PK, NOT NULL | Unique identifier |
|
|
| user_id | UUID | FK(users.id), NOT NULL | Owner reference |
|
|
| filename | VARCHAR(255) | NOT NULL | Original filename |
|
|
| storage_path | VARCHAR(512) | NOT NULL | Path in MinIO |
|
|
| file_size | BIGINT | NOT NULL | Size in bytes |
|
|
| mime_type | VARCHAR(100) | NOT NULL | MIME type (image/jpeg, etc) |
|
|
| width | INTEGER | NOT NULL | Original width in pixels |
|
|
| height | INTEGER | NOT NULL | Original height in pixels |
|
|
| metadata | JSONB | NOT NULL | Additional metadata |
|
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Upload time |
|
|
| reference_count | INTEGER | NOT NULL, DEFAULT 0 | How many boards use this |
|
|
|
|
**Validation Rules:**
|
|
- filename: non-empty, sanitized (no path traversal)
|
|
- file_size: 1 byte to 50MB (52,428,800 bytes)
|
|
- mime_type: must be in allowed list (image/jpeg, image/png, image/gif, image/webp, image/svg+xml)
|
|
- width, height: 1 to 10,000 pixels
|
|
- metadata must contain: `{format: string, exif?: object, checksum: string}`
|
|
|
|
**Indexes:**
|
|
- PRIMARY KEY (id)
|
|
- INDEX (user_id, created_at)
|
|
- INDEX (filename)
|
|
- GIN INDEX (metadata)
|
|
|
|
**Relationships:**
|
|
- Image → User (N:1)
|
|
- Image → BoardImage (1:N)
|
|
|
|
**Example metadata:**
|
|
```json
|
|
{
|
|
"format": "jpeg",
|
|
"exif": {
|
|
"DateTimeOriginal": "2025:11:02 12:00:00",
|
|
"Model": "Camera Model"
|
|
},
|
|
"checksum": "sha256:abc123...",
|
|
"thumbnails": {
|
|
"low": "/thumbnails/low/abc123.webp",
|
|
"medium": "/thumbnails/medium/abc123.webp",
|
|
"high": "/thumbnails/high/abc123.webp"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### BoardImage
|
|
|
|
**Purpose:** Junction table connecting boards and images with position/transformation data
|
|
|
|
**Fields:**
|
|
| Field | Type | Constraints | Description |
|
|
|-------|------|-------------|-------------|
|
|
| id | UUID | PK, NOT NULL | Unique identifier |
|
|
| board_id | UUID | FK(boards.id), NOT NULL | Board reference |
|
|
| image_id | UUID | FK(images.id), NOT NULL | Image reference |
|
|
| position | JSONB | NOT NULL | X, Y coordinates |
|
|
| transformations | JSONB | NOT NULL | Scale, rotation, crop, etc |
|
|
| z_order | INTEGER | NOT NULL | Layer order (higher = front) |
|
|
| group_id | UUID | FK(groups.id), NULL | Optional group membership |
|
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Added to board time |
|
|
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Last transformation time |
|
|
|
|
**Validation Rules:**
|
|
- position: `{x: number, y: number}` - no bounds (infinite canvas)
|
|
- transformations must contain: `{scale: number, rotation: number, opacity: number, flipped_h: bool, flipped_v: bool, crop?: object, greyscale: bool}`
|
|
- scale: 0.01 to 10.0
|
|
- rotation: 0 to 360 degrees
|
|
- opacity: 0.0 to 1.0
|
|
- z_order: 0 to 999999
|
|
- One image can appear on multiple boards (via different BoardImage records)
|
|
|
|
**Indexes:**
|
|
- PRIMARY KEY (id)
|
|
- UNIQUE INDEX (board_id, image_id) - prevent duplicates
|
|
- INDEX (board_id, z_order) - for layer sorting
|
|
- INDEX (group_id)
|
|
- GIN INDEX (position, transformations)
|
|
|
|
**Relationships:**
|
|
- BoardImage → Board (N:1)
|
|
- BoardImage → Image (N:1)
|
|
- BoardImage → Group (N:1, optional)
|
|
|
|
**Example position:**
|
|
```json
|
|
{
|
|
"x": 100,
|
|
"y": 250
|
|
}
|
|
```
|
|
|
|
**Example transformations:**
|
|
```json
|
|
{
|
|
"scale": 1.5,
|
|
"rotation": 45,
|
|
"opacity": 0.8,
|
|
"flipped_h": false,
|
|
"flipped_v": false,
|
|
"crop": {
|
|
"x": 10,
|
|
"y": 10,
|
|
"width": 200,
|
|
"height": 200
|
|
},
|
|
"greyscale": false
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Group
|
|
|
|
**Purpose:** Groups of images with shared annotation and color label
|
|
|
|
**Fields:**
|
|
| Field | Type | Constraints | Description |
|
|
|-------|------|-------------|-------------|
|
|
| id | UUID | PK, NOT NULL | Unique identifier |
|
|
| board_id | UUID | FK(boards.id), NOT NULL | Board reference |
|
|
| name | VARCHAR(255) | NOT NULL | Group name |
|
|
| color | VARCHAR(7) | NOT NULL | Hex color (e.g., #FF5733) |
|
|
| annotation | TEXT | NULL | Optional text note |
|
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Creation time |
|
|
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Last update |
|
|
|
|
**Validation Rules:**
|
|
- name: 1-255 characters, non-empty
|
|
- color: must be valid hex color (#RRGGBB format)
|
|
- annotation: max 10,000 characters
|
|
|
|
**Indexes:**
|
|
- PRIMARY KEY (id)
|
|
- INDEX (board_id, created_at)
|
|
|
|
**Relationships:**
|
|
- Group → Board (N:1)
|
|
- Group → BoardImage (1:N)
|
|
|
|
---
|
|
|
|
### ShareLink
|
|
|
|
**Purpose:** Shareable links to boards with permission control
|
|
|
|
**Fields:**
|
|
| Field | Type | Constraints | Description |
|
|
|-------|------|-------------|-------------|
|
|
| id | UUID | PK, NOT NULL | Unique identifier |
|
|
| board_id | UUID | FK(boards.id), NOT NULL | Board reference |
|
|
| token | VARCHAR(64) | UNIQUE, NOT NULL | Secure random token |
|
|
| permission_level | VARCHAR(20) | NOT NULL | 'view-only' or 'view-comment' |
|
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Link creation time |
|
|
| expires_at | TIMESTAMP | NULL | Optional expiration |
|
|
| last_accessed_at | TIMESTAMP | NULL | Last time link was used |
|
|
| access_count | INTEGER | NOT NULL, DEFAULT 0 | Usage counter |
|
|
| is_revoked | BOOLEAN | NOT NULL, DEFAULT FALSE | Revocation flag |
|
|
|
|
**Validation Rules:**
|
|
- token: 64 character random string (URL-safe base64)
|
|
- permission_level: must be 'view-only' or 'view-comment'
|
|
- expires_at: if set, must be future date
|
|
- Access count incremented on each use
|
|
|
|
**Indexes:**
|
|
- PRIMARY KEY (id)
|
|
- UNIQUE INDEX (token)
|
|
- INDEX (board_id, is_revoked)
|
|
- INDEX (expires_at, is_revoked)
|
|
|
|
**Relationships:**
|
|
- ShareLink → Board (N:1)
|
|
|
|
**State Transitions:**
|
|
```
|
|
[Created] → [Active] → [Revoked]
|
|
↓
|
|
[Expired] (if expires_at set)
|
|
```
|
|
|
|
---
|
|
|
|
### Comment (for View+Comment links)
|
|
|
|
**Purpose:** Comments from viewers on shared boards
|
|
|
|
**Fields:**
|
|
| Field | Type | Constraints | Description |
|
|
|-------|------|-------------|-------------|
|
|
| id | UUID | PK, NOT NULL | Unique identifier |
|
|
| board_id | UUID | FK(boards.id), NOT NULL | Board reference |
|
|
| share_link_id | UUID | FK(share_links.id), NULL | Origin link (optional) |
|
|
| author_name | VARCHAR(100) | NOT NULL | Commenter name |
|
|
| content | TEXT | NOT NULL | Comment text |
|
|
| position | JSONB | NULL | Optional canvas position reference |
|
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | Comment time |
|
|
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | Soft delete |
|
|
|
|
**Validation Rules:**
|
|
- author_name: 1-100 characters, sanitized
|
|
- content: 1-5,000 characters, non-empty
|
|
- position: if set, `{x: number, y: number}`
|
|
|
|
**Indexes:**
|
|
- PRIMARY KEY (id)
|
|
- INDEX (board_id, created_at)
|
|
- INDEX (share_link_id)
|
|
|
|
**Relationships:**
|
|
- Comment → Board (N:1)
|
|
- Comment → ShareLink (N:1, optional)
|
|
|
|
---
|
|
|
|
## Database Schema SQL
|
|
|
|
### PostgreSQL Schema Creation
|
|
|
|
```sql
|
|
-- Enable UUID extension
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
|
-- Users table
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
email VARCHAR(255) UNIQUE NOT NULL CHECK (email = LOWER(email)),
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
|
);
|
|
|
|
CREATE INDEX idx_users_created_at ON users(created_at);
|
|
|
|
-- Boards table
|
|
CREATE TABLE boards (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
title VARCHAR(255) NOT NULL CHECK (LENGTH(title) > 0),
|
|
description TEXT,
|
|
viewport_state JSONB NOT NULL DEFAULT '{"x": 0, "y": 0, "zoom": 1.0, "rotation": 0}',
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
|
|
);
|
|
|
|
CREATE INDEX idx_boards_user_created ON boards(user_id, created_at);
|
|
CREATE INDEX idx_boards_updated ON boards(updated_at);
|
|
CREATE INDEX idx_boards_viewport ON boards USING GIN (viewport_state);
|
|
|
|
-- Images table
|
|
CREATE TABLE images (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
filename VARCHAR(255) NOT NULL,
|
|
storage_path VARCHAR(512) NOT NULL,
|
|
file_size BIGINT NOT NULL CHECK (file_size > 0 AND file_size <= 52428800),
|
|
mime_type VARCHAR(100) NOT NULL,
|
|
width INTEGER NOT NULL CHECK (width > 0 AND width <= 10000),
|
|
height INTEGER NOT NULL CHECK (height > 0 AND height <= 10000),
|
|
metadata JSONB NOT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
reference_count INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE INDEX idx_images_user_created ON images(user_id, created_at);
|
|
CREATE INDEX idx_images_filename ON images(filename);
|
|
CREATE INDEX idx_images_metadata ON images USING GIN (metadata);
|
|
|
|
-- Groups table
|
|
CREATE TABLE groups (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
board_id UUID NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
|
|
name VARCHAR(255) NOT NULL CHECK (LENGTH(name) > 0),
|
|
color VARCHAR(7) NOT NULL CHECK (color ~ '^#[0-9A-Fa-f]{6}$'),
|
|
annotation TEXT,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_groups_board_created ON groups(board_id, created_at);
|
|
|
|
-- BoardImages junction table
|
|
CREATE TABLE board_images (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
board_id UUID NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
|
|
image_id UUID NOT NULL REFERENCES images(id) ON DELETE CASCADE,
|
|
position JSONB NOT NULL,
|
|
transformations JSONB NOT NULL DEFAULT '{"scale": 1.0, "rotation": 0, "opacity": 1.0, "flipped_h": false, "flipped_v": false, "greyscale": false}',
|
|
z_order INTEGER NOT NULL DEFAULT 0,
|
|
group_id UUID REFERENCES groups(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
UNIQUE(board_id, image_id)
|
|
);
|
|
|
|
CREATE INDEX idx_board_images_board_z ON board_images(board_id, z_order);
|
|
CREATE INDEX idx_board_images_group ON board_images(group_id);
|
|
CREATE INDEX idx_board_images_position ON board_images USING GIN (position);
|
|
CREATE INDEX idx_board_images_transformations ON board_images USING GIN (transformations);
|
|
|
|
-- ShareLinks table
|
|
CREATE TABLE share_links (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
board_id UUID NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
|
|
token VARCHAR(64) UNIQUE NOT NULL,
|
|
permission_level VARCHAR(20) NOT NULL CHECK (permission_level IN ('view-only', 'view-comment')),
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
expires_at TIMESTAMP,
|
|
last_accessed_at TIMESTAMP,
|
|
access_count INTEGER NOT NULL DEFAULT 0,
|
|
is_revoked BOOLEAN NOT NULL DEFAULT FALSE
|
|
);
|
|
|
|
CREATE UNIQUE INDEX idx_share_links_token ON share_links(token);
|
|
CREATE INDEX idx_share_links_board_revoked ON share_links(board_id, is_revoked);
|
|
CREATE INDEX idx_share_links_expires_revoked ON share_links(expires_at, is_revoked);
|
|
|
|
-- Comments table
|
|
CREATE TABLE comments (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
board_id UUID NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
|
|
share_link_id UUID REFERENCES share_links(id) ON DELETE SET NULL,
|
|
author_name VARCHAR(100) NOT NULL,
|
|
content TEXT NOT NULL CHECK (LENGTH(content) > 0 AND LENGTH(content) <= 5000),
|
|
position JSONB,
|
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
|
|
);
|
|
|
|
CREATE INDEX idx_comments_board_created ON comments(board_id, created_at);
|
|
CREATE INDEX idx_comments_share_link ON comments(share_link_id);
|
|
|
|
-- Triggers for updated_at
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ language 'plpgsql';
|
|
|
|
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
CREATE TRIGGER update_boards_updated_at BEFORE UPDATE ON boards FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
CREATE TRIGGER update_groups_updated_at BEFORE UPDATE ON groups FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
CREATE TRIGGER update_board_images_updated_at BEFORE UPDATE ON board_images FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
```
|
|
|
|
---
|
|
|
|
## Migrations Strategy
|
|
|
|
**Tool:** Alembic (SQLAlchemy migration tool)
|
|
|
|
**Process:**
|
|
1. Initial migration creates all tables
|
|
2. Subsequent migrations track schema changes
|
|
3. All migrations tested in staging before production
|
|
4. Rollback scripts maintained for each migration
|
|
5. Migrations run automatically during deployment
|
|
|
|
**Naming Convention:**
|
|
```
|
|
YYYYMMDD_HHMMSS_descriptive_name.py
|
|
```
|
|
|
|
Example:
|
|
```
|
|
20251102_100000_initial_schema.py
|
|
20251110_140000_add_comments_table.py
|
|
```
|
|
|
|
---
|
|
|
|
## Data Integrity Rules
|
|
|
|
### Referential Integrity
|
|
- All foreign keys have ON DELETE CASCADE or SET NULL as appropriate
|
|
- No orphaned records allowed
|
|
|
|
### Business Rules
|
|
1. User must own board to modify it
|
|
2. Images can only be added to boards by board owner
|
|
3. Share links can only be created/revoked by board owner
|
|
4. Comments only allowed on boards with active View+Comment links
|
|
5. Soft deletes used for boards (is_deleted flag) to preserve history
|
|
6. Hard deletes for images only when reference_count = 0
|
|
|
|
### Validation
|
|
- All constraints enforced at database level
|
|
- Additional validation in application layer (Pydantic models)
|
|
- Client-side validation for UX (pre-submit checks)
|
|
|
|
---
|
|
|
|
## Query Patterns
|
|
|
|
### Common Queries
|
|
|
|
**1. Get user's boards (with image count):**
|
|
```sql
|
|
SELECT b.*, COUNT(bi.id) as image_count
|
|
FROM boards b
|
|
LEFT JOIN board_images bi ON b.id = bi.board_id
|
|
WHERE b.user_id = $1 AND b.is_deleted = FALSE
|
|
GROUP BY b.id
|
|
ORDER BY b.updated_at DESC;
|
|
```
|
|
|
|
**2. Get board with all images (sorted by Z-order):**
|
|
```sql
|
|
SELECT bi.*, i.*, bi.transformations, bi.position
|
|
FROM board_images bi
|
|
JOIN images i ON bi.image_id = i.id
|
|
WHERE bi.board_id = $1
|
|
ORDER BY bi.z_order ASC;
|
|
```
|
|
|
|
**3. Get groups with member count:**
|
|
```sql
|
|
SELECT g.*, COUNT(bi.id) as member_count
|
|
FROM groups g
|
|
LEFT JOIN board_images bi ON g.id = bi.group_id
|
|
WHERE g.board_id = $1
|
|
GROUP BY g.id
|
|
ORDER BY g.created_at DESC;
|
|
```
|
|
|
|
**4. Validate share link:**
|
|
```sql
|
|
SELECT sl.*, b.user_id as board_owner_id
|
|
FROM share_links sl
|
|
JOIN boards b ON sl.board_id = b.id
|
|
WHERE sl.token = $1
|
|
AND sl.is_revoked = FALSE
|
|
AND (sl.expires_at IS NULL OR sl.expires_at > NOW());
|
|
```
|
|
|
|
**5. Search user's image library:**
|
|
```sql
|
|
SELECT *
|
|
FROM images
|
|
WHERE user_id = $1
|
|
AND filename ILIKE $2
|
|
ORDER BY created_at DESC
|
|
LIMIT 50;
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Considerations
|
|
|
|
### Indexes
|
|
- All foreign keys indexed
|
|
- JSONB fields use GIN indexes for fast queries
|
|
- Compound indexes for common query patterns
|
|
|
|
### Optimization
|
|
- Pagination for large result sets (LIMIT/OFFSET)
|
|
- Connection pooling (SQLAlchemy default: 5-20 connections)
|
|
- Prepared statements for repeated queries
|
|
- JSONB queries optimized with proper indexing
|
|
|
|
### Monitoring
|
|
- Slow query log enabled (>100ms)
|
|
- Query explain plans reviewed regularly
|
|
- Database statistics collected (pg_stat_statements)
|
|
|
|
---
|
|
|
|
## Backup & Recovery
|
|
|
|
**Strategy:**
|
|
- Daily full backups (pg_dump)
|
|
- Point-in-time recovery enabled (WAL archiving)
|
|
- Retention: 30 days
|
|
- Test restores monthly
|
|
|
|
**Data Durability:**
|
|
- Database: PostgreSQL with WAL (99.99% durability)
|
|
- Images: MinIO with erasure coding (99.999% durability)
|
|
- Separate backup of both systems
|
|
|
|
---
|
|
|
|
This data model supports all 18 functional requirements and ensures data integrity, performance, and scalability.
|
|
|