39948-vm/backend/docs/modules/db-models.md
2026-07-03 16:11:24 +02:00

37 KiB

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:

{
  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:

// 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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

// 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:

// 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:

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:

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:

indexes: [
  { fields: ['projectId', 'environment', 'slug'], unique: true },
]

6. JSON Fields

Complex configurations stored as JSON:

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:

is_active: {
  type: DataTypes.VIRTUAL,
  get() {
    return true;
  },
}

Validation Patterns

String Length

name: {
  type: DataTypes.TEXT,
  validate: {
    len: {
      args: [1, 255],
      msg: 'Name must be between 1 and 255 characters',
    },
  },
}

Required Fields

email: {
  type: DataTypes.TEXT,
  allowNull: false,
  validate: {
    notEmpty: { msg: 'Email is required' },
  },
}

Email Format

email: {
  validate: {
    isEmail: { msg: 'Must be a valid email address' },
  },
}

Custom Validators

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

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

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

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

const project = await db.projects.findOne({
  where: { id: projectId },
  include: [
    { association: 'tour_pages_project', where: { environment: 'production' } },
    { association: 'project_memberships_project', include: ['user'] },
  ],
});