978 lines
37 KiB
Markdown
978 lines
37 KiB
Markdown
# Backend DB Models Module
|
|
|
|
## Overview
|
|
|
|
The DB Models module defines the Sequelize ORM models that map to PostgreSQL database tables. It provides the data layer for the Tour Builder Platform, handling entity definitions, relationships, validations, and lifecycle hooks.
|
|
|
|
**Location:** `backend/src/db/models/`
|
|
|
|
**Files:** 22 model-loader entries (20 models + loader + index bridge). During
|
|
the backend TS/ESM migration, model entries have a typed `.ts` source plus a
|
|
typed ESM source file. There is no model-level CommonJS compatibility facade.
|
|
|
|
| File | Model | Purpose | LOC |
|
|
|------|-------|---------|-----|
|
|
| `index.ts` | - | ESM entrypoint re-exporting `loader.ts` | 1 |
|
|
| `loader.ts` | - | Typed model registry and Sequelize initialization | 128 |
|
|
| `users.ts` + `.js` bridge | `users` | User accounts with authentication | 246 |
|
|
| `projects.ts` + `.js` bridge | `projects` | Virtual tour projects | 211 |
|
|
| `production_presentation_access.ts` + `.js` bridge | `production_presentation_access` | Customer grants for private production presentations | 67 |
|
|
| `tour_pages.ts` + `.js` bridge | `tour_pages` | Individual tour pages with UI schema | 131 |
|
|
| `assets.ts` + `.js` bridge | `assets` | Uploaded media files | 169 |
|
|
| `asset_variants.ts` + `.js` bridge | `asset_variants` | Asset size/format variants | 103 |
|
|
| `roles.ts` + `roles.js` bridge | `roles` | RBAC roles | 85 |
|
|
| `permissions.ts` + `permissions.js` bridge | `permissions` | RBAC permissions | 52 |
|
|
| `project_memberships.ts` + `.js` bridge | `project_memberships` | User-project access | 89 |
|
|
| `publish_events.ts` + `.js` bridge | `publish_events` | Publishing history | 148 |
|
|
| `pwa_caches.ts` + `.js` bridge | `pwa_caches` | PWA offline cache manifests | 84 |
|
|
| `access_logs.ts` + `.js` bridge | `access_logs` | Activity audit trail | 105 |
|
|
| `element_type_defaults.ts` + `.js` bridge | `element_type_defaults` | Global UI element defaults | 91 |
|
|
| `project_element_defaults.ts` + `.js` bridge | `project_element_defaults` | Project-specific element defaults | 101 |
|
|
| `project_audio_tracks.ts` + `.js` bridge | `project_audio_tracks` | Background audio tracks | 103 |
|
|
| `project_transition_settings.ts` + `.js` bridge | `project_transition_settings` | Environment-aware CSS transition settings | 95 |
|
|
| `global_transition_defaults.ts` + `.js` bridge | `global_transition_defaults` | Platform defaults for CSS page transitions | 65 |
|
|
| `global_ui_control_defaults.ts` + `.js` bridge | `global_ui_control_defaults` | Platform defaults for fullscreen, sound, and offline controls | 33 |
|
|
| `project_ui_control_settings.ts` + `.js` bridge | `project_ui_control_settings` | Project/environment overrides for global UI controls | 61 |
|
|
| `presigned_url_requests.ts` + `.js` bridge | `presigned_url_requests` | S3 presigned URL audit | 118 |
|
|
| `file.ts` + `.js` bridge | `file` | Generic file attachments | 53 |
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
### TS/ESM Migration Boundary
|
|
|
|
The model loader uses explicit typed ESM imports from `backend/src/db/models/`.
|
|
Migrated model definitions use this structure:
|
|
|
|
- `model-name.ts` contains the typed ESM model factory.
|
|
- `loader.ts` explicitly imports every model factory and builds the Sequelize
|
|
registry without dynamic CommonJS discovery.
|
|
- `index.ts` is the ESM entrypoint to `loader.ts` used by DB consumers.
|
|
- `backend/src/types/db-models.ts` keeps the service-specific overload facade
|
|
as reusable TypeScript contracts.
|
|
- Shared model factory contracts live in `backend/src/types/sequelize-models.ts`
|
|
and use official Sequelize types.
|
|
|
|
As of 2026-07-01, all model definitions and the model loader use typed ESM
|
|
source. `users.ts` keeps the hook payload typed through reusable
|
|
`UserModelInstance` and `UsersSequelizeModel` contracts without type
|
|
assertions.
|
|
|
|
`npm run check:esm-boundaries` guards this boundary: new `.js` source or
|
|
CommonJS syntax in `.ts` fails the check unless it is an explicitly documented
|
|
bridge or immutable migration.
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ Model Loading Flow │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
|
|
┌─────────────────┐
|
|
│ index.ts │
|
|
│ (entrypoint) │
|
|
└────────┬────────┘
|
|
│
|
|
│ 1. require('./loader.ts')
|
|
▼
|
|
┌─────────────────┐
|
|
│ loader.ts │
|
|
└────────┬────────┘
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|
│ Model Files │
|
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
|
|
│ │ users │ │projects │ │ assets │ │ roles │ │ tour_pages │ │
|
|
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └──────┬──────┘ │
|
|
│ │ │ │ │ │ │
|
|
│ └───────────┴───────────┴───────────┴─────────────┘ │
|
|
│ │ │
|
|
└───────────────────────────────┼──────────────────────────────────────┘
|
|
│
|
|
3. Call model.associate(db) for each model
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|
│ Associations Created │
|
|
│ │
|
|
│ users ─────────────┬─── belongsTo ───→ roles (app_role) │
|
|
│ ├─── hasMany ─────→ project_memberships │
|
|
│ ├─── hasMany ─────→ production_presentation_access│
|
|
│ ├─── hasMany ─────→ publish_events │
|
|
│ └─── belongsToMany → permissions (custom) │
|
|
│ │
|
|
│ projects ──────────┬─── hasMany ─────→ tour_pages │
|
|
│ ├─── hasMany ─────→ assets │
|
|
│ ├─── hasMany ─────→ project_memberships │
|
|
│ ├─── hasMany ─────→ production_presentation_access│
|
|
│ └─── hasMany ─────→ publish_events │
|
|
│ │
|
|
│ roles ─────────────┬─── belongsToMany → permissions │
|
|
│ └─── hasMany ─────→ users │
|
|
└─────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|
│ Exported db Object │
|
|
│ { │
|
|
│ users, projects, tour_pages, assets, roles, permissions, │
|
|
│ project_memberships, publish_events, pwa_caches, access_logs, │
|
|
│ element_type_defaults, project_element_defaults, ... │
|
|
│ sequelize, // Connection instance │
|
|
│ Sequelize // Sequelize library │
|
|
│ } │
|
|
└─────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Model Loader
|
|
|
|
**Locations:**
|
|
- `backend/src/db/models/loader.ts`
|
|
- `backend/src/db/models/index.ts`
|
|
- `backend/src/types/db-models.ts`
|
|
|
|
`loader.ts` initializes Sequelize, explicitly imports every model factory, and
|
|
calls `associate()` for models that define associations. `index.ts` re-exports
|
|
the typed registry, and `src/types/db-models.ts` provides the overload surface
|
|
for service-specific model calls.
|
|
|
|
---
|
|
|
|
## Database Configuration
|
|
|
|
**Location:** `backend/src/db/db-config.ts`
|
|
|
|
| Environment | Database | Logging | Notes |
|
|
|-------------|----------|---------|-------|
|
|
| `production` | From env vars | Disabled | Live production |
|
|
| `development` | `db_tour_builder_platform` | Console | Local dev |
|
|
| `dev_stage` | From env vars | Console | Staging server |
|
|
|
|
---
|
|
|
|
## Model Definitions
|
|
|
|
### Common Model Options
|
|
|
|
All models share these Sequelize options:
|
|
|
|
```javascript
|
|
{
|
|
timestamps: true, // Adds createdAt, updatedAt
|
|
paranoid: true, // Soft delete with deletedAt
|
|
freezeTableName: true, // Use exact model name as table name
|
|
}
|
|
```
|
|
|
|
### Common Fields
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `id` | `UUID` | Primary key (auto-generated UUIDv4) |
|
|
| `importHash` | `STRING(255)` | Unique hash for bulk import deduplication |
|
|
| `createdAt` | `DATE` | Auto-managed creation timestamp |
|
|
| `updatedAt` | `DATE` | Auto-managed update timestamp |
|
|
| `deletedAt` | `DATE` | Soft delete timestamp (paranoid mode) |
|
|
|
|
### Common Associations
|
|
|
|
Most models have these standard associations:
|
|
|
|
```javascript
|
|
// Audit trail associations
|
|
db.MODEL.belongsTo(db.users, { as: 'createdBy' });
|
|
db.MODEL.belongsTo(db.users, { as: 'updatedBy' });
|
|
```
|
|
|
|
---
|
|
|
|
## Core Models
|
|
|
|
### users
|
|
|
|
**Purpose:** User accounts for authentication and authorization.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `firstName` | TEXT | Yes | - | Trimmed |
|
|
| `lastName` | TEXT | Yes | - | Trimmed |
|
|
| `phoneNumber` | TEXT | Yes | - | - |
|
|
| `email` | TEXT | No | - | isEmail, notEmpty, unique |
|
|
| `password` | TEXT | No | - | Hashed with bcrypt |
|
|
| `disabled` | BOOLEAN | No | false | - |
|
|
| `emailVerified` | BOOLEAN | No | false | - |
|
|
| `emailVerificationToken` | TEXT | Yes | - | - |
|
|
| `emailVerificationTokenExpiresAt` | DATE | Yes | - | - |
|
|
| `passwordResetToken` | TEXT | Yes | - | - |
|
|
| `passwordResetTokenExpiresAt` | DATE | Yes | - | - |
|
|
| `provider` | TEXT | No | 'local' | OAuth provider |
|
|
| `app_roleId` | UUID | Yes | - | FK to roles |
|
|
|
|
**Indexes:**
|
|
- `email` (unique)
|
|
- `app_roleId`
|
|
- `deletedAt`
|
|
|
|
**Associations:**
|
|
```javascript
|
|
users.belongsTo(roles, { as: 'app_role' });
|
|
users.belongsToMany(permissions, { as: 'custom_permissions', through: 'usersCustom_permissionsPermissions' });
|
|
users.hasMany(project_memberships, { as: 'project_memberships_user' });
|
|
users.hasMany(presigned_url_requests, { as: 'presigned_url_requests_user' });
|
|
users.hasMany(publish_events, { as: 'publish_events_user' });
|
|
users.hasMany(access_logs, { as: 'access_logs_user' });
|
|
users.hasMany(file, { as: 'avatar', scope: { belongsTo: 'users', belongsToColumn: 'avatar' } });
|
|
```
|
|
|
|
**Hooks:**
|
|
```javascript
|
|
users.beforeCreate((user) => {
|
|
// Trim string fields
|
|
// Auto-verify email for OAuth providers
|
|
// Generate random password for OAuth if not provided
|
|
});
|
|
|
|
users.beforeUpdate((user) => {
|
|
// Trim string fields
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### projects
|
|
|
|
**Purpose:** Virtual tour projects container.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `name` | TEXT | No | - | notEmpty, len[1,255] |
|
|
| `slug` | TEXT | No | - | notEmpty, unique, alphanumeric + dashes/underscores |
|
|
| `description` | TEXT | Yes | - | - |
|
|
| `logo_url` | TEXT | Yes | - | - |
|
|
| `favicon_url` | TEXT | Yes | - | - |
|
|
| `og_image_url` | TEXT | Yes | - | - |
|
|
| `production_presentation_visibility` | ENUM | No | public | public, private |
|
|
|
|
**Indexes:**
|
|
- `slug` (unique)
|
|
- `deletedAt`
|
|
|
|
**Associations:**
|
|
```javascript
|
|
projects.hasMany(project_memberships, { as: 'project_memberships_project', onDelete: 'CASCADE' });
|
|
projects.hasMany(assets, { as: 'assets_project', onDelete: 'CASCADE' });
|
|
projects.hasMany(presigned_url_requests, { as: 'presigned_url_requests_project', onDelete: 'CASCADE' });
|
|
projects.hasMany(tour_pages, { as: 'tour_pages_project', onDelete: 'CASCADE' });
|
|
projects.hasMany(project_audio_tracks, { as: 'project_audio_tracks_project', onDelete: 'CASCADE' });
|
|
projects.hasMany(project_transition_settings, { as: 'project_transition_settings_project', onDelete: 'CASCADE' });
|
|
projects.hasMany(publish_events, { as: 'publish_events_project', onDelete: 'CASCADE' });
|
|
projects.hasMany(pwa_caches, { as: 'pwa_caches_project', onDelete: 'CASCADE' });
|
|
projects.hasMany(access_logs, { as: 'access_logs_project', onDelete: 'CASCADE' });
|
|
projects.hasMany(project_element_defaults, { as: 'project_element_defaults_project', onDelete: 'CASCADE' });
|
|
projects.hasMany(production_presentation_access, { as: 'production_presentation_access_project', onDelete: 'CASCADE' });
|
|
```
|
|
|
|
---
|
|
|
|
### production_presentation_access
|
|
|
|
**Purpose:** Grants Public-role customer users access to selected private
|
|
production presentations.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `projectId` | UUID | No | - | FK to projects |
|
|
| `userId` | UUID | No | - | FK to users |
|
|
| `createdById` | UUID | Yes | - | FK to users |
|
|
| `updatedById` | UUID | Yes | - | FK to users |
|
|
| `importHash` | STRING(255) | Yes | - | unique |
|
|
|
|
**Indexes:**
|
|
- `projectId`
|
|
- `userId`
|
|
- `projectId, userId` unique for active rows
|
|
|
|
**Associations:**
|
|
```javascript
|
|
production_presentation_access.belongsTo(projects, { as: 'project', onDelete: 'CASCADE' });
|
|
production_presentation_access.belongsTo(users, { as: 'user', onDelete: 'CASCADE' });
|
|
production_presentation_access.belongsTo(users, { as: 'createdBy', onDelete: 'SET NULL' });
|
|
production_presentation_access.belongsTo(users, { as: 'updatedBy', onDelete: 'SET NULL' });
|
|
```
|
|
|
|
---
|
|
|
|
### tour_pages
|
|
|
|
**Purpose:** Individual pages within a tour with UI elements schema.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `environment` | ENUM | No | 'dev' | dev, stage, production |
|
|
| `source_key` | TEXT | Yes | - | Original page ID for cloning |
|
|
| `name` | TEXT | No | - | notEmpty, len[1,255] |
|
|
| `slug` | TEXT | No | - | notEmpty, alphanumeric + dashes |
|
|
| `sort_order` | INTEGER | No | 0 | - |
|
|
| `background_image_url` | TEXT | Yes | - | - |
|
|
| `background_video_url` | TEXT | Yes | - | - |
|
|
| `background_audio_url` | TEXT | Yes | - | - |
|
|
| `background_loop` | BOOLEAN | No | false | - |
|
|
| `requires_auth` | BOOLEAN | No | false | - |
|
|
| `ui_schema_json` | JSON | Yes | - | Page elements, links, transitions |
|
|
| `projectId` | UUID | Yes | - | FK to projects |
|
|
|
|
**Indexes:**
|
|
- `projectId`
|
|
- `[projectId, environment, slug]` (unique) - Composite unique per project+environment
|
|
- `[projectId, environment, sort_order]` - For ordering queries
|
|
- `deletedAt`
|
|
|
|
**Note:** The `ui_schema_json` field stores all page elements (buttons, hotspots, galleries, tooltips, media players), navigation links, and transition configurations.
|
|
|
|
---
|
|
|
|
### roles
|
|
|
|
**Purpose:** RBAC role definitions.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `name` | TEXT | No | - | notEmpty, len[1,100] |
|
|
| `role_customization` | TEXT | Yes | - | Custom role metadata |
|
|
|
|
**Associations:**
|
|
```javascript
|
|
roles.belongsToMany(permissions, { as: 'permissions', through: 'rolesPermissionsPermissions' });
|
|
roles.hasMany(users, { as: 'users_app_role', onDelete: 'SET NULL' });
|
|
```
|
|
|
|
---
|
|
|
|
### permissions
|
|
|
|
**Purpose:** Individual permission definitions.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `name` | TEXT | No | - | notEmpty, unique, len[1,100] |
|
|
|
|
**Permission Naming Convention:** `{ACTION}_{ENTITY}` (e.g., `READ_USERS`, `CREATE_ASSETS`)
|
|
|
|
---
|
|
|
|
## Asset Models
|
|
|
|
### assets
|
|
|
|
**Purpose:** Uploaded media files (images, videos, audio, documents).
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `name` | TEXT | Yes | - | len[0,255] |
|
|
| `asset_type` | ENUM | No | - | image, video, audio, file |
|
|
| `type` | ENUM | No | 'general' | icon, background_image, audio, video, transition, logo, favicon, document, general |
|
|
| `cdn_url` | TEXT | Yes | - | - |
|
|
| `storage_key` | TEXT | Yes | - | S3/storage path |
|
|
| `mime_type` | TEXT | Yes | - | MIME type format |
|
|
| `size_mb` | DECIMAL | Yes | - | - |
|
|
| `width_px` | INTEGER | Yes | - | Image/video width |
|
|
| `height_px` | INTEGER | Yes | - | Image/video height |
|
|
| `duration_sec` | DECIMAL | Yes | - | Audio/video duration |
|
|
| `checksum` | TEXT | Yes | - | File hash |
|
|
| `is_public` | BOOLEAN | No | false | - |
|
|
| `projectId` | UUID | Yes | - | FK to projects |
|
|
|
|
**Indexes:**
|
|
- `projectId`
|
|
- `asset_type`
|
|
- `type`
|
|
- `is_public`
|
|
- `deletedAt`
|
|
|
|
**Associations:**
|
|
```javascript
|
|
assets.hasMany(asset_variants, { as: 'asset_variants_asset', onDelete: 'CASCADE' });
|
|
assets.belongsTo(projects, { as: 'project', onDelete: 'CASCADE' });
|
|
```
|
|
|
|
---
|
|
|
|
### asset_variants
|
|
|
|
**Purpose:** Optimized versions of assets (thumbnails, different formats).
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `variant_type` | ENUM | Yes | - | thumbnail, preview, webp, mp4_low, mp4_high, original |
|
|
| `cdn_url` | TEXT | Yes | - | len[0,2048], URL format |
|
|
| `width_px` | INTEGER | Yes | - | min: 0 |
|
|
| `height_px` | INTEGER | Yes | - | min: 0 |
|
|
| `size_mb` | DECIMAL | Yes | - | min: 0 |
|
|
| `assetId` | UUID | Yes | - | FK to assets |
|
|
|
|
**Associations:**
|
|
```javascript
|
|
asset_variants.belongsTo(assets, { as: 'asset', onDelete: 'CASCADE' });
|
|
```
|
|
|
|
---
|
|
|
|
## Element Defaults Models
|
|
|
|
### element_type_defaults
|
|
|
|
**Purpose:** Global platform-wide default settings for UI element types.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `element_type` | TEXT | No | - | notEmpty, unique, len[1,100] |
|
|
| `name` | TEXT | No | - | notEmpty, len[1,255] |
|
|
| `sort_order` | INTEGER | No | 0 | - |
|
|
| `is_active` | VIRTUAL | - | true | Always returns true |
|
|
| `default_settings_json` | TEXT | Yes | - | Mapped from `settings_json` column |
|
|
|
|
**Indexes:**
|
|
- `element_type`
|
|
- `sort_order`
|
|
- `deletedAt`
|
|
|
|
**Associations:**
|
|
```javascript
|
|
element_type_defaults.hasMany(project_element_defaults, {
|
|
as: 'project_defaults',
|
|
foreignKey: 'source_element_id',
|
|
onDelete: 'SET NULL'
|
|
});
|
|
```
|
|
|
|
**Element Types:** button, hotspot, gallery, tooltip, video_player, audio_player, image, text, link, form, iframe
|
|
|
|
---
|
|
|
|
### project_element_defaults
|
|
|
|
**Purpose:** Project-specific overrides for element defaults.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `element_type` | TEXT | No | - | notEmpty, len[1,100] |
|
|
| `name` | TEXT | Yes | - | len[0,255] |
|
|
| `sort_order` | INTEGER | No | 0 | - |
|
|
| `settings_json` | TEXT | Yes | - | Element configuration |
|
|
| `source_element_id` | UUID | Yes | - | FK to element_type_defaults |
|
|
| `snapshot_version` | INTEGER | No | 1 | Version tracking |
|
|
| `projectId` | UUID | No | - | FK to projects |
|
|
|
|
**Indexes:**
|
|
- `projectId`
|
|
- `[projectId, element_type]` (unique)
|
|
- `element_type`
|
|
- `source_element_id`
|
|
- `deletedAt`
|
|
|
|
**Associations:**
|
|
```javascript
|
|
project_element_defaults.belongsTo(projects, { as: 'project', onDelete: 'CASCADE' });
|
|
project_element_defaults.belongsTo(element_type_defaults, {
|
|
as: 'source_element',
|
|
onDelete: 'SET NULL'
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Publishing & Audit Models
|
|
|
|
### publish_events
|
|
|
|
**Purpose:** Track publishing actions between environments.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `title` | STRING | Yes | - | len[0,255] |
|
|
| `description` | TEXT | Yes | - | len[0,5000] |
|
|
| `from_environment` | ENUM | No | - | dev, stage, production |
|
|
| `to_environment` | ENUM | No | - | dev, stage, production |
|
|
| `started_at` | DATE | Yes | - | - |
|
|
| `finished_at` | DATE | Yes | - | - |
|
|
| `status` | ENUM | No | 'queued' | queued, running, success, failed |
|
|
| `error_message` | TEXT | Yes | - | - |
|
|
| `pages_copied` | INTEGER | Yes | - | min: 0 |
|
|
| `transitions_copied` | INTEGER | Yes | - | min: 0 |
|
|
| `audios_copied` | INTEGER | Yes | - | min: 0 |
|
|
| `projectId` | UUID | Yes | - | FK to projects |
|
|
| `userId` | UUID | Yes | - | FK to users |
|
|
|
|
**Indexes:**
|
|
- `projectId`
|
|
- `userId`
|
|
- `status`
|
|
- `started_at`
|
|
|
|
---
|
|
|
|
### access_logs
|
|
|
|
**Purpose:** Audit trail for user activity.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `environment` | ENUM | No | - | admin, stage, production |
|
|
| `path` | TEXT | Yes | - | len[0,2048] |
|
|
| `ip_address` | TEXT | Yes | - | len[0,45] (IPv6 max) |
|
|
| `user_agent` | TEXT | Yes | - | len[0,1024] |
|
|
| `accessed_at` | DATE | No | NOW | - |
|
|
| `projectId` | UUID | Yes | - | FK to projects |
|
|
| `userId` | UUID | Yes | - | FK to users |
|
|
|
|
**Indexes:**
|
|
- `projectId`
|
|
- `environment`
|
|
- `userId`
|
|
- `accessed_at`
|
|
|
|
---
|
|
|
|
## Supporting Models
|
|
|
|
### project_memberships
|
|
|
|
**Purpose:** User access to projects with role-based permissions.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `access_level` | ENUM | No | 'viewer' | owner, editor, reviewer, viewer |
|
|
| `is_active` | BOOLEAN | No | false | - |
|
|
| `invited_at` | DATE | Yes | - | - |
|
|
| `accepted_at` | DATE | Yes | - | - |
|
|
| `projectId` | UUID | Yes | - | FK to projects |
|
|
| `userId` | UUID | Yes | - | FK to users |
|
|
|
|
**Indexes:**
|
|
- `projectId`
|
|
- `userId`
|
|
- `[projectId, userId]` (unique) - One membership per user per project
|
|
- `is_active`
|
|
- `deletedAt`
|
|
|
|
---
|
|
|
|
### project_audio_tracks
|
|
|
|
**Purpose:** Background audio tracks for projects.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `environment` | ENUM | Yes | - | dev, stage, production |
|
|
| `source_key` | TEXT | Yes | - | Original track ID for cloning |
|
|
| `name` | TEXT | Yes | - | len[0,255] |
|
|
| `slug` | TEXT | Yes | - | - |
|
|
| `url` | TEXT | Yes | - | - |
|
|
| `loop` | BOOLEAN | No | false | - |
|
|
| `volume` | DECIMAL | Yes | - | min: 0, max: 1 |
|
|
| `sort_order` | INTEGER | Yes | - | - |
|
|
| `is_enabled` | BOOLEAN | No | false | - |
|
|
| `projectId` | UUID | Yes | - | FK to projects |
|
|
|
|
---
|
|
|
|
### project_transition_settings
|
|
|
|
**Purpose:** Environment-aware CSS transition settings for page navigation.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `environment` | ENUM | No | - | dev, stage, production |
|
|
| `source_key` | TEXT | Yes | - | Original settings ID for cloning |
|
|
| `transition_type` | TEXT | No | 'fade' | CSS transition type |
|
|
| `duration_ms` | INTEGER | No | 700 | Transition duration in ms |
|
|
| `easing` | TEXT | No | 'ease-in-out' | CSS easing function |
|
|
| `overlay_color` | TEXT | No | '#000000' | Transition overlay color |
|
|
| `projectId` | UUID | No | - | FK to projects |
|
|
| `createdById` | UUID | Yes | - | FK to users |
|
|
| `updatedById` | UUID | Yes | - | FK to users |
|
|
|
|
**Indexes:**
|
|
- `[projectId, environment]` (unique where deletedAt IS NULL)
|
|
- `projectId`
|
|
- `deletedAt`
|
|
|
|
**Associations:**
|
|
```javascript
|
|
project_transition_settings.belongsTo(projects, { as: 'project', onDelete: 'CASCADE' });
|
|
project_transition_settings.belongsTo(users, { as: 'createdBy' });
|
|
project_transition_settings.belongsTo(users, { as: 'updatedBy' });
|
|
```
|
|
|
|
**Publishing Integration:** Copied between environments during Save to Stage (dev → stage) and Publish (stage → production). Uses `source_key` to track lineage.
|
|
|
|
---
|
|
|
|
### pwa_caches
|
|
|
|
**Purpose:** PWA offline cache manifest tracking.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `environment` | ENUM | Yes | - | dev, stage, production |
|
|
| `cache_version` | TEXT | Yes | - | len[0,255] |
|
|
| `manifest_json` | JSON | Yes | - | PWA manifest |
|
|
| `asset_list_json` | JSON | Yes | - | Cached asset URLs |
|
|
| `generated_at` | DATE | Yes | - | - |
|
|
| `is_active` | BOOLEAN | No | false | - |
|
|
| `projectId` | UUID | Yes | - | FK to projects |
|
|
|
|
---
|
|
|
|
### presigned_url_requests
|
|
|
|
**Purpose:** Audit log for S3 presigned URL requests.
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `purpose` | ENUM | Yes | - | upload, download |
|
|
| `asset_type` | ENUM | Yes | - | image, video, audio, file |
|
|
| `requested_key` | TEXT | Yes | - | len[0,1024] |
|
|
| `mime_type` | TEXT | Yes | - | MIME format, len[0,255] |
|
|
| `requested_size_mb` | DECIMAL | Yes | - | min: 0 |
|
|
| `expires_at` | DATE | Yes | - | - |
|
|
| `status` | TEXT | Yes | - | - |
|
|
| `projectId` | UUID | Yes | - | FK to projects |
|
|
| `userId` | UUID | Yes | - | FK to users |
|
|
|
|
---
|
|
|
|
### file
|
|
|
|
**Purpose:** Generic file attachments (user avatars, etc.).
|
|
|
|
| Field | Type | Nullable | Default | Validation |
|
|
|-------|------|----------|---------|------------|
|
|
| `id` | UUID | No | UUIDv4 | - |
|
|
| `belongsTo` | STRING(255) | Yes | - | Parent table name |
|
|
| `belongsToId` | UUID | Yes | - | Parent record ID |
|
|
| `belongsToColumn` | STRING(255) | Yes | - | Parent column name |
|
|
| `name` | STRING(2083) | No | - | notEmpty |
|
|
| `sizeInBytes` | INTEGER | Yes | - | - |
|
|
| `privateUrl` | STRING(2083) | Yes | - | - |
|
|
| `publicUrl` | STRING(2083) | No | - | notEmpty |
|
|
|
|
**Usage Pattern:** Polymorphic association via `belongsTo`, `belongsToId`, `belongsToColumn` fields and scoped `hasMany` on parent models:
|
|
|
|
```javascript
|
|
// In users model:
|
|
db.users.hasMany(db.file, {
|
|
as: 'avatar',
|
|
foreignKey: 'belongsToId',
|
|
scope: {
|
|
belongsTo: 'users',
|
|
belongsToColumn: 'avatar',
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Entity Relationship Diagram
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ Entity Relationships │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
┌─────────────┐
|
|
│ roles │
|
|
│ │
|
|
│ name │
|
|
│ permissions │◄─── M:N ───┐
|
|
└──────┬──────┘ │
|
|
│ │
|
|
│ 1:N │
|
|
│ │
|
|
┌─────────────────────────┐ ┌──────▼──────┐ ┌───────┴───────┐
|
|
│ element_type_defaults │ │ users │ │ permissions │
|
|
│ │ │ │ │ │
|
|
│ element_type │ │ email │ │ name │
|
|
│ name │ │ password │ │ │
|
|
│ settings_json │ │ app_role ───┼───┘ │
|
|
└───────────┬─────────────┘ └──────┬──────┘ │
|
|
│ │ │
|
|
│ 1:N │ 1:N │
|
|
│ │ │
|
|
▼ ▼ │
|
|
┌───────────────────────┐ ┌────────────────────┐ │
|
|
│project_element_defaults│ │project_memberships │ │
|
|
│ │ │ │ │
|
|
│ element_type │ │ access_level │ │
|
|
│ settings_json │ │ is_active │ │
|
|
│ source_element_id ────┼───┘ │ │
|
|
│ projectId ────────────┼───┐ │ │
|
|
└───────────────────────┘ │ │ │
|
|
│ │ │
|
|
│ N:1 │ N:1 │
|
|
│ │ │
|
|
▼ ▼ │
|
|
┌─────────────────────────────────────┐ │
|
|
│ projects │ │
|
|
│ │ │
|
|
│ name, slug, description │ │
|
|
│ logo_url, favicon_url, og_image_url │ │
|
|
└───────────────┬─────────────────────┘ │
|
|
│ │
|
|
┌─────────────┬───────────────┼───────────────┬─────────┘
|
|
│ │ │ │
|
|
│ 1:N │ 1:N │ 1:N │ 1:N
|
|
▼ ▼ ▼ ▼
|
|
┌───────────────┐ ┌───────────┐ ┌───────────────┐ ┌───────────────┐
|
|
│ tour_pages │ │ assets │ │publish_events │ │ pwa_caches │
|
|
│ │ │ │ │ │ │ │
|
|
│ environment │ │ asset_type│ │ status │ │ environment │
|
|
│ name, slug │ │ cdn_url │ │ from/to_env │ │ manifest_json │
|
|
│ ui_schema_json│ │ storage_key│ │ pages_copied │ │ asset_list │
|
|
└───────────────┘ └─────┬─────┘ └───────────────┘ └───────────────┘
|
|
│
|
|
│ 1:N
|
|
▼
|
|
┌─────────────────┐
|
|
│ asset_variants │
|
|
│ │
|
|
│ variant_type │
|
|
│ cdn_url │
|
|
│ width_px │
|
|
└─────────────────┘
|
|
|
|
|
|
Additional Models (project-scoped):
|
|
• project_audio_tracks → projects (N:1)
|
|
• project_transition_settings → projects (N:1)
|
|
• production_presentation_access → projects (N:1), users (N:1)
|
|
• access_logs → projects (N:1), users (N:1)
|
|
• presigned_url_requests → projects (N:1), users (N:1)
|
|
```
|
|
|
|
---
|
|
|
|
## Model Patterns
|
|
|
|
### 1. Soft Delete (Paranoid Mode)
|
|
|
|
All models use `paranoid: true`, which adds a `deletedAt` column and modifies queries:
|
|
|
|
```javascript
|
|
// This:
|
|
await Model.destroy({ where: { id } });
|
|
|
|
// Does NOT delete, but sets deletedAt = NOW()
|
|
// All find queries automatically filter deletedAt IS NULL
|
|
```
|
|
|
|
### 2. Environment Enum
|
|
|
|
Several models use the environment enum for multi-environment content:
|
|
|
|
```javascript
|
|
environment: {
|
|
type: DataTypes.ENUM,
|
|
values: ['dev', 'stage', 'production'],
|
|
}
|
|
```
|
|
|
|
**Used by:** tour_pages, project_audio_tracks, project_transition_settings, pwa_caches, publish_events
|
|
|
|
### 3. Import Hash Deduplication
|
|
|
|
The `importHash` field prevents duplicate imports:
|
|
|
|
```javascript
|
|
importHash: {
|
|
type: DataTypes.STRING(255),
|
|
allowNull: true,
|
|
unique: true,
|
|
}
|
|
```
|
|
|
|
### 4. Cascade Delete Patterns
|
|
|
|
| Relationship | onDelete | Use Case |
|
|
|--------------|----------|----------|
|
|
| `CASCADE` | Delete children when parent deleted | Projects → tour_pages |
|
|
| `SET NULL` | Keep children, null the FK | Roles → users |
|
|
|
|
### 5. Composite Unique Constraints
|
|
|
|
For scoped uniqueness:
|
|
|
|
```javascript
|
|
indexes: [
|
|
{ fields: ['projectId', 'environment', 'slug'], unique: true },
|
|
]
|
|
```
|
|
|
|
### 6. JSON Fields
|
|
|
|
Complex configurations stored as JSON:
|
|
|
|
```javascript
|
|
ui_schema_json: { type: DataTypes.JSON } // Parsed JSON column
|
|
settings_json: { type: DataTypes.TEXT } // Stringified JSON text
|
|
```
|
|
|
|
### 7. Virtual Fields
|
|
|
|
Computed fields that don't exist in the database:
|
|
|
|
```javascript
|
|
is_active: {
|
|
type: DataTypes.VIRTUAL,
|
|
get() {
|
|
return true;
|
|
},
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Validation Patterns
|
|
|
|
### String Length
|
|
|
|
```javascript
|
|
name: {
|
|
type: DataTypes.TEXT,
|
|
validate: {
|
|
len: {
|
|
args: [1, 255],
|
|
msg: 'Name must be between 1 and 255 characters',
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
### Required Fields
|
|
|
|
```javascript
|
|
email: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: false,
|
|
validate: {
|
|
notEmpty: { msg: 'Email is required' },
|
|
},
|
|
}
|
|
```
|
|
|
|
### Email Format
|
|
|
|
```javascript
|
|
email: {
|
|
validate: {
|
|
isEmail: { msg: 'Must be a valid email address' },
|
|
},
|
|
}
|
|
```
|
|
|
|
### Custom Validators
|
|
|
|
```javascript
|
|
cdn_url: {
|
|
validate: {
|
|
isUrlOrEmpty(value) {
|
|
if (value && value.length > 0 && !/^https?:\/\/.+/.test(value)) {
|
|
throw new Error('CDN URL must be a valid URL');
|
|
}
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
### Numeric Range
|
|
|
|
```javascript
|
|
volume: {
|
|
type: DataTypes.DECIMAL,
|
|
validate: {
|
|
min: { args: [0], msg: 'Volume must be at least 0' },
|
|
max: { args: [1], msg: 'Volume must be at most 1' },
|
|
},
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Usage Examples
|
|
|
|
### Importing Models
|
|
|
|
```javascript
|
|
const db = require('./db/models');
|
|
|
|
// Access models
|
|
const user = await db.users.findByPk(id);
|
|
const project = await db.projects.findOne({ where: { slug } });
|
|
|
|
// Access Sequelize instance
|
|
const transaction = await db.sequelize.transaction();
|
|
|
|
// Access Sequelize operators
|
|
const { Op } = db.Sequelize;
|
|
const users = await db.users.findAll({
|
|
where: { email: { [Op.like]: '%@example.com' } }
|
|
});
|
|
```
|
|
|
|
### Creating Records with Associations
|
|
|
|
```javascript
|
|
const project = await db.projects.create({
|
|
name: 'My Tour',
|
|
slug: 'my-tour',
|
|
createdById: userId,
|
|
});
|
|
|
|
const page = await db.tour_pages.create({
|
|
name: 'Home',
|
|
slug: 'home',
|
|
projectId: project.id,
|
|
environment: 'dev',
|
|
createdById: userId,
|
|
});
|
|
```
|
|
|
|
### Querying with Includes
|
|
|
|
```javascript
|
|
const project = await db.projects.findOne({
|
|
where: { id: projectId },
|
|
include: [
|
|
{ association: 'tour_pages_project', where: { environment: 'production' } },
|
|
{ association: 'project_memberships_project', include: ['user'] },
|
|
],
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [Database Schema](../database-schema.md) - Complete schema reference
|
|
- [Factories Module](./factories.md) - Service and router factories that use models
|
|
- [Services Module](./services.md) - Business logic using models
|
|
- [API Endpoints](../api-endpoints.md) - REST API exposing models
|