[Spec Kit] Initial commit — constitution, spec, plan, and tasks for Reaction Image Board v1
This commit is contained in:
151
specs/001-reaction-image-board/data-model.md
Normal file
151
specs/001-reaction-image-board/data-model.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Data Model: Reaction Image Board v1
|
||||
|
||||
**Date**: 2026-05-02
|
||||
|
||||
---
|
||||
|
||||
## Entities
|
||||
|
||||
### Image
|
||||
|
||||
Represents a single uploaded image file.
|
||||
|
||||
| Field | Type | Constraints | Notes |
|
||||
|---|---|---|---|
|
||||
| `id` | UUID | PK, not null | Generated on insert |
|
||||
| `hash` | VARCHAR(64) | UNIQUE, not null | SHA-256 hex digest of file bytes |
|
||||
| `filename` | VARCHAR | not null | Original filename; display only |
|
||||
| `mime_type` | VARCHAR(20) | not null | `image/jpeg`, `image/png`, `image/gif`, `image/webp` |
|
||||
| `size_bytes` | BIGINT | not null, > 0 | File size |
|
||||
| `width` | INTEGER | not null, > 0 | Pixel width |
|
||||
| `height` | INTEGER | not null, > 0 | Pixel height |
|
||||
| `storage_key` | VARCHAR(64) | not null | S3 object key; equals `hash` in v1 |
|
||||
| `created_at` | TIMESTAMPTZ | not null, default now() | Set on insert; never updated |
|
||||
|
||||
**Indexes**:
|
||||
- `images_hash_idx` UNIQUE on `hash` — supports fast duplicate detection
|
||||
|
||||
**Validation rules**:
|
||||
- `mime_type` MUST be one of: `image/jpeg`, `image/png`, `image/gif`, `image/webp`
|
||||
- `size_bytes` MUST be > 0 and ≤ `MAX_UPLOAD_BYTES` (default 52 428 800)
|
||||
- `hash` MUST be a 64-character lowercase hex string (SHA-256)
|
||||
|
||||
---
|
||||
|
||||
### Tag
|
||||
|
||||
Represents a single normalised tag string.
|
||||
|
||||
| Field | Type | Constraints | Notes |
|
||||
|---|---|---|---|
|
||||
| `id` | UUID | PK, not null | Generated on insert |
|
||||
| `name` | VARCHAR(64) | UNIQUE, not null | Normalised: lowercase, trimmed |
|
||||
| `created_at` | TIMESTAMPTZ | not null, default now() | Set on insert |
|
||||
|
||||
**Indexes**:
|
||||
- `tags_name_idx` UNIQUE on `name` — supports upsert by name
|
||||
- `tags_name_prefix_idx` on `name` with `varchar_pattern_ops` — supports
|
||||
prefix search (`LIKE 'prefix%'`)
|
||||
|
||||
**Validation rules**:
|
||||
- `name` MUST match `^[a-z0-9_-]{1,64}$` (after normalisation)
|
||||
- Normalisation applied before validation: lowercase + whitespace trim
|
||||
|
||||
**Lifecycle**:
|
||||
- Tags are created implicitly on first use; no explicit creation endpoint
|
||||
- Tag records are never deleted even when all image associations are removed
|
||||
|
||||
---
|
||||
|
||||
### ImageTag (join)
|
||||
|
||||
Many-to-many association between Image and Tag.
|
||||
|
||||
| Field | Type | Constraints | Notes |
|
||||
|---|---|---|---|
|
||||
| `image_id` | UUID | FK → images.id ON DELETE CASCADE | |
|
||||
| `tag_id` | UUID | FK → tags.id ON DELETE RESTRICT | |
|
||||
|
||||
**Primary key**: composite `(image_id, tag_id)`
|
||||
|
||||
**Notes**:
|
||||
- Deleting an image cascades to all its ImageTag rows
|
||||
- Deleting a tag is RESTRICT (not permitted while image associations exist)
|
||||
- In practice, tags are never deleted in v1 so RESTRICT is never triggered
|
||||
|
||||
---
|
||||
|
||||
## Relationships
|
||||
|
||||
```
|
||||
Image ──< ImageTag >── Tag
|
||||
(1) (M:M) (1)
|
||||
```
|
||||
|
||||
- One Image has zero or more Tags (through ImageTag)
|
||||
- One Tag is applied to zero or more Images (through ImageTag)
|
||||
|
||||
---
|
||||
|
||||
## State Transitions
|
||||
|
||||
### Image lifecycle
|
||||
|
||||
```
|
||||
[upload received]
|
||||
│
|
||||
▼
|
||||
Validate MIME + size
|
||||
│
|
||||
Compute SHA-256
|
||||
│
|
||||
Existing hash? ──yes──► return existing record (duplicate: true)
|
||||
│
|
||||
no
|
||||
│
|
||||
Write to S3
|
||||
│
|
||||
Insert images row
|
||||
│
|
||||
Upsert tags + insert image_tag rows
|
||||
│
|
||||
Return new record (duplicate: false)
|
||||
│
|
||||
[user deletes image]
|
||||
│
|
||||
Delete image_tag rows (cascade)
|
||||
Delete images row
|
||||
Delete S3 object
|
||||
│
|
||||
[gone]
|
||||
```
|
||||
|
||||
### Tag set update (PATCH)
|
||||
|
||||
```
|
||||
[PATCH /api/v1/images/{id}/tags with new_tags=[...]]
|
||||
│
|
||||
Validate each tag name (post-normalisation)
|
||||
│
|
||||
Fetch current tag set for image
|
||||
│
|
||||
Compute removed = current \ new_tags
|
||||
Compute added = new_tags \ current
|
||||
│
|
||||
Delete ImageTag rows for removed tags
|
||||
Upsert Tag records for added tags
|
||||
Insert ImageTag rows for added tags
|
||||
│
|
||||
Return full updated image record
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Migration Strategy
|
||||
|
||||
- Alembic manages all schema changes
|
||||
- Migration files are committed to `api/alembic/versions/`
|
||||
- Schema is applied on API startup (`alembic upgrade head`)
|
||||
- M0: initial empty migration (no tables)
|
||||
- M1: `images` table
|
||||
- M2: `tags` table + `image_tags` table + indexes
|
||||
Reference in New Issue
Block a user