feat: V2 microservices stack — backend services, gateway, JWT auth

Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.

Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.

Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).

Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-29 23:29:31 +03:30
parent 53ea78a00d
commit 90ac0b81d1
7636 changed files with 3707504 additions and 240 deletions
+90
View File
@@ -0,0 +1,90 @@
# FlatRender V2 — Database Schemas
PostgreSQL 15+. Single database, one schema per microservice.
## Run order
Apply migrations in numerical order:
```bash
psql -d flatrender -f migrations/00_setup.sql
psql -d flatrender -f migrations/01_identity_tenants.sql
psql -d flatrender -f migrations/02_identity_users.sql
psql -d flatrender -f migrations/03_identity_billing.sql
psql -d flatrender -f migrations/04_identity_gamification.sql
psql -d flatrender -f migrations/05_content_taxonomy.sql
psql -d flatrender -f migrations/06_content_projects.sql
psql -d flatrender -f migrations/07_content_scenes.sql
psql -d flatrender -f migrations/08_content_characters_presets.sql
psql -d flatrender -f migrations/09_content_cms.sql
psql -d flatrender -f migrations/10_studio_saved_projects.sql
psql -d flatrender -f migrations/11_render_nodes.sql
psql -d flatrender -f migrations/12_render_jobs.sql
psql -d flatrender -f migrations/13_file_manager.sql
psql -d flatrender -f migrations/14_notification.sql
```
## Schemas
| Schema | Owner Service | Purpose |
|---|---|---|
| `identity` | Identity Service (.NET) | tenants, users, auth, plans, payments, gamification |
| `content` | Content Service (.NET) | templates, scenes, presets, blogs, CMS |
| `studio` | Studio Service (.NET) | user's saved projects + audio (music/voiceover/sfx) |
| `render` | Render Orchestrator (Go) | jobs, nodes, frame jobs, snapshots, exports |
| `file_mgr` | File Service (Go) | user files, folders, quotas, cleanup |
| `notification` | Notification Service (Go) | in-app, push, email, SMS, telegram |
## Cross-schema design
Schemas are **loosely coupled**. Where it matters for integrity (within a
service), FKs are used. Across services, FKs are deliberately omitted so
services can evolve independently — referential integrity is enforced
via service code and events.
### Hard FKs across schemas (intentional)
- `identity.earned_gifts.notification_id``notification.notifications.id`
Everything else uses **soft references** (column documented but no FK).
## Multi-tenancy
`identity.tenants` is the root of multi-tenancy. The default FlatRender
tenant has UUID `00000000-0000-0000-0000-000000000001`.
Every user, project, render job, file, and notification carries a
`tenant_id`. Resellers (B2B API customers) are tenants. White-label
branding, API keys, webhooks, and usage metering all hang off
`identity.tenants.*`.
## New features (vs V1)
- **Multi-tenancy / Reseller API**: `identity.tenants`, `tenant_branding`,
`tenant_api_keys`, `tenant_webhooks`, `tenant_usage_daily`
- **Voiceover support**: `studio.saved_projects.voiceover_*`,
`render.render_jobs.has_voiceover`
- **Per-track volume**: `music_volume`, `sfx_volume`, `voiceover_volume`
- **Scene snapshots**: `render.snapshots` with cache key
- **AE crash tracking**: `render.node_crashes` + auto-recovery
- **Frame repair jobs**: `render.frame_repair_jobs`
- **AEP local cache on nodes**: `render.node_template_cache` (LRU)
- **SVG color previews**: `content.template_svg_previews` (drop image → traced SVG)
- **PWA push subscriptions**: `identity.push_subscriptions`
- **MFA**: `identity.mfa_factors`
- **Multipart uploads**: `file_mgr.upload_sessions`
- **Cleanup scheduler**: `file_mgr.cleanup_schedules`
- **Per-user / per-channel notification preferences**:
`notification.notification_preferences`
## Partitioning
Time-series tables are partitioned monthly (initial partition for
2026-01 created; ops creates new ones via cron):
- `identity.tenant_api_request_logs`
- `render.node_health_logs`
## Service user grants
Each microservice connects with its own DB role limited to its schema.
See top of `00_setup.sql` for the recipe.
+51
View File
@@ -0,0 +1,51 @@
-- =====================================================================
-- FlatRender V2 — Database Setup
-- Single PostgreSQL database with per-service schemas
-- =====================================================================
-- Extensions
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
CREATE EXTENSION IF NOT EXISTS "citext"; -- case-insensitive text (emails)
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- fuzzy text search
-- =====================================================================
-- Schemas (one per microservice)
-- =====================================================================
CREATE SCHEMA IF NOT EXISTS identity;
CREATE SCHEMA IF NOT EXISTS content;
CREATE SCHEMA IF NOT EXISTS studio;
CREATE SCHEMA IF NOT EXISTS render;
CREATE SCHEMA IF NOT EXISTS file_mgr;
CREATE SCHEMA IF NOT EXISTS notification;
-- =====================================================================
-- Service users (each microservice connects with limited grants)
-- =====================================================================
-- Run separately by ops:
-- CREATE USER svc_identity WITH PASSWORD '...';
-- CREATE USER svc_content WITH PASSWORD '...';
-- CREATE USER svc_studio WITH PASSWORD '...';
-- CREATE USER svc_render WITH PASSWORD '...';
-- CREATE USER svc_file WITH PASSWORD '...';
-- CREATE USER svc_notification WITH PASSWORD '...';
-- GRANT ALL ON SCHEMA identity TO svc_identity;
-- GRANT ALL ON SCHEMA content TO svc_content;
-- ... etc.
-- Read-only cross-schema grants where needed (defined per service)
-- =====================================================================
-- Common helper: auto-update updated_at on row update
-- =====================================================================
CREATE OR REPLACE FUNCTION public.tg_set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- =====================================================================
-- Common helper: soft-delete check (used in policies/views later)
-- =====================================================================
-- Convention: every soft-deletable table has `deleted_at TIMESTAMPTZ NULL`
-- Active rows: WHERE deleted_at IS NULL
@@ -0,0 +1,352 @@
-- =====================================================================
-- IDENTITY SCHEMA — Part 1: Tenants (Multi-tenancy / Reseller API)
-- =====================================================================
-- Every user, project, render, export belongs to a tenant.
-- FlatRender's own customers belong to the DEFAULT tenant.
-- Resellers (B2B) are their own tenants with their own users.
-- =====================================================================
SET search_path TO identity, public;
-- ---------------------------------------------------------------------
-- tenants — companies / resellers (and FlatRender itself = default)
-- ---------------------------------------------------------------------
CREATE TYPE tenant_status AS ENUM ('Active','Trial','Suspended','Cancelled');
CREATE TYPE tenant_kind AS ENUM ('Internal','Reseller','Enterprise');
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug CITEXT UNIQUE NOT NULL, -- 'flatrender', 'acme', ...
name TEXT NOT NULL,
kind tenant_kind NOT NULL DEFAULT 'Reseller',
status tenant_status NOT NULL DEFAULT 'Trial',
-- Domains
custom_domain CITEXT UNIQUE, -- videos.acme.com
domain_verified BOOLEAN NOT NULL DEFAULT FALSE,
allowed_origins TEXT[] NOT NULL DEFAULT '{}', -- CORS
-- Contact
contact_name TEXT,
contact_email CITEXT,
contact_phone TEXT,
billing_email CITEXT,
-- Limits (overrideable from default plan)
max_users INT, -- NULL = unlimited
max_storage_gb INT,
monthly_render_qty INT,
monthly_render_sec INT,
-- Lifecycle
trial_ends_at TIMESTAMPTZ,
suspended_at TIMESTAMPTZ,
suspension_reason TEXT,
-- Metadata
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_tenants_status ON tenants(status) WHERE deleted_at IS NULL;
CREATE INDEX idx_tenants_custom_domain ON tenants(custom_domain) WHERE custom_domain IS NOT NULL;
CREATE TRIGGER tg_tenants_updated_at
BEFORE UPDATE ON tenants
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- Seed the default (internal) FlatRender tenant
INSERT INTO tenants (id, slug, name, kind, status)
VALUES (
'00000000-0000-0000-0000-000000000001',
'flatrender',
'FlatRender',
'Internal',
'Active'
);
-- ---------------------------------------------------------------------
-- tenant_branding — white-label appearance
-- ---------------------------------------------------------------------
CREATE TABLE tenant_branding (
tenant_id UUID PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
-- Identity
display_name TEXT,
logo_url TEXT,
logo_dark_url TEXT,
favicon_url TEXT,
og_image_url TEXT,
-- Theme
primary_color TEXT NOT NULL DEFAULT '#3B82F6',
secondary_color TEXT NOT NULL DEFAULT '#8B5CF6',
accent_color TEXT,
background_color TEXT,
font_family TEXT,
-- Email
email_from_name TEXT,
email_from_address CITEXT,
email_reply_to CITEXT,
email_footer_html TEXT,
-- Links
support_url TEXT,
terms_url TEXT,
privacy_url TEXT,
-- Studio embed
embed_enabled BOOLEAN NOT NULL DEFAULT FALSE,
embed_allowed_hosts TEXT[] NOT NULL DEFAULT '{}',
-- Custom CSS for advanced clients (sanitized)
custom_css TEXT,
-- Watermark for free renders (reseller can override)
watermark_text TEXT,
watermark_image_url TEXT,
watermark_enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TRIGGER tg_tenant_branding_updated_at
BEFORE UPDATE ON tenant_branding
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- tenant_settings — feature flags + integration config
-- ---------------------------------------------------------------------
CREATE TABLE tenant_settings (
tenant_id UUID PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
-- Locale
default_locale TEXT NOT NULL DEFAULT 'fa',
supported_locales TEXT[] NOT NULL DEFAULT '{fa,en}',
default_currency TEXT NOT NULL DEFAULT 'IRR',
-- Payments allowed for this tenant's users
payment_gateways TEXT[] NOT NULL DEFAULT '{ZarinPal}', -- {ZarinPal,IdPay,Bazaar,Stripe}
-- Render
max_resolution TEXT NOT NULL DEFAULT 'FullHD', -- HD/FullHD/FourK
max_duration_sec INT NOT NULL DEFAULT 600,
allow_4k BOOLEAN NOT NULL DEFAULT FALSE,
allow_voiceover BOOLEAN NOT NULL DEFAULT TRUE,
allow_music_visualizer BOOLEAN NOT NULL DEFAULT TRUE,
allow_mockup BOOLEAN NOT NULL DEFAULT TRUE,
-- Webhooks
webhook_signing_secret TEXT,
-- Feature flags
features JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TRIGGER tg_tenant_settings_updated_at
BEFORE UPDATE ON tenant_settings
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- tenant_api_keys — server-to-server API access
-- ---------------------------------------------------------------------
CREATE TYPE api_key_environment AS ENUM ('Live','Test');
CREATE TABLE tenant_api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL, -- "Production server"
environment api_key_environment NOT NULL DEFAULT 'Live',
-- Storage
key_prefix TEXT NOT NULL, -- "fr_live_abc..." (first 12 chars visible)
key_hash TEXT NOT NULL, -- SHA-256 of full key
last4 TEXT NOT NULL, -- last 4 chars for UI
-- Permissions
scopes TEXT[] NOT NULL DEFAULT '{}', -- ["renders:create","projects:read",...]
allowed_ips INET[], -- NULL = any
rate_limit_rpm INT NOT NULL DEFAULT 600,
-- Lifecycle
is_active BOOLEAN NOT NULL DEFAULT TRUE,
expires_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
revoked_reason TEXT,
last_used_at TIMESTAMPTZ,
last_used_ip INET,
usage_count BIGINT NOT NULL DEFAULT 0,
created_by_user_id UUID, -- FK added after users table exists
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX uq_api_keys_hash ON tenant_api_keys(key_hash);
CREATE INDEX idx_api_keys_tenant ON tenant_api_keys(tenant_id) WHERE is_active = TRUE;
CREATE INDEX idx_api_keys_prefix ON tenant_api_keys(key_prefix);
CREATE TRIGGER tg_tenant_api_keys_updated_at
BEFORE UPDATE ON tenant_api_keys
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- tenant_webhooks — outbound events to reseller systems
-- ---------------------------------------------------------------------
CREATE TABLE tenant_webhooks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
url TEXT NOT NULL,
events TEXT[] NOT NULL, -- ["render.completed","render.failed",...]
secret TEXT NOT NULL, -- HMAC signing secret
is_active BOOLEAN NOT NULL DEFAULT TRUE,
last_triggered_at TIMESTAMPTZ,
last_status_code INT,
consecutive_failures INT NOT NULL DEFAULT 0,
disabled_at TIMESTAMPTZ, -- auto-disable after N failures
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_webhooks_tenant_active ON tenant_webhooks(tenant_id) WHERE is_active = TRUE;
CREATE TRIGGER tg_tenant_webhooks_updated_at
BEFORE UPDATE ON tenant_webhooks
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- tenant_webhook_deliveries — log of attempts
-- ---------------------------------------------------------------------
CREATE TABLE tenant_webhook_deliveries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
webhook_id UUID NOT NULL REFERENCES tenant_webhooks(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL, -- denormalized for partitioning
event_type TEXT NOT NULL,
event_id UUID NOT NULL,
payload JSONB NOT NULL,
request_url TEXT NOT NULL,
response_status INT,
response_body TEXT,
duration_ms INT,
attempt INT NOT NULL DEFAULT 1,
succeeded BOOLEAN NOT NULL DEFAULT FALSE,
next_retry_at TIMESTAMPTZ,
error_message TEXT,
delivered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_webhook_deliveries_webhook ON tenant_webhook_deliveries(webhook_id, created_at DESC);
CREATE INDEX idx_webhook_deliveries_pending ON tenant_webhook_deliveries(next_retry_at) WHERE succeeded = FALSE AND next_retry_at IS NOT NULL;
-- ---------------------------------------------------------------------
-- tenant_usage_daily — aggregate metering for billing the reseller
-- ---------------------------------------------------------------------
CREATE TABLE tenant_usage_daily (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
usage_date DATE NOT NULL,
-- Render
renders_started INT NOT NULL DEFAULT 0,
renders_completed INT NOT NULL DEFAULT 0,
renders_failed INT NOT NULL DEFAULT 0,
render_seconds BIGINT NOT NULL DEFAULT 0, -- output duration sum
render_compute_sec BIGINT NOT NULL DEFAULT 0, -- node compute time
-- Storage
storage_bytes BIGINT NOT NULL DEFAULT 0, -- snapshot at end of day
-- API
api_calls BIGINT NOT NULL DEFAULT 0,
api_4xx BIGINT NOT NULL DEFAULT 0,
api_5xx BIGINT NOT NULL DEFAULT 0,
-- Users
active_users INT NOT NULL DEFAULT 0, -- DAU
new_users INT NOT NULL DEFAULT 0,
-- Billing
amount_billed NUMERIC(14,2) NOT NULL DEFAULT 0,
billing_currency TEXT NOT NULL DEFAULT 'IRR',
billing_status TEXT NOT NULL DEFAULT 'Pending', -- Pending/Invoiced/Paid
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, usage_date)
);
CREATE INDEX idx_tenant_usage_tenant_date ON tenant_usage_daily(tenant_id, usage_date DESC);
CREATE TRIGGER tg_tenant_usage_updated_at
BEFORE UPDATE ON tenant_usage_daily
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- tenant_api_request_logs — per-call audit (partitioned by month)
-- ---------------------------------------------------------------------
CREATE TABLE tenant_api_request_logs (
id BIGSERIAL,
tenant_id UUID NOT NULL,
api_key_id UUID,
method TEXT NOT NULL,
path TEXT NOT NULL,
status_code INT NOT NULL,
duration_ms INT NOT NULL,
request_id UUID NOT NULL,
user_id UUID,
ip_address INET,
user_agent TEXT,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
CREATE INDEX idx_api_logs_tenant ON tenant_api_request_logs(tenant_id, created_at DESC);
CREATE INDEX idx_api_logs_key ON tenant_api_request_logs(api_key_id, created_at DESC);
-- Initial partition (ops creates monthly going forward)
CREATE TABLE tenant_api_request_logs_y2026m01
PARTITION OF tenant_api_request_logs
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
-- ---------------------------------------------------------------------
-- tenant_domain_verifications — DNS / file verification
-- ---------------------------------------------------------------------
CREATE TABLE tenant_domain_verifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
domain CITEXT NOT NULL,
method TEXT NOT NULL, -- 'DNS_TXT' or 'HTTP_FILE'
challenge_token TEXT NOT NULL,
verified BOOLEAN NOT NULL DEFAULT FALSE,
verified_at TIMESTAMPTZ,
last_checked_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_domain_verif_tenant ON tenant_domain_verifications(tenant_id);
+227
View File
@@ -0,0 +1,227 @@
-- =====================================================================
-- IDENTITY SCHEMA — Part 2: Users, Auth, Sessions
-- =====================================================================
SET search_path TO identity, public;
CREATE TYPE register_mode AS ENUM ('Email','Mobile','Google','Telegram','SSO','Reseller');
CREATE TYPE gender_kind AS ENUM ('Male','Female','Other','PreferNotToSay');
-- ---------------------------------------------------------------------
-- users
-- ---------------------------------------------------------------------
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE RESTRICT,
-- Auth
email CITEXT,
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
email_verified_at TIMESTAMPTZ,
phone_number TEXT,
phone_country_code TEXT,
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone_verified_at TIMESTAMPTZ,
password_hash TEXT, -- bcrypt; NULL for OAuth-only
password_set_at TIMESTAMPTZ,
last_password_reset_date TIMESTAMPTZ,
register_mode register_mode NOT NULL DEFAULT 'Email',
external_provider TEXT, -- google_oauth_subject etc.
external_provider_id TEXT,
-- Profile
full_name TEXT,
avatar_url TEXT,
birth_date DATE,
gender gender_kind,
national_code TEXT, -- Iran-specific
country_code TEXT,
company_name TEXT,
website_name TEXT,
slogan TEXT,
about_me TEXT,
method_of_introduction TEXT,
-- Balances (cents/rial; use BIGINT to avoid float)
balance_minor BIGINT NOT NULL DEFAULT 0,
affiliate_balance_minor BIGINT NOT NULL DEFAULT 0,
affiliate_owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
profit_percentage NUMERIC(5,2) NOT NULL DEFAULT 0,
-- Gamification (kept lean)
loyalty_score INT NOT NULL DEFAULT 0,
purple_point INT NOT NULL DEFAULT 0,
-- Render quotas (computed from plan + bonuses)
daily_remain_render_count INT NOT NULL DEFAULT 0,
max_daily_render_count INT NOT NULL DEFAULT 0,
parallel_rendering_ceiling INT NOT NULL DEFAULT 1,
user_daily_free_charge_sec INT NOT NULL DEFAULT 0,
daily_free_charge_reset_date TIMESTAMPTZ,
max_preview_duration_sec INT NOT NULL DEFAULT 30,
force_render_queue BOOLEAN NOT NULL DEFAULT FALSE,
remove_watermark_service BOOLEAN NOT NULL DEFAULT FALSE,
-- Telegram (legacy but kept)
telegram_id TEXT,
telegram_token TEXT,
telegram_token_expire_date TIMESTAMPTZ,
telegram_tell_me BOOLEAN NOT NULL DEFAULT FALSE,
telegram_reset_date TIMESTAMPTZ,
user_telegram_charge INT NOT NULL DEFAULT 0,
-- Comms preferences
email_tell_me BOOLEAN NOT NULL DEFAULT TRUE,
sms_tell_me BOOLEAN NOT NULL DEFAULT FALSE,
push_tell_me BOOLEAN NOT NULL DEFAULT TRUE,
-- Storage
storage_endpoint TEXT,
used_storage_bytes BIGINT NOT NULL DEFAULT 0,
-- Status
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
is_tenant_admin BOOLEAN NOT NULL DEFAULT FALSE, -- admin within a tenant
ban_account BOOLEAN NOT NULL DEFAULT FALSE,
ban_reason TEXT,
unblock_date TIMESTAMPTZ,
-- Activity
last_active_date TIMESTAMPTZ,
last_login_at TIMESTAMPTZ,
last_login_ip INET,
registered_with_mobile_app BOOLEAN NOT NULL DEFAULT FALSE,
register_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Misc
cid TEXT, -- legacy
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
-- Uniqueness scoped to tenant (so two tenants can have user@x.com independently)
CONSTRAINT uq_users_tenant_email UNIQUE (tenant_id, email),
CONSTRAINT uq_users_tenant_phone UNIQUE (tenant_id, phone_number),
CONSTRAINT uq_users_external UNIQUE (external_provider, external_provider_id)
);
CREATE INDEX idx_users_tenant ON users(tenant_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_email ON users(email) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_affiliate ON users(affiliate_owner_id) WHERE affiliate_owner_id IS NOT NULL;
CREATE INDEX idx_users_last_active ON users(last_active_date DESC);
CREATE INDEX idx_users_fullname_trgm ON users USING gin (full_name gin_trgm_ops);
CREATE TRIGGER tg_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- Now fix FK from tenant_api_keys.created_by_user_id
ALTER TABLE tenant_api_keys
ADD CONSTRAINT fk_api_keys_creator
FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
-- ---------------------------------------------------------------------
-- user_sessions — JWT refresh tokens / device tracking
-- ---------------------------------------------------------------------
CREATE TABLE user_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
refresh_token_hash TEXT NOT NULL,
device_id TEXT,
device_name TEXT,
user_agent TEXT,
ip_address INET,
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX uq_sessions_token ON user_sessions(refresh_token_hash);
CREATE INDEX idx_sessions_user ON user_sessions(user_id) WHERE revoked_at IS NULL;
-- ---------------------------------------------------------------------
-- confirmation_tokens — email/phone verify, password reset, MFA
-- ---------------------------------------------------------------------
CREATE TYPE token_purpose AS ENUM (
'EmailVerification','PhoneVerification',
'PasswordReset','MfaSetup','Login','EmailChange'
);
CREATE TABLE confirmation_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
purpose token_purpose NOT NULL,
identifier CITEXT NOT NULL, -- email or phone being verified
next_identifier CITEXT, -- for email change
token_hash TEXT NOT NULL,
code TEXT, -- 6-digit OTP (hashed in token_hash)
is_consumed BOOLEAN NOT NULL DEFAULT FALSE,
consumed_at TIMESTAMPTZ,
try_count INT NOT NULL DEFAULT 0,
max_tries INT NOT NULL DEFAULT 5,
request_ip INET,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_conf_tokens_user ON confirmation_tokens(user_id, purpose) WHERE is_consumed = FALSE;
CREATE INDEX idx_conf_tokens_lookup ON confirmation_tokens(token_hash) WHERE is_consumed = FALSE;
-- ---------------------------------------------------------------------
-- push_subscriptions — PWA Web Push
-- ---------------------------------------------------------------------
CREATE TABLE push_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
endpoint TEXT NOT NULL,
p256dh_key TEXT NOT NULL,
auth_key TEXT NOT NULL,
user_agent TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
last_used_at TIMESTAMPTZ,
failure_count INT NOT NULL DEFAULT 0,
last_failure_at TIMESTAMPTZ,
last_failure_status INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, endpoint)
);
CREATE INDEX idx_push_subs_user_active ON push_subscriptions(user_id) WHERE is_active = TRUE;
-- ---------------------------------------------------------------------
-- mfa_factors — TOTP, SMS, recovery codes
-- ---------------------------------------------------------------------
CREATE TYPE mfa_factor_type AS ENUM ('TOTP','SMS','Email','RecoveryCode');
CREATE TABLE mfa_factors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
factor_type mfa_factor_type NOT NULL,
secret_encrypted TEXT,
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
label TEXT,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_mfa_user ON mfa_factors(user_id) WHERE is_verified = TRUE;
@@ -0,0 +1,314 @@
-- =====================================================================
-- IDENTITY SCHEMA — Part 3: Plans, Subscriptions, Payments, Discounts
-- =====================================================================
SET search_path TO identity, public;
CREATE TYPE plan_scope AS ENUM ('User','Tenant');
CREATE TYPE billing_period AS ENUM ('Monthly','Quarterly','SemiAnnual','Annual','Lifetime','OneTime');
CREATE TYPE payment_gateway AS ENUM ('ZarinPal','IdPay','Bazaar','Stripe','Balance','Manual');
CREATE TYPE payment_status AS ENUM ('Pending','Succeeded','Failed','Refunded','Cancelled');
CREATE TYPE payment_action AS ENUM ('PlanPurchase','BalanceCharge','ProjectRender','UserProject','StorageUpgrade','Other');
CREATE TYPE discount_kind AS ENUM ('Percentage','FixedAmount','FreeMonths','RenderCredits');
-- ---------------------------------------------------------------------
-- plans — subscription tiers (FlatRender or tenant-defined)
-- ---------------------------------------------------------------------
CREATE TABLE plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
-- NULL tenant_id = global plan available to all tenants
scope plan_scope NOT NULL DEFAULT 'User',
code TEXT NOT NULL, -- 'pro_monthly', 'business_annual'
name TEXT NOT NULL,
description TEXT,
-- Pricing
price_minor BIGINT NOT NULL DEFAULT 0,
before_price_minor BIGINT, -- shown crossed-out
currency TEXT NOT NULL DEFAULT 'IRR',
discount_percentage NUMERIC(5,2) NOT NULL DEFAULT 0,
billing_period billing_period NOT NULL DEFAULT 'Monthly',
months_duration INT, -- for non-monthly
-- Quotas
seconds_charge INT NOT NULL DEFAULT 0, -- render seconds included
monthly_renders_quota INT, -- NULL = unlimited
storage_gb INT NOT NULL DEFAULT 1,
parallel_renders INT NOT NULL DEFAULT 1,
max_resolution TEXT NOT NULL DEFAULT 'FullHD',
min_video_length_sec INT NOT NULL DEFAULT 0,
render_speed_factor NUMERIC(4,2) NOT NULL DEFAULT 1.0,
-- UI
sort INT NOT NULL DEFAULT 0,
icon TEXT,
cover TEXT,
loyalty_mark TEXT,
color TEXT,
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
-- Feature flags carried by this plan
features JSONB NOT NULL DEFAULT '{}'::jsonb,
-- Lifecycle
is_active BOOLEAN NOT NULL DEFAULT TRUE,
available_from TIMESTAMPTZ,
available_until TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uq_plans_tenant_code UNIQUE (tenant_id, code)
);
CREATE INDEX idx_plans_tenant_active ON plans(tenant_id) WHERE is_active = TRUE AND deleted_at IS NULL;
CREATE TRIGGER tg_plans_updated_at
BEFORE UPDATE ON plans
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- user_plans — active subscriptions
-- ---------------------------------------------------------------------
CREATE TABLE user_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
plan_id UUID NOT NULL REFERENCES plans(id),
-- Snapshot at purchase time
plan_code TEXT NOT NULL,
plan_name TEXT NOT NULL,
price_minor_paid BIGINT NOT NULL,
currency TEXT NOT NULL,
-- Quota state
initial_seconds_charge INT NOT NULL,
remain_charge_sec INT NOT NULL,
added_charge_from_past_plan INT NOT NULL DEFAULT 0,
monthly_renders_used INT NOT NULL DEFAULT 0,
monthly_renders_reset_at TIMESTAMPTZ,
-- Lifecycle
register_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
cancelled_at TIMESTAMPTZ,
cancel_reason TEXT,
auto_renew BOOLEAN NOT NULL DEFAULT FALSE,
-- Reference to payment
payment_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_user_plans_active ON user_plans(user_id) WHERE cancelled_at IS NULL;
CREATE INDEX idx_user_plans_expire ON user_plans(expires_at) WHERE cancelled_at IS NULL;
CREATE TRIGGER tg_user_plans_updated_at
BEFORE UPDATE ON user_plans
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- discounts — coupons / affiliate codes
-- ---------------------------------------------------------------------
CREATE TABLE discounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
code CITEXT NOT NULL,
kind discount_kind NOT NULL,
value NUMERIC(14,2) NOT NULL, -- percent or amount
-- Affiliate split
owner_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
owner_profit_percentage NUMERIC(5,2) NOT NULL DEFAULT 0,
only_owner BOOLEAN NOT NULL DEFAULT FALSE, -- usable only by owner
-- Constraints
max_use_count INT, -- NULL = unlimited
used_count INT NOT NULL DEFAULT 0,
min_purchase_minor BIGINT NOT NULL DEFAULT 0,
applies_to_plan_ids UUID[],
starts_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_discounts_tenant_code UNIQUE (tenant_id, code)
);
CREATE INDEX idx_discounts_active ON discounts(tenant_id) WHERE is_active = TRUE;
CREATE INDEX idx_discounts_owner ON discounts(owner_user_id) WHERE owner_user_id IS NOT NULL;
CREATE TRIGGER tg_discounts_updated_at
BEFORE UPDATE ON discounts
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- used_discounts — usage log per user
-- ---------------------------------------------------------------------
CREATE TABLE used_discounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
discount_id UUID NOT NULL REFERENCES discounts(id) ON DELETE RESTRICT,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
payment_id UUID,
code CITEXT NOT NULL,
amount_discounted_minor BIGINT NOT NULL,
use_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_used_discounts_user ON used_discounts(user_id, use_date DESC);
-- ---------------------------------------------------------------------
-- payments
-- ---------------------------------------------------------------------
CREATE TABLE payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
gateway payment_gateway NOT NULL,
status payment_status NOT NULL DEFAULT 'Pending',
action payment_action NOT NULL,
-- Amounts
amount_minor BIGINT NOT NULL,
currency TEXT NOT NULL DEFAULT 'IRR',
balance_reducer_minor BIGINT NOT NULL DEFAULT 0, -- portion paid from balance
discount_value_minor BIGINT NOT NULL DEFAULT 0,
-- Gateway refs
gateway_token TEXT,
gateway_order_id TEXT,
gateway_track_id TEXT,
gateway_response JSONB,
card_last4 TEXT,
card_hash TEXT,
-- Description
title TEXT,
description TEXT,
-- Affiliate
owner_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
affiliate_profit_minor BIGINT NOT NULL DEFAULT 0,
after_pay_score NUMERIC(8,2) NOT NULL DEFAULT 0,
-- Polymorphic product reference
used_discount_id UUID REFERENCES used_discounts(id),
plan_id UUID REFERENCES plans(id),
user_project_id UUID, -- FK added later if user_projects keeps existing
render_job_id UUID,
product_id UUID,
-- Lifecycle
confirmed_at TIMESTAMPTZ,
failed_at TIMESTAMPTZ,
failure_reason TEXT,
refunded_at TIMESTAMPTZ,
refund_amount_minor BIGINT,
refund_reason TEXT,
-- Checkout/receipt
checkouted BOOLEAN NOT NULL DEFAULT FALSE,
checkout_date TIMESTAMPTZ,
checkout_recipe TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_payments_user ON payments(user_id, created_at DESC);
CREATE INDEX idx_payments_tenant ON payments(tenant_id, created_at DESC);
CREATE INDEX idx_payments_status ON payments(status) WHERE status IN ('Pending','Failed');
CREATE INDEX idx_payments_track ON payments(gateway, gateway_track_id) WHERE gateway_track_id IS NOT NULL;
CREATE TRIGGER tg_payments_updated_at
BEFORE UPDATE ON payments
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- Now wire up the FKs that were dangling
ALTER TABLE used_discounts
ADD CONSTRAINT fk_used_disc_payment
FOREIGN KEY (payment_id) REFERENCES payments(id) ON DELETE SET NULL;
ALTER TABLE user_plans
ADD CONSTRAINT fk_user_plans_payment
FOREIGN KEY (payment_id) REFERENCES payments(id) ON DELETE SET NULL;
-- ---------------------------------------------------------------------
-- checkouts — receipts / affiliate payouts log
-- ---------------------------------------------------------------------
CREATE TABLE checkouts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
affiliate_owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
title TEXT,
description TEXT,
amount_minor BIGINT NOT NULL,
currency TEXT NOT NULL DEFAULT 'IRR',
card_number_last4 TEXT,
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
paid_at TIMESTAMPTZ,
recipe TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_checkouts_user ON checkouts(user_id, requested_at DESC);
-- ---------------------------------------------------------------------
-- user_projects — freelance custom-template requests
-- ---------------------------------------------------------------------
CREATE TYPE user_project_status AS ENUM (
'Draft','Submitted','Quoted','InProgress','Review','Completed','Cancelled'
);
CREATE TABLE user_projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
title TEXT,
description TEXT,
graphist_description TEXT,
attachment_path TEXT,
price_minor BIGINT,
user_suggested_price_minor BIGINT,
status user_project_status NOT NULL DEFAULT 'Draft',
user_max_requested_day_to_create INT,
user_graphist_max_requested_day_to_create INT,
created_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
end_time TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_user_projects_user ON user_projects(user_id, status);
CREATE TRIGGER tg_user_projects_updated_at
BEFORE UPDATE ON user_projects
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
ALTER TABLE payments
ADD CONSTRAINT fk_payments_user_project
FOREIGN KEY (user_project_id) REFERENCES user_projects(id) ON DELETE SET NULL;
@@ -0,0 +1,149 @@
-- =====================================================================
-- IDENTITY SCHEMA — Part 4: Gamification (simplified)
-- =====================================================================
-- Quests, gifts, loyalty — leaner than V1 but still functional.
-- =====================================================================
SET search_path TO identity, public;
CREATE TYPE quest_type AS ENUM ('OneTime','Daily','Weekly','Onboarding','Milestone');
CREATE TYPE prize_type AS ENUM ('Balance','RenderSeconds','LoyaltyPoints','StorageGB','Plan','Discount');
CREATE TYPE gift_type AS ENUM ('Bonus','Referral','Compensation','Promotion','Achievement');
-- ---------------------------------------------------------------------
-- quests
-- ---------------------------------------------------------------------
CREATE TABLE quests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, -- NULL = global
title TEXT NOT NULL,
challenge TEXT,
why TEXT,
hint TEXT,
aphorism TEXT,
icon TEXT,
quest_type quest_type NOT NULL,
target_event TEXT NOT NULL, -- 'user.registered','project.created',...
target_count INT NOT NULL DEFAULT 1, -- how many times event must fire
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
prize_type prize_type NOT NULL,
prize_amount BIGINT NOT NULL, -- minor units or seconds/points
level_limit INT, -- minimum loyalty level
start_url TEXT,
post_action_name TEXT,
order_value INT NOT NULL DEFAULT 0,
starts_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_quests_active ON quests(quest_type, is_active);
CREATE TRIGGER tg_quests_updated_at
BEFORE UPDATE ON quests
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- user_quest_progress — incremental tracking
-- ---------------------------------------------------------------------
CREATE TABLE user_quest_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
quest_id UUID NOT NULL REFERENCES quests(id) ON DELETE CASCADE,
current_count INT NOT NULL DEFAULT 0,
text_value TEXT, -- for input-based quests
is_completed BOOLEAN NOT NULL DEFAULT FALSE,
completed_at TIMESTAMPTZ,
prize_claimed BOOLEAN NOT NULL DEFAULT FALSE,
prize_claimed_at TIMESTAMPTZ,
period_start DATE, -- for Daily/Weekly resets
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, quest_id, period_start)
);
CREATE INDEX idx_quest_prog_user_open ON user_quest_progress(user_id) WHERE is_completed = FALSE;
CREATE TRIGGER tg_quest_prog_updated_at
BEFORE UPDATE ON user_quest_progress
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- gifts — admin-issued bonuses
-- ---------------------------------------------------------------------
CREATE TABLE gifts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
icon TEXT,
gift_type gift_type NOT NULL,
prize_type prize_type NOT NULL,
value BIGINT NOT NULL,
unit TEXT, -- 'seconds','IRR','points',...
assigned_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ---------------------------------------------------------------------
-- earned_gifts — issued to user (consumed via used_gifts)
-- ---------------------------------------------------------------------
CREATE TABLE earned_gifts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
gift_id UUID NOT NULL REFERENCES gifts(id) ON DELETE RESTRICT,
notification_id UUID, -- FK added later
source TEXT, -- 'quest','admin','referral','plan'
source_ref UUID, -- e.g. quest_id
earned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ,
is_used BOOLEAN NOT NULL DEFAULT FALSE,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_earned_gifts_user ON earned_gifts(user_id) WHERE is_used = FALSE;
-- ---------------------------------------------------------------------
-- used_gifts — claim log
-- ---------------------------------------------------------------------
CREATE TABLE used_gifts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
earned_gift_id UUID NOT NULL REFERENCES earned_gifts(id) ON DELETE RESTRICT,
used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_used_gifts_user ON used_gifts(user_id, used_at DESC);
-- ---------------------------------------------------------------------
-- avatars — preset avatar library
-- ---------------------------------------------------------------------
CREATE TABLE avatars (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT UNIQUE NOT NULL,
url TEXT NOT NULL,
description TEXT,
sort INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
@@ -0,0 +1,159 @@
-- =====================================================================
-- CONTENT SCHEMA — Part 1: Taxonomy & Assets (categories, tags, fonts, music)
-- =====================================================================
SET search_path TO content, public;
-- ---------------------------------------------------------------------
-- categories — hierarchical
-- ---------------------------------------------------------------------
CREATE TABLE categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
parent_id UUID REFERENCES categories(id) ON DELETE SET NULL,
name TEXT NOT NULL,
slug CITEXT NOT NULL UNIQUE,
description TEXT,
image_url TEXT,
icon TEXT,
-- SEO
meta_title TEXT,
meta_description TEXT,
meta_keywords TEXT,
bot_follow BOOLEAN NOT NULL DEFAULT TRUE,
sort INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_categories_parent ON categories(parent_id);
CREATE INDEX idx_categories_active ON categories(is_active) WHERE deleted_at IS NULL;
CREATE TRIGGER tg_categories_updated_at
BEFORE UPDATE ON categories FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- container_categories — many-to-many
-- ---------------------------------------------------------------------
-- Will be created after project_containers table
-- ---------------------------------------------------------------------
-- tags
-- ---------------------------------------------------------------------
CREATE TYPE choose_mode AS ENUM ('FIX','FLEXIBLE','MockUp','MusicVisualizer','VoiceOver');
CREATE TABLE tags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
latin_name TEXT,
slug CITEXT NOT NULL UNIQUE,
applies_to_mode choose_mode,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_tags_active ON tags(is_active) WHERE deleted_at IS NULL;
CREATE TRIGGER tg_tags_updated_at
BEFORE UPDATE ON tags FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- fonts
-- ---------------------------------------------------------------------
CREATE TABLE fonts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL, -- display name
original_name TEXT, -- as registered in AE
system_name TEXT, -- exact OS family name
family TEXT,
weight INT, -- 100-900
style TEXT, -- 'normal' | 'italic'
direction TEXT NOT NULL DEFAULT 'LTR', -- LTR/RTL/Auto
file_url TEXT, -- .ttf/.otf URL
sample_image_url TEXT,
is_premium BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
installed_on_nodes BOOLEAN NOT NULL DEFAULT FALSE,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_fonts_active ON fonts(is_active) WHERE deleted_at IS NULL;
CREATE TRIGGER tg_fonts_updated_at
BEFORE UPDATE ON fonts FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- music_tracks
-- ---------------------------------------------------------------------
CREATE TABLE music_tracks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
caption TEXT,
keywords TEXT,
url TEXT NOT NULL,
waveform_data JSONB, -- precomputed visualization
duration_sec NUMERIC(8,2) NOT NULL,
bpm INT,
genre TEXT,
mood TEXT,
is_premium BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_music_active ON music_tracks(is_active) WHERE deleted_at IS NULL;
CREATE INDEX idx_music_genre ON music_tracks(genre);
CREATE TRIGGER tg_music_updated_at
BEFORE UPDATE ON music_tracks FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- project_servers — render server configs (multi-region)
-- ---------------------------------------------------------------------
CREATE TABLE project_servers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
region TEXT NOT NULL, -- 'tehran','frankfurt',...
ip INET,
physical_path_output TEXT,
default_project_address TEXT,
render_output_location TEXT,
pre_need_folder_address TEXT,
minio_endpoint TEXT,
minio_bucket_templates TEXT,
minio_bucket_outputs TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_project_servers_region ON project_servers(region) WHERE is_active = TRUE;
CREATE TRIGGER tg_project_servers_updated_at
BEFORE UPDATE ON project_servers FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- admin_files — admin-uploaded resources
-- ---------------------------------------------------------------------
CREATE TABLE admin_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT,
url TEXT NOT NULL,
thumbnail_url TEXT,
file_type TEXT,
size_bytes BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
@@ -0,0 +1,143 @@
-- =====================================================================
-- CONTENT SCHEMA — Part 2: Project Containers & Projects (Templates)
-- =====================================================================
SET search_path TO content, public;
-- Tenants can mark projects as private (only their users see them)
-- or use the global FlatRender catalog.
CREATE TYPE resolution_kind AS ENUM ('HD','FullHD','TwoK','FourK');
-- ---------------------------------------------------------------------
-- project_containers — the "product" (template pack)
-- ---------------------------------------------------------------------
CREATE TABLE project_containers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID, -- NULL = global
-- FK to identity.tenants is enforced via service code (cross-schema FK kept loose)
slug CITEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT,
keywords TEXT,
news_text TEXT,
-- Media
image TEXT,
demo TEXT,
full_demo TEXT,
mini_demo TEXT,
demo_script_tag TEXT,
-- Modes & classifications
is_published BOOLEAN NOT NULL DEFAULT FALSE,
is_premium BOOLEAN NOT NULL DEFAULT FALSE,
is_mockup BOOLEAN NOT NULL DEFAULT FALSE,
primary_mode choose_mode NOT NULL DEFAULT 'FLEXIBLE',
-- Stats (denormalized for speed)
rate_avg NUMERIC(3,2),
rate_count INT NOT NULL DEFAULT 0,
view_count BIGINT NOT NULL DEFAULT 0,
use_count BIGINT NOT NULL DEFAULT 0,
sort INT NOT NULL DEFAULT 0,
sort_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_containers_published ON project_containers(is_published, sort_date DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_containers_tenant ON project_containers(tenant_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_containers_name_trgm ON project_containers USING gin (name gin_trgm_ops);
CREATE TRIGGER tg_containers_updated_at
BEFORE UPDATE ON project_containers FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- M2M: container ↔ categories
-- ---------------------------------------------------------------------
CREATE TABLE container_categories (
container_id UUID NOT NULL REFERENCES project_containers(id) ON DELETE CASCADE,
category_id UUID NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
sort INT NOT NULL DEFAULT 0,
PRIMARY KEY (container_id, category_id)
);
CREATE INDEX idx_cc_category ON container_categories(category_id);
-- ---------------------------------------------------------------------
-- M2M: container ↔ tags
-- ---------------------------------------------------------------------
CREATE TABLE container_tags (
container_id UUID NOT NULL REFERENCES project_containers(id) ON DELETE CASCADE,
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (container_id, tag_id)
);
CREATE INDEX idx_ct_tag ON container_tags(tag_id);
-- ---------------------------------------------------------------------
-- projects — one aspect-ratio variant of a container
-- ---------------------------------------------------------------------
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
container_id UUID NOT NULL REFERENCES project_containers(id) ON DELETE CASCADE,
project_server_id UUID REFERENCES project_servers(id),
name TEXT NOT NULL,
description TEXT,
image TEXT,
full_demo TEXT,
demo_script_tag TEXT,
download_link TEXT,
-- AEP file storage
aep_minio_bucket TEXT,
aep_minio_key TEXT,
aep_file_url TEXT,
aep_file_md5 TEXT, -- for node cache check
aep_file_size_bytes BIGINT,
aep_uploaded_at TIMESTAMPTZ,
folder TEXT, -- legacy path on server
-- Geometry
original_width INT NOT NULL,
original_height INT NOT NULL,
aspect TEXT, -- '16:9','9:16','1:1','4:5',...
-- Timing
project_duration_sec NUMERIC(8,2) NOT NULL,
min_duration_sec NUMERIC(8,2),
max_duration_sec NUMERIC(8,2),
free_fps INT NOT NULL DEFAULT 30,
-- Mode
choose_mode choose_mode NOT NULL,
resolution resolution_kind NOT NULL DEFAULT 'FullHD',
vip_factor NUMERIC(4,2) NOT NULL DEFAULT 1.0,
render_aep_comp TEXT NOT NULL DEFAULT 'flatrender', -- main comp name
-- Misc (legacy artifacts to preserve)
shared_layer_image TEXT,
shared_colors_svg TEXT,
shared_color_presets_svg TEXT,
-- Status
is_published BOOLEAN NOT NULL DEFAULT FALSE,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_projects_container ON projects(container_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_projects_published ON projects(is_published) WHERE deleted_at IS NULL;
CREATE INDEX idx_projects_aep_md5 ON projects(aep_file_md5) WHERE aep_file_md5 IS NOT NULL;
CREATE TRIGGER tg_projects_updated_at
BEFORE UPDATE ON projects FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
+368
View File
@@ -0,0 +1,368 @@
-- =====================================================================
-- CONTENT SCHEMA — Part 3: Scenes & Editable Elements
-- =====================================================================
SET search_path TO content, public;
CREATE TYPE scene_kind AS ENUM ('Normal','Config','DesignStart','DesignEnd');
CREATE TYPE content_element_type AS ENUM (
'Text','TextArea','Media','Audio','Voiceover',
'CheckBox','DropDown','Fill','Color','Number',
'Date','Toggle','Slider','Counter','Hidden'
);
CREATE TYPE justify_kind AS ENUM ('LEFT_JUSTIFY','CENTER_JUSTIFY','RIGHT_JUSTIFY','FULL_JUSTIFY');
CREATE TYPE ai_input_type AS ENUM ('None','TitleSuggest','BodySuggest','TranslateRtl','TranslateLtr','RemoveBG','UpscaleImage','TTS');
CREATE TYPE repeat_sort_strategy AS ENUM ('Manual','Alphabetical','Numerical','InsertOrder');
CREATE TYPE attr_value_kind AS ENUM ('fill','stroke','tracking','dropshadow');
-- ---------------------------------------------------------------------
-- scenes
-- ---------------------------------------------------------------------
CREATE TABLE scenes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
-- Identity (maps to AE comp name)
key TEXT NOT NULL, -- matches AE comp name
title TEXT NOT NULL,
localized_title JSONB, -- {"fa":"...","en":"..."}
-- Type
scene_type scene_kind NOT NULL DEFAULT 'Normal',
-- Media
image TEXT,
demo TEXT,
scene_color_svg TEXT, -- SVG color preview (legacy)
snapshot_url TEXT, -- pre-rendered representative frame
-- Animation flags
generate_kf BOOLEAN NOT NULL DEFAULT FALSE,
-- Timing
default_duration_sec NUMERIC(8,2),
min_duration_sec NUMERIC(8,2),
max_duration_sec NUMERIC(8,2),
overlap_at_end_sec NUMERIC(6,2) NOT NULL DEFAULT 0,
can_handle_duration BOOLEAN NOT NULL DEFAULT TRUE,
-- Customization
manual_color_selection BOOLEAN NOT NULL DEFAULT FALSE,
sort INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
UNIQUE (project_id, key)
);
CREATE INDEX idx_scenes_project ON scenes(project_id, sort) WHERE deleted_at IS NULL;
CREATE INDEX idx_scenes_type ON scenes(project_id, scene_type);
CREATE TRIGGER tg_scenes_updated_at
BEFORE UPDATE ON scenes FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- M2M: scenes ↔ categories
-- ---------------------------------------------------------------------
CREATE TABLE scene_categories (
scene_id UUID NOT NULL REFERENCES scenes(id) ON DELETE CASCADE,
category_id UUID NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
PRIMARY KEY (scene_id, category_id)
);
-- ---------------------------------------------------------------------
-- repeater_items — repeating sub-blocks within a scene
-- ---------------------------------------------------------------------
CREATE TABLE repeater_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scene_id UUID NOT NULL REFERENCES scenes(id) ON DELETE CASCADE,
title TEXT NOT NULL,
repeat_box_key TEXT NOT NULL, -- AE layer name of container
repeat_item_key TEXT NOT NULL, -- AE layer name of item template
max_repeat_count INT NOT NULL DEFAULT 10,
user_can_change_sort BOOLEAN NOT NULL DEFAULT TRUE,
repeat_sort_strategy repeat_sort_strategy NOT NULL DEFAULT 'Manual',
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_repeaters_scene ON repeater_items(scene_id);
CREATE TRIGGER tg_repeaters_updated_at
BEFORE UPDATE ON repeater_items FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- scene_content_elements — every editable field in a scene
-- ---------------------------------------------------------------------
CREATE TABLE scene_content_elements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scene_id UUID NOT NULL REFERENCES scenes(id) ON DELETE CASCADE,
repeater_item_id UUID REFERENCES repeater_items(id) ON DELETE CASCADE,
-- Identity (maps to AE frl_/frd_ layer name)
key TEXT NOT NULL,
title TEXT NOT NULL,
localized_title JSONB,
hint TEXT,
type content_element_type NOT NULL,
default_value TEXT,
-- Text-specific
font_id UUID REFERENCES fonts(id),
font_face TEXT,
font_face_name TEXT,
font_size INT,
default_font_size INT,
default_font_face TEXT,
is_font_changeable BOOLEAN NOT NULL DEFAULT TRUE,
is_font_size_changeable BOOLEAN NOT NULL DEFAULT TRUE,
justify justify_kind NOT NULL DEFAULT 'CENTER_JUSTIFY',
can_justify BOOLEAN NOT NULL DEFAULT TRUE,
position_in_container INT NOT NULL DEFAULT 0, -- 0-8 (see JSX positionMode)
is_text_box BOOLEAN NOT NULL DEFAULT FALSE,
max_size INT, -- max char count
direction_layer_key TEXT, -- companion frd_ for RTL
direction_layer_value INT NOT NULL DEFAULT 0, -- 0=LTR, 1=RTL
-- Media-specific
video_support BOOLEAN NOT NULL DEFAULT FALSE,
min_duration_sec NUMERIC(6,2),
max_duration_sec NUMERIC(6,2),
width INT,
height INT,
thumbnail TEXT,
-- Dropdown / mapped list
mapped_list JSONB, -- [{"label","value"}, ...]
counter_mode TEXT,
-- AI
ai_input_type ai_input_type NOT NULL DEFAULT 'None',
-- Visibility / linking
is_hidden BOOLEAN NOT NULL DEFAULT FALSE,
is_focused BOOLEAN NOT NULL DEFAULT FALSE,
opacity_controller_key TEXT, -- ref another element's key
-- Design pattern variants (legacy DP1-4 system)
dp1_image TEXT, dp1_title TEXT,
dp2_image TEXT, dp2_title TEXT,
dp3_image TEXT, dp3_title TEXT,
dp4_image TEXT, dp4_title TEXT,
-- Repeater virtualization
virtual_count INT NOT NULL DEFAULT 1,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (scene_id, key)
);
CREATE INDEX idx_sce_scene ON scene_content_elements(scene_id, sort);
CREATE INDEX idx_sce_repeater ON scene_content_elements(repeater_item_id) WHERE repeater_item_id IS NOT NULL;
CREATE TRIGGER tg_sce_updated_at
BEFORE UPDATE ON scene_content_elements FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- scene_color_elements — color zones (frd_ data layers per scene)
-- ---------------------------------------------------------------------
CREATE TABLE scene_color_elements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scene_id UUID NOT NULL REFERENCES scenes(id) ON DELETE CASCADE,
element_key TEXT NOT NULL, -- matches frd_ layer name
title TEXT NOT NULL,
icon TEXT,
attr_value attr_value_kind NOT NULL DEFAULT 'fill',
default_color TEXT NOT NULL, -- '#RRGGBB' or 'r,g,b'
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (scene_id, element_key)
);
CREATE INDEX idx_sce_color_scene ON scene_color_elements(scene_id, sort);
CREATE TRIGGER tg_sce_color_updated_at
BEFORE UPDATE ON scene_color_elements FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- scene_color_presets — theme presets per scene
-- ---------------------------------------------------------------------
CREATE TABLE scene_color_presets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scene_id UUID NOT NULL REFERENCES scenes(id) ON DELETE CASCADE,
name TEXT,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE scene_color_preset_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
preset_id UUID NOT NULL REFERENCES scene_color_presets(id) ON DELETE CASCADE,
element_key TEXT NOT NULL,
value TEXT NOT NULL,
sort INT NOT NULL DEFAULT 0
);
CREATE INDEX idx_sc_preset_items_preset ON scene_color_preset_items(preset_id);
-- ---------------------------------------------------------------------
-- shared_colors — global colors across project (frshare comp)
-- ---------------------------------------------------------------------
CREATE TABLE shared_colors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
element_key TEXT NOT NULL,
title TEXT NOT NULL,
icon TEXT,
attr_value attr_value_kind NOT NULL DEFAULT 'fill',
default_color TEXT NOT NULL,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (project_id, element_key)
);
CREATE INDEX idx_shared_colors_project ON shared_colors(project_id, sort);
CREATE TRIGGER tg_shared_colors_updated_at
BEFORE UPDATE ON shared_colors FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- shared_color_presets — project-wide palette themes
-- ---------------------------------------------------------------------
CREATE TABLE shared_color_presets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name TEXT,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE shared_color_preset_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
preset_id UUID NOT NULL REFERENCES shared_color_presets(id) ON DELETE CASCADE,
element_key TEXT NOT NULL,
value TEXT NOT NULL,
sort INT NOT NULL DEFAULT 0
);
CREATE INDEX idx_shared_preset_items_preset ON shared_color_preset_items(preset_id);
-- ---------------------------------------------------------------------
-- shared_layers — global text/media layers across all scenes
-- ---------------------------------------------------------------------
CREATE TABLE shared_layers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
key TEXT NOT NULL,
title TEXT NOT NULL,
localized_title JSONB,
hint TEXT,
type content_element_type NOT NULL,
default_value TEXT,
font_id UUID REFERENCES fonts(id),
font_face TEXT,
font_face_name TEXT,
font_size INT,
default_font_size INT,
default_font_face TEXT,
is_font_changeable BOOLEAN NOT NULL DEFAULT TRUE,
is_font_size_changeable BOOLEAN NOT NULL DEFAULT TRUE,
justify justify_kind NOT NULL DEFAULT 'CENTER_JUSTIFY',
can_justify BOOLEAN NOT NULL DEFAULT TRUE,
position_in_container INT NOT NULL DEFAULT 0,
is_text_box BOOLEAN NOT NULL DEFAULT FALSE,
max_size INT,
direction_layer_key TEXT,
direction_layer_value INT NOT NULL DEFAULT 0,
video_support BOOLEAN NOT NULL DEFAULT FALSE,
min_duration_sec NUMERIC(6,2),
max_duration_sec NUMERIC(6,2),
width INT,
height INT,
thumbnail TEXT,
mapped_list JSONB,
counter_mode TEXT,
ai_input_type ai_input_type NOT NULL DEFAULT 'None',
is_hidden BOOLEAN NOT NULL DEFAULT FALSE,
is_focused BOOLEAN NOT NULL DEFAULT FALSE,
dp1_image TEXT, dp1_title TEXT,
dp2_image TEXT, dp2_title TEXT,
dp3_image TEXT, dp3_title TEXT,
dp4_image TEXT, dp4_title TEXT,
virtual_count INT NOT NULL DEFAULT 1,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (project_id, key)
);
CREATE INDEX idx_shared_layers_project ON shared_layers(project_id, sort);
CREATE TRIGGER tg_shared_layers_updated_at
BEFORE UPDATE ON shared_layers FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- template_svg_previews — NEW: drop-an-image → traced SVG for live color preview
-- ---------------------------------------------------------------------
CREATE TABLE template_svg_previews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
scene_id UUID REFERENCES scenes(id) ON DELETE CASCADE,
-- Exactly one of project_id or scene_id should be set
source_image_url TEXT, -- the dropped image
svg_url TEXT NOT NULL, -- in MinIO
thumbnail_url TEXT,
-- Maps each SVG <path data-color-key="frd_bg"> to a color element
color_zones JSONB NOT NULL,
-- Example: [{"element_key":"frd_bg","detected_color":"#1A1A2E","bbox":[x,y,w,h]}]
width INT,
height INT,
generation_method TEXT, -- 'auto','manual','ai-assisted'
generated_by_ai BOOLEAN NOT NULL DEFAULT FALSE,
quality_score NUMERIC(3,2), -- 0-1 confidence
created_by_user_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CHECK ((project_id IS NULL) <> (scene_id IS NULL)) -- exactly one
);
CREATE INDEX idx_svg_previews_project ON template_svg_previews(project_id);
CREATE INDEX idx_svg_previews_scene ON template_svg_previews(scene_id);
CREATE TRIGGER tg_svg_previews_updated_at
BEFORE UPDATE ON template_svg_previews FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
@@ -0,0 +1,160 @@
-- =====================================================================
-- CONTENT SCHEMA — Part 4: Characters & Preset Stories
-- =====================================================================
SET search_path TO content, public;
-- ---------------------------------------------------------------------
-- scene_characters — character elements per scene
-- ---------------------------------------------------------------------
CREATE TABLE scene_characters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scene_id UUID NOT NULL REFERENCES scenes(id) ON DELETE CASCADE,
key TEXT NOT NULL,
name TEXT NOT NULL,
icon TEXT,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (scene_id, key)
);
CREATE INDEX idx_scene_chars_scene ON scene_characters(scene_id);
CREATE TRIGGER tg_scene_chars_updated_at
BEFORE UPDATE ON scene_characters FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- scene_character_controllers — animation properties of a character
-- ---------------------------------------------------------------------
CREATE TABLE scene_character_controllers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scene_character_id UUID NOT NULL REFERENCES scene_characters(id) ON DELETE CASCADE,
name TEXT NOT NULL,
key TEXT NOT NULL,
default_value TEXT,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_scene_char_ctrl_char ON scene_character_controllers(scene_character_id);
CREATE TRIGGER tg_scene_char_ctrl_updated_at
BEFORE UPDATE ON scene_character_controllers FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- scene_controller_options — discrete value options per controller
-- ---------------------------------------------------------------------
CREATE TABLE scene_controller_options (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
controller_id UUID NOT NULL REFERENCES scene_character_controllers(id) ON DELETE CASCADE,
name TEXT NOT NULL,
icon TEXT,
value TEXT NOT NULL,
sort INT NOT NULL DEFAULT 0
);
CREATE INDEX idx_scene_ctrl_opt_ctrl ON scene_controller_options(controller_id);
-- ---------------------------------------------------------------------
-- project_character_controllers — project-wide character animation defs
-- ---------------------------------------------------------------------
CREATE TABLE project_character_controllers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name TEXT NOT NULL,
key TEXT NOT NULL,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (project_id, key)
);
CREATE INDEX idx_proj_char_ctrl_project ON project_character_controllers(project_id);
CREATE TRIGGER tg_proj_char_ctrl_updated_at
BEFORE UPDATE ON project_character_controllers FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- project_character_controller_options
-- ---------------------------------------------------------------------
CREATE TABLE project_character_controller_options (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
controller_id UUID NOT NULL REFERENCES project_character_controllers(id) ON DELETE CASCADE,
name TEXT NOT NULL,
icon TEXT,
value TEXT NOT NULL,
sort INT NOT NULL DEFAULT 0
);
-- ---------------------------------------------------------------------
-- project_character_presets — named bundles of controller values
-- ---------------------------------------------------------------------
CREATE TABLE project_character_presets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
key UUID NOT NULL DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
icon TEXT,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_char_presets_project ON project_character_presets(project_id);
CREATE TRIGGER tg_char_presets_updated_at
BEFORE UPDATE ON project_character_presets FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- preset_character_controllers — controller values inside a preset
-- ---------------------------------------------------------------------
CREATE TABLE preset_character_controllers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
character_preset_id UUID NOT NULL REFERENCES project_character_presets(id) ON DELETE CASCADE,
name TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_preset_char_ctrl_preset ON preset_character_controllers(character_preset_id);
-- ---------------------------------------------------------------------
-- preset_stories — pre-made scene combinations
-- ---------------------------------------------------------------------
CREATE TABLE preset_stories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
demo TEXT,
music_id UUID REFERENCES music_tracks(id),
scenes_spa TEXT, -- legacy serialized SPA (kept for migration)
sort INT NOT NULL DEFAULT 0,
is_published BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_preset_stories_project ON preset_stories(project_id) WHERE deleted_at IS NULL;
CREATE TRIGGER tg_preset_stories_updated_at
BEFORE UPDATE ON preset_stories FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- preset_scenes — which scenes appear in a preset story and order
-- ---------------------------------------------------------------------
CREATE TABLE preset_scenes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
preset_story_id UUID NOT NULL REFERENCES preset_stories(id) ON DELETE CASCADE,
scene_id UUID NOT NULL REFERENCES scenes(id) ON DELETE CASCADE,
sort INT NOT NULL DEFAULT 0,
default_duration_sec NUMERIC(8,2),
UNIQUE (preset_story_id, sort)
);
CREATE INDEX idx_preset_scenes_story ON preset_scenes(preset_story_id, sort);
+249
View File
@@ -0,0 +1,249 @@
-- =====================================================================
-- CONTENT SCHEMA — Part 5: CMS (blogs, comments, slides, routes, settings)
-- =====================================================================
SET search_path TO content, public;
-- ---------------------------------------------------------------------
-- blogs / landings
-- ---------------------------------------------------------------------
CREATE TYPE blog_kind AS ENUM ('Blog','Landing');
CREATE TABLE blogs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID, -- NULL = global
kind blog_kind NOT NULL DEFAULT 'Blog',
slug CITEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
short_description TEXT,
content TEXT NOT NULL,
-- SEO
meta_title TEXT,
meta_description TEXT,
meta_keywords TEXT,
include_in_site_map BOOLEAN NOT NULL DEFAULT TRUE,
-- Media
image TEXT,
cover TEXT,
-- Author
author_user_id UUID, -- references identity.users (loose)
author_display_name TEXT,
is_published BOOLEAN NOT NULL DEFAULT FALSE,
publish_date TIMESTAMPTZ,
view_count BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_blogs_published ON blogs(is_published, publish_date DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_blogs_tenant ON blogs(tenant_id) WHERE deleted_at IS NULL;
CREATE TRIGGER tg_blogs_updated_at
BEFORE UPDATE ON blogs FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- comments — on blogs or project containers
-- ---------------------------------------------------------------------
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID,
user_id UUID NOT NULL, -- references identity.users (loose)
blog_id UUID REFERENCES blogs(id) ON DELETE CASCADE,
container_id UUID REFERENCES project_containers(id) ON DELETE CASCADE,
parent_comment_id UUID REFERENCES comments(id) ON DELETE CASCADE,
content TEXT NOT NULL,
rate NUMERIC(3,2), -- 0-5
is_approved BOOLEAN NOT NULL DEFAULT FALSE,
is_pinned BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CHECK (
(blog_id IS NOT NULL)::INT + (container_id IS NOT NULL)::INT = 1
)
);
CREATE INDEX idx_comments_blog ON comments(blog_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_comments_container ON comments(container_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_comments_user ON comments(user_id);
CREATE INDEX idx_comments_pending ON comments(is_approved) WHERE is_approved = FALSE AND deleted_at IS NULL;
CREATE TRIGGER tg_comments_updated_at
BEFORE UPDATE ON comments FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- home_page_events — marketing banners / events
-- ---------------------------------------------------------------------
CREATE TABLE home_page_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID,
title TEXT,
subtitle TEXT,
description TEXT,
badge TEXT,
badge_class TEXT,
button_text TEXT,
button_url TEXT,
button_class TEXT,
color TEXT,
background_color TEXT,
text_color TEXT,
image TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
sort INT NOT NULL DEFAULT 0,
starts_at TIMESTAMPTZ,
ends_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_home_events_active ON home_page_events(tenant_id, is_active);
CREATE TRIGGER tg_home_events_updated_at
BEFORE UPDATE ON home_page_events FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- new_slides — carousel
-- ---------------------------------------------------------------------
CREATE TYPE slide_type AS ENUM ('Hero','Promo','Tutorial','Category','Custom');
CREATE TABLE new_slides (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID,
keyword TEXT,
title TEXT,
image TEXT,
parameter TEXT,
slide_type slide_type NOT NULL DEFAULT 'Hero',
expire_date TIMESTAMPTZ,
sort INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_slides_active ON new_slides(tenant_id, is_active);
CREATE TRIGGER tg_slides_updated_at
BEFORE UPDATE ON new_slides FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- internal_routes / custom_routes — SEO + redirects
-- ---------------------------------------------------------------------
CREATE TABLE internal_routes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID,
name TEXT,
image TEXT,
slug CITEXT NOT NULL,
priority INT NOT NULL DEFAULT 5,
last_date TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE custom_routes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID,
target TEXT NOT NULL, -- source path
destination TEXT NOT NULL, -- redirect target
redirect_code INT NOT NULL DEFAULT 301,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_custom_routes_active ON custom_routes(tenant_id, is_active);
-- ---------------------------------------------------------------------
-- website_settings — key-value config (per tenant)
-- ---------------------------------------------------------------------
CREATE TABLE website_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID, -- NULL = global default
key TEXT NOT NULL,
value JSONB NOT NULL,
description TEXT,
is_secret BOOLEAN NOT NULL DEFAULT FALSE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, key)
);
CREATE INDEX idx_settings_tenant ON website_settings(tenant_id);
-- ---------------------------------------------------------------------
-- learn — help articles
-- ---------------------------------------------------------------------
CREATE TABLE learn (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID,
title TEXT NOT NULL,
body TEXT,
demo_url TEXT,
mode TEXT,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ---------------------------------------------------------------------
-- trainings — video tutorials
-- ---------------------------------------------------------------------
CREATE TABLE trainings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID,
title TEXT NOT NULL,
description TEXT,
video_url TEXT,
thumbnail_url TEXT,
sort INT NOT NULL DEFAULT 0,
is_published BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ---------------------------------------------------------------------
-- favorite_folders — user collections of templates
-- ---------------------------------------------------------------------
CREATE TABLE favorite_folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL, -- references identity.users
tenant_id UUID NOT NULL,
name TEXT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_fav_folders_user ON favorite_folders(user_id);
CREATE TRIGGER tg_fav_folders_updated_at
BEFORE UPDATE ON favorite_folders FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- favorite_containers — saved template references in user's folders
-- ---------------------------------------------------------------------
CREATE TABLE favorite_containers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
tenant_id UUID NOT NULL,
container_id UUID NOT NULL REFERENCES project_containers(id) ON DELETE CASCADE,
folder_id UUID REFERENCES favorite_folders(id) ON DELETE SET NULL,
note TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, container_id)
);
CREATE INDEX idx_fav_containers_user ON favorite_containers(user_id);
@@ -0,0 +1,335 @@
-- =====================================================================
-- STUDIO SCHEMA — Saved Projects (User's Project Instances)
-- =====================================================================
-- This is where user input lives: their text values, color choices,
-- media uploads, music, voiceover, etc. The render service reads from
-- here to build the JSX.
-- =====================================================================
SET search_path TO studio, public;
CREATE TYPE saved_project_type AS ENUM ('Draft','Active','Archived','Trash');
-- ---------------------------------------------------------------------
-- saved_projects — root of user's project instance
-- ---------------------------------------------------------------------
CREATE TABLE saved_projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL, -- references identity.tenants
user_id UUID NOT NULL, -- references identity.users
-- Source template snapshot (so deleting template doesn't break this)
original_project_id UUID NOT NULL, -- references content.projects
original_project_name TEXT NOT NULL,
original_container_id UUID,
original_container_slug CITEXT,
-- Identity
name TEXT NOT NULL,
image TEXT,
type saved_project_type NOT NULL DEFAULT 'Draft',
-- Snapshot of project metadata
frame_rate INT NOT NULL DEFAULT 30,
project_duration_sec NUMERIC(8,2) NOT NULL,
resolution TEXT NOT NULL, -- HD/FullHD/TwoK/FourK
choose_mode TEXT NOT NULL, -- FIX/FLEXIBLE/MockUp/MusicVisualizer
vip_factor NUMERIC(4,2) NOT NULL DEFAULT 1.0,
-- =====================================
-- Audio (NEW — voiceover + volumes)
-- =====================================
music_file_id UUID, -- references file_mgr.user_files
music_track_id UUID, -- references content.music_tracks (library)
music_volume NUMERIC(4,3) NOT NULL DEFAULT 0.7 CHECK (music_volume BETWEEN 0 AND 1),
voiceover_file_id UUID, -- references file_mgr.user_files
voiceover_volume NUMERIC(4,3) NOT NULL DEFAULT 1.0 CHECK (voiceover_volume BETWEEN 0 AND 1),
voiceover_recorded_in_browser BOOLEAN NOT NULL DEFAULT FALSE,
sfx_volume NUMERIC(4,3) NOT NULL DEFAULT 1.0 CHECK (sfx_volume BETWEEN 0 AND 1),
sfx_enabled BOOLEAN NOT NULL DEFAULT TRUE,
-- Music visualizer mode
audio_visualizer_music_url TEXT,
audio_visualizer_duration_sec NUMERIC(8,2),
-- =====================================
-- Customization options
-- =====================================
manual_color_picker BOOLEAN NOT NULL DEFAULT FALSE,
selected_preset_story_id UUID, -- references content.preset_stories
-- Auto-save / state
last_edit_step TEXT, -- track wizard step
edit_state JSONB NOT NULL DEFAULT '{}'::jsonb,
create_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_edit_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_saved_proj_user ON saved_projects(user_id, last_edit_date DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_saved_proj_tenant ON saved_projects(tenant_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_saved_proj_original ON saved_projects(original_project_id);
CREATE INDEX idx_saved_proj_name_trgm ON saved_projects USING gin (name gin_trgm_ops);
CREATE TRIGGER tg_saved_projects_updated_at
BEFORE UPDATE ON saved_projects FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- saved_scenes — user's chosen scenes (FLEXIBLE mode = multiple per project)
-- ---------------------------------------------------------------------
CREATE TABLE saved_scenes (
id BIGSERIAL PRIMARY KEY,
saved_project_id UUID NOT NULL REFERENCES saved_projects(id) ON DELETE CASCADE,
-- Snapshot from original scene
original_scene_id UUID, -- references content.scenes
key TEXT NOT NULL, -- AE comp name
title TEXT,
image TEXT,
demo TEXT,
scene_color_svg TEXT,
scene_type TEXT NOT NULL, -- Normal/Config/DesignStart/DesignEnd
-- Timing
sort INT NOT NULL,
scene_length_sec NUMERIC(8,2) NOT NULL,
min_duration_sec NUMERIC(8,2),
max_duration_sec NUMERIC(8,2),
overlap_at_end_sec NUMERIC(6,2) NOT NULL DEFAULT 0,
can_handle_duration BOOLEAN NOT NULL DEFAULT TRUE,
-- Customization
manual_color_selection BOOLEAN NOT NULL DEFAULT FALSE,
selected_color_preset_id UUID, -- ref to saved_scene_color_presets
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_saved_scenes_proj ON saved_scenes(saved_project_id, sort);
CREATE TRIGGER tg_saved_scenes_updated_at
BEFORE UPDATE ON saved_scenes FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- saved_scene_contents — user-filled content values per scene
-- ---------------------------------------------------------------------
CREATE TABLE saved_scene_contents (
id BIGSERIAL PRIMARY KEY,
saved_scene_id BIGINT NOT NULL REFERENCES saved_scenes(id) ON DELETE CASCADE,
-- Element identity
key TEXT NOT NULL,
title TEXT,
localized_title JSONB,
hint TEXT,
type TEXT NOT NULL, -- Text/Media/Audio/Voiceover/...
-- User value
value TEXT, -- text or file UUID
value_file_id UUID, -- if file: references file_mgr.user_files
inserted_file_type TEXT, -- Image/Video/Audio
file_url_cached TEXT, -- resolved CDN url at last save
file_url_cached_at TIMESTAMPTZ,
-- Text styling
font_face TEXT,
font_face_name TEXT,
font_size INT,
default_font_size INT,
default_font_face TEXT,
justify TEXT,
position_in_container INT NOT NULL DEFAULT 0,
direction_layer_value INT NOT NULL DEFAULT 0,
is_text_box BOOLEAN NOT NULL DEFAULT FALSE,
-- AI assistance
ai_input_type TEXT,
-- Design pattern choice
selected_dp INT, -- 1-4
-- Repeater
repeater_item_key TEXT,
repeater_index INT,
-- Selection state
is_focused BOOLEAN NOT NULL DEFAULT FALSE,
status TEXT,
mapped_list JSONB,
thumbnail TEXT,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_saved_contents_scene ON saved_scene_contents(saved_scene_id, sort);
CREATE INDEX idx_saved_contents_filerefs ON saved_scene_contents(value_file_id) WHERE value_file_id IS NOT NULL;
CREATE TRIGGER tg_saved_contents_updated_at
BEFORE UPDATE ON saved_scene_contents FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- saved_scene_colors — color choices per scene
-- ---------------------------------------------------------------------
CREATE TABLE saved_scene_colors (
id BIGSERIAL PRIMARY KEY,
saved_scene_id BIGINT NOT NULL REFERENCES saved_scenes(id) ON DELETE CASCADE,
element_key TEXT NOT NULL,
title TEXT,
icon TEXT,
attr_value TEXT NOT NULL DEFAULT 'fill',
value TEXT NOT NULL, -- hex or rgb
is_selected BOOLEAN NOT NULL DEFAULT TRUE,
sort INT NOT NULL DEFAULT 0,
UNIQUE (saved_scene_id, element_key)
);
CREATE INDEX idx_saved_colors_scene ON saved_scene_colors(saved_scene_id);
-- ---------------------------------------------------------------------
-- saved_scene_color_presets + items
-- ---------------------------------------------------------------------
CREATE TABLE saved_scene_color_presets (
id BIGSERIAL PRIMARY KEY,
saved_scene_id BIGINT NOT NULL REFERENCES saved_scenes(id) ON DELETE CASCADE,
is_selected BOOLEAN NOT NULL DEFAULT FALSE,
sort INT NOT NULL DEFAULT 0
);
CREATE TABLE saved_scene_color_preset_items (
id BIGSERIAL PRIMARY KEY,
preset_id BIGINT NOT NULL REFERENCES saved_scene_color_presets(id) ON DELETE CASCADE,
element_key TEXT NOT NULL,
value TEXT NOT NULL,
sort INT NOT NULL DEFAULT 0
);
CREATE INDEX idx_saved_scp_items_preset ON saved_scene_color_preset_items(preset_id);
-- ---------------------------------------------------------------------
-- saved_scene_characters
-- ---------------------------------------------------------------------
CREATE TABLE saved_scene_characters (
id BIGSERIAL PRIMARY KEY,
saved_scene_id BIGINT NOT NULL REFERENCES saved_scenes(id) ON DELETE CASCADE,
key UUID NOT NULL,
name TEXT,
icon TEXT
);
CREATE INDEX idx_saved_chars_scene ON saved_scene_characters(saved_scene_id);
-- ---------------------------------------------------------------------
-- saved_scene_character_controllers
-- ---------------------------------------------------------------------
CREATE TABLE saved_scene_character_controllers (
id BIGSERIAL PRIMARY KEY,
saved_scene_character_id BIGINT NOT NULL REFERENCES saved_scene_characters(id) ON DELETE CASCADE,
name TEXT,
key TEXT NOT NULL,
value TEXT NOT NULL,
sort INT NOT NULL DEFAULT 0
);
CREATE INDEX idx_saved_char_ctrl_char ON saved_scene_character_controllers(saved_scene_character_id);
-- ---------------------------------------------------------------------
-- saved_shared_colors — project-level color choices
-- ---------------------------------------------------------------------
CREATE TABLE saved_shared_colors (
id BIGSERIAL PRIMARY KEY,
saved_project_id UUID NOT NULL REFERENCES saved_projects(id) ON DELETE CASCADE,
element_key TEXT NOT NULL,
title TEXT,
icon TEXT,
attr_value TEXT NOT NULL DEFAULT 'fill',
value TEXT NOT NULL,
is_selected BOOLEAN NOT NULL DEFAULT TRUE,
sort INT NOT NULL DEFAULT 0,
UNIQUE (saved_project_id, element_key)
);
CREATE INDEX idx_saved_shared_colors_proj ON saved_shared_colors(saved_project_id);
-- ---------------------------------------------------------------------
-- saved_shared_color_presets
-- ---------------------------------------------------------------------
CREATE TABLE saved_shared_color_presets (
id BIGSERIAL PRIMARY KEY,
saved_project_id UUID NOT NULL REFERENCES saved_projects(id) ON DELETE CASCADE,
name TEXT,
is_selected BOOLEAN NOT NULL DEFAULT FALSE,
sort INT NOT NULL DEFAULT 0
);
CREATE TABLE saved_shared_color_preset_items (
id BIGSERIAL PRIMARY KEY,
preset_id BIGINT NOT NULL REFERENCES saved_shared_color_presets(id) ON DELETE CASCADE,
element_key TEXT NOT NULL,
value TEXT NOT NULL,
sort INT NOT NULL DEFAULT 0
);
CREATE INDEX idx_saved_sscp_items_preset ON saved_shared_color_preset_items(preset_id);
-- ---------------------------------------------------------------------
-- saved_shared_layers — project-level layer values
-- ---------------------------------------------------------------------
CREATE TABLE saved_shared_layers (
id BIGSERIAL PRIMARY KEY,
saved_project_id UUID NOT NULL REFERENCES saved_projects(id) ON DELETE CASCADE,
key TEXT NOT NULL,
title TEXT,
localized_title JSONB,
hint TEXT,
type TEXT NOT NULL,
value TEXT,
value_file_id UUID,
file_url_cached TEXT,
file_url_cached_at TIMESTAMPTZ,
font_face TEXT,
font_face_name TEXT,
font_size INT,
default_font_size INT,
default_font_face TEXT,
justify TEXT,
position_in_container INT NOT NULL DEFAULT 0,
direction_layer_value INT NOT NULL DEFAULT 0,
is_text_box BOOLEAN NOT NULL DEFAULT FALSE,
ai_input_type TEXT,
mapped_list JSONB,
thumbnail TEXT,
width INT,
height INT,
min_duration_sec NUMERIC(6,2),
max_duration_sec NUMERIC(6,2),
is_focused BOOLEAN NOT NULL DEFAULT FALSE,
is_font_changeable BOOLEAN NOT NULL DEFAULT TRUE,
is_font_size_changeable BOOLEAN NOT NULL DEFAULT TRUE,
status TEXT,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (saved_project_id, key)
);
CREATE INDEX idx_saved_shared_layers_proj ON saved_shared_layers(saved_project_id);
CREATE TRIGGER tg_saved_shared_layers_updated_at
BEFORE UPDATE ON saved_shared_layers FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
+176
View File
@@ -0,0 +1,176 @@
-- =====================================================================
-- RENDER SCHEMA — Part 1: Farm Nodes & Health
-- =====================================================================
SET search_path TO render, public;
CREATE TYPE node_status AS ENUM ('Ready','Busy','Offline','Maintenance','Crashed','Updating','Disabled');
CREATE TYPE node_kind AS ENUM ('Shared','Dedicated','Spot');
CREATE TYPE render_type AS ENUM ('Free','Paid','Snapshot','Mockup');
CREATE TYPE ae_version AS ENUM ('2020','2021','2022','2023','2024','2025');
-- ---------------------------------------------------------------------
-- render_nodes — registry of farm machines
-- ---------------------------------------------------------------------
CREATE TABLE render_nodes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
region TEXT NOT NULL, -- 'tehran','frankfurt',...
-- Network
node_ip INET NOT NULL,
worker_port INT NOT NULL DEFAULT 5555,
public_endpoint TEXT,
-- Spec
ram_gb INT,
cpu_cores INT,
gpu_model TEXT,
storage_gb INT,
-- Software
current_ae_version ae_version NOT NULL,
available_ae_versions TEXT[] NOT NULL DEFAULT '{}',
node_agent_version TEXT,
last_update_at TIMESTAMPTZ,
last_update_error TEXT,
-- Ownership
node_kind node_kind NOT NULL DEFAULT 'Shared',
owner_user_id UUID, -- references identity.users (for Dedicated)
owner_tenant_id UUID, -- references identity.tenants
-- State
status node_status NOT NULL DEFAULT 'Offline',
current_job_id UUID, -- references render_jobs
current_frame_job_id UUID, -- references frame_jobs
job_started_at TIMESTAMPTZ,
render_type render_type, -- which queue it's serving now
-- Health (denormalized for hot reads)
last_heartbeat_at TIMESTAMPTZ,
last_cpu_pct INT,
last_ram_available_mb INT,
ae_running BOOLEAN NOT NULL DEFAULT FALSE,
-- Stats
lifetime_task_count BIGINT NOT NULL DEFAULT 0,
lifetime_crash_count INT NOT NULL DEFAULT 0,
consecutive_failures INT NOT NULL DEFAULT 0,
-- Scheduling
priority INT NOT NULL DEFAULT 100,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
accepts_new_jobs BOOLEAN NOT NULL DEFAULT TRUE,
-- Maintenance
last_maintenance_at TIMESTAMPTZ,
next_maintenance_at TIMESTAMPTZ,
maintenance_reason TEXT,
-- Local cache state (templates the node has downloaded)
cached_template_md5s TEXT[] NOT NULL DEFAULT '{}',
cache_used_gb INT NOT NULL DEFAULT 0,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_nodes_region_status ON render_nodes(region, status) WHERE is_active = TRUE;
CREATE INDEX idx_nodes_ready ON render_nodes(region, priority DESC)
WHERE status = 'Ready' AND accepts_new_jobs = TRUE AND is_active = TRUE;
CREATE INDEX idx_nodes_owner ON render_nodes(owner_user_id) WHERE node_kind = 'Dedicated';
CREATE INDEX idx_nodes_heartbeat ON render_nodes(last_heartbeat_at) WHERE is_active = TRUE;
CREATE UNIQUE INDEX uq_nodes_ip_port ON render_nodes(node_ip, worker_port);
CREATE TRIGGER tg_render_nodes_updated_at
BEFORE UPDATE ON render_nodes FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- node_health_logs — historical heartbeat data (partitioned monthly)
-- ---------------------------------------------------------------------
CREATE TABLE node_health_logs (
id BIGSERIAL,
node_id UUID NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
status node_status NOT NULL,
cpu_pct INT,
ram_available_mb INT,
ae_running BOOLEAN,
current_job_id UUID,
current_frame INT,
-- Templates cached (size summary only)
cache_used_gb INT,
PRIMARY KEY (id, recorded_at)
) PARTITION BY RANGE (recorded_at);
CREATE INDEX idx_node_health_node ON node_health_logs(node_id, recorded_at DESC);
CREATE TABLE node_health_logs_y2026m01
PARTITION OF node_health_logs
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
-- ---------------------------------------------------------------------
-- node_crashes — every detected AE crash
-- ---------------------------------------------------------------------
CREATE TABLE node_crashes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
node_id UUID NOT NULL REFERENCES render_nodes(id) ON DELETE CASCADE,
render_job_id UUID, -- which job was running
frame_job_id UUID,
crashed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_known_frame INT,
crash_signal TEXT, -- exit code or signal
error_log TEXT, -- last N lines of AE log
log_file_url TEXT, -- MinIO upload of full log
-- Recovery
auto_recovered BOOLEAN NOT NULL DEFAULT FALSE,
recovery_action TEXT, -- 'reset_prefs','restart_ae','reassign_job'
recovered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_node_crashes_node ON node_crashes(node_id, crashed_at DESC);
CREATE INDEX idx_node_crashes_job ON node_crashes(render_job_id) WHERE render_job_id IS NOT NULL;
-- ---------------------------------------------------------------------
-- node_updates — software/AE update tracking
-- ---------------------------------------------------------------------
CREATE TABLE node_updates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
update_file_name TEXT NOT NULL,
update_number INT NOT NULL,
description TEXT,
target_ae_version ae_version,
in_update_queue BOOLEAN NOT NULL DEFAULT FALSE,
rolled_out_to_node_ids UUID[] NOT NULL DEFAULT '{}',
last_update_queue_date TIMESTAMPTZ,
create_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ---------------------------------------------------------------------
-- node_template_cache — what's currently cached on each node
-- ---------------------------------------------------------------------
CREATE TABLE node_template_cache (
id BIGSERIAL PRIMARY KEY,
node_id UUID NOT NULL REFERENCES render_nodes(id) ON DELETE CASCADE,
project_id UUID NOT NULL, -- references content.projects
aep_file_md5 TEXT NOT NULL,
file_size_bytes BIGINT NOT NULL,
cached_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
use_count INT NOT NULL DEFAULT 1,
local_path TEXT NOT NULL,
UNIQUE (node_id, aep_file_md5)
);
CREATE INDEX idx_node_cache_node_lru ON node_template_cache(node_id, last_used_at);
CREATE INDEX idx_node_cache_md5 ON node_template_cache(aep_file_md5);
+315
View File
@@ -0,0 +1,315 @@
-- =====================================================================
-- RENDER SCHEMA — Part 2: Jobs, Frames, Snapshots, Exports
-- =====================================================================
SET search_path TO render, public;
CREATE TYPE render_step AS ENUM (
'Queued','Preparing','TemplateCache','JsxGen','Music',
'Rendering','Validating','Repairing','Optimisation','Video',
'Mixing','Final','Uploading','Done','Failed','Cancelled'
);
CREATE TYPE price_kind AS ENUM ('Free','Preview','Cash','Plan','Snapshot','Reseller');
CREATE TYPE render_quality AS ENUM ('Low','Medium','High','Full','Lossless');
CREATE TYPE frame_job_status AS ENUM (
'Pending','Rendering','Validated','Repairing','Converting','Done','Failed'
);
CREATE TYPE render_priority_queue AS ENUM (
'snapshot','vip','paid','preview','mockup','voiceover'
);
-- ---------------------------------------------------------------------
-- render_jobs — top-level render task
-- ---------------------------------------------------------------------
CREATE TABLE render_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
-- References (loose cross-schema)
saved_project_id UUID NOT NULL,
original_project_id UUID NOT NULL,
project_name TEXT,
-- Job identity
title TEXT,
name TEXT,
external_job_id TEXT, -- RabbitMQ message ID
priority_queue render_priority_queue NOT NULL,
priority_score INT NOT NULL DEFAULT 50, -- 0-100, higher = sooner
-- Pipeline state
step render_step NOT NULL DEFAULT 'Queued',
render_progress INT NOT NULL DEFAULT 0 CHECK (render_progress BETWEEN 0 AND 100),
convert_progress INT NOT NULL DEFAULT 0,
image_preview_b64 TEXT, -- last frame thumbnail
-- Pricing
price_type price_kind NOT NULL,
paid_price_minor BIGINT NOT NULL DEFAULT 0,
discount_code TEXT,
support_flatrender BOOLEAN NOT NULL DEFAULT FALSE,
-- Output config
mode TEXT NOT NULL, -- FIX/FLEXIBLE/MockUp/MusicVisualizer
quality render_quality NOT NULL DEFAULT 'High',
resolution TEXT NOT NULL, -- FullHD/FourK
r_height INT NOT NULL,
frame_rate INT NOT NULL DEFAULT 30,
is_60_fps BOOLEAN NOT NULL DEFAULT FALSE,
duration_sec NUMERIC(8,2) NOT NULL,
export_duration_sec NUMERIC(8,2),
-- Audio (NEW)
has_music BOOLEAN NOT NULL DEFAULT FALSE,
has_sfx BOOLEAN NOT NULL DEFAULT FALSE,
has_voiceover BOOLEAN NOT NULL DEFAULT FALSE,
music_volume NUMERIC(4,3),
sfx_volume NUMERIC(4,3),
voiceover_volume NUMERIC(4,3),
-- Resource allocation
render_node_count INT NOT NULL DEFAULT 1,
current_active_nodes INT NOT NULL DEFAULT 0,
region TEXT, -- preferred region
tell_me_when_done BOOLEAN NOT NULL DEFAULT TRUE,
-- Retry / recovery
retry_count INT NOT NULL DEFAULT 0,
max_retries INT NOT NULL DEFAULT 3,
repair_attempts INT NOT NULL DEFAULT 0,
failed_message TEXT,
failed_at_step render_step,
-- File outputs (paths in MinIO; final URL goes in exports.path)
render_folder TEXT, -- temp working dir
output_folder TEXT, -- frames dir
physical_render_folder TEXT, -- absolute path for nodes
physical_output_folder TEXT,
target_replica_name TEXT,
-- Reference to result
export_id UUID,
-- Timing
task_start_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
start_render_date TIMESTAMPTZ,
done_task_date TIMESTAMPTZ,
queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_render_jobs_user ON render_jobs(user_id, created_at DESC);
CREATE INDEX idx_render_jobs_tenant ON render_jobs(tenant_id, created_at DESC);
CREATE INDEX idx_render_jobs_step ON render_jobs(step) WHERE step NOT IN ('Done','Failed','Cancelled');
CREATE INDEX idx_render_jobs_queue ON render_jobs(priority_queue, priority_score DESC, queued_at)
WHERE step = 'Queued';
CREATE INDEX idx_render_jobs_in_flight ON render_jobs(started_at) WHERE step NOT IN ('Done','Failed','Cancelled','Queued');
CREATE TRIGGER tg_render_jobs_updated_at
BEFORE UPDATE ON render_jobs FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- frame_jobs — per-node frame-range assignments
-- ---------------------------------------------------------------------
CREATE TABLE frame_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
render_job_id UUID NOT NULL REFERENCES render_jobs(id) ON DELETE CASCADE,
node_id UUID NOT NULL REFERENCES render_nodes(id),
-- Range
start_frame INT NOT NULL,
end_frame INT NOT NULL,
collect_frame_count INT NOT NULL,
order_value INT NOT NULL DEFAULT 0,
folder_name TEXT NOT NULL, -- "O0", "O1"...
convert_url TEXT,
-- Status
status frame_job_status NOT NULL DEFAULT 'Pending',
frames_rendered INT NOT NULL DEFAULT 0,
frames_validated INT NOT NULL DEFAULT 0,
-- Errors
attempt INT NOT NULL DEFAULT 1,
last_error TEXT,
-- Outputs
output_mp4_url TEXT, -- chunk MP4 after ffmpeg
-- Timing
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
last_progress_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_frame_jobs_render ON frame_jobs(render_job_id, order_value);
CREATE INDEX idx_frame_jobs_node ON frame_jobs(node_id, status);
CREATE INDEX idx_frame_jobs_stalled ON frame_jobs(last_progress_at) WHERE status = 'Rendering';
CREATE TRIGGER tg_frame_jobs_updated_at
BEFORE UPDATE ON frame_jobs FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- frame_repair_jobs — missing/corrupt frame repair tracking
-- ---------------------------------------------------------------------
CREATE TABLE frame_repair_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
render_job_id UUID NOT NULL REFERENCES render_jobs(id) ON DELETE CASCADE,
node_id UUID REFERENCES render_nodes(id),
-- Range to repair
start_frame INT NOT NULL,
end_frame INT NOT NULL,
missing_frames INT[] NOT NULL DEFAULT '{}',
corrupt_frames INT[] NOT NULL DEFAULT '{}',
attempt INT NOT NULL DEFAULT 1,
status frame_job_status NOT NULL DEFAULT 'Pending',
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
CREATE INDEX idx_frame_repair_job ON frame_repair_jobs(render_job_id, attempt);
-- ---------------------------------------------------------------------
-- snapshots — single-frame scene previews (new feature)
-- ---------------------------------------------------------------------
CREATE TABLE snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
saved_project_id UUID NOT NULL,
scene_key TEXT NOT NULL,
frame_number INT NOT NULL,
-- Cache key — same inputs = same output
inputs_hash TEXT NOT NULL,
-- Status
status TEXT NOT NULL DEFAULT 'Pending', -- Pending/Rendering/Done/Failed
render_node_id UUID REFERENCES render_nodes(id),
-- Output
image_url TEXT,
thumbnail_url TEXT,
width INT,
height INT,
size_bytes BIGINT,
-- Timing
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
duration_ms INT,
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '7 days',
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX uq_snapshots_cache ON snapshots(saved_project_id, scene_key, frame_number, inputs_hash)
WHERE status = 'Done';
CREATE INDEX idx_snapshots_user ON snapshots(user_id, requested_at DESC);
CREATE INDEX idx_snapshots_expire ON snapshots(expires_at) WHERE status = 'Done';
-- ---------------------------------------------------------------------
-- exports — final rendered output records
-- ---------------------------------------------------------------------
CREATE TYPE export_create_type AS ENUM ('Render','Upload','Snapshot','Reupload');
CREATE TYPE export_file_type AS ENUM ('Video','Image','Audio','GIF','PDF');
CREATE TABLE exports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
saved_project_id UUID NOT NULL,
project_id UUID NOT NULL,
render_job_id UUID REFERENCES render_jobs(id) ON DELETE SET NULL,
-- Output
image TEXT, -- thumbnail
path TEXT NOT NULL, -- main file URL
file_extension TEXT NOT NULL DEFAULT 'mp4',
file_type export_file_type NOT NULL DEFAULT 'Video',
render_quality render_quality NOT NULL,
create_type export_create_type NOT NULL DEFAULT 'Render',
size_bytes BIGINT NOT NULL,
duration_sec NUMERIC(8,2),
width INT,
height INT,
-- Lifecycle
produce_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
auto_delete_date TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '30 days',
delete_notified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_exports_user ON exports(user_id, produce_date DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_exports_tenant ON exports(tenant_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_exports_saved_project ON exports(saved_project_id);
CREATE INDEX idx_exports_auto_delete ON exports(auto_delete_date) WHERE deleted_at IS NULL;
-- Now wire FK back to render_jobs
ALTER TABLE render_jobs
ADD CONSTRAINT fk_render_jobs_export
FOREIGN KEY (export_id) REFERENCES exports(id) ON DELETE SET NULL;
-- ---------------------------------------------------------------------
-- export_files — mockup mode produces multiple images per export
-- ---------------------------------------------------------------------
CREATE TABLE export_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
export_id UUID NOT NULL REFERENCES exports(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
name TEXT,
thumbnail TEXT,
path TEXT NOT NULL,
size_bytes BIGINT NOT NULL,
file_type export_file_type NOT NULL DEFAULT 'Image',
width INT,
height INT,
sort INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_export_files_export ON export_files(export_id, sort);
-- ---------------------------------------------------------------------
-- render_progress_events — WebSocket fan-out source (short-lived)
-- ---------------------------------------------------------------------
CREATE TABLE render_progress_events (
id BIGSERIAL PRIMARY KEY,
render_job_id UUID NOT NULL REFERENCES render_jobs(id) ON DELETE CASCADE,
step render_step NOT NULL,
progress INT NOT NULL,
current_frame INT,
total_frames INT,
eta_seconds INT,
preview_b64 TEXT,
message TEXT,
emitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_progress_events_job ON render_progress_events(render_job_id, emitted_at DESC);
-- Cleanup: keep last N per job via cron, drop > 7 days
+236
View File
@@ -0,0 +1,236 @@
-- =====================================================================
-- FILE_MGR SCHEMA — File Manager, Storage Quotas, Cleanup Scheduler
-- =====================================================================
SET search_path TO file_mgr, public;
CREATE TYPE file_kind AS ENUM ('Video','Image','Audio','Voiceover','Document','Other');
CREATE TYPE folder_kind AS ENUM ('System','User','Shared','Tenant');
CREATE TYPE upload_status AS ENUM ('Pending','Uploading','Processing','Ready','Failed','Quarantined');
CREATE TYPE cleanup_entity_type AS ENUM ('Export','TempRenderFolder','OrphanedFile','UnusedUpload','SnapshotExpired');
CREATE TYPE cleanup_status AS ENUM ('Scheduled','Notified','Processing','Done','Skipped','Failed');
-- ---------------------------------------------------------------------
-- user_folders — hierarchical folders
-- ---------------------------------------------------------------------
CREATE TABLE user_folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
name TEXT NOT NULL,
folder_type folder_kind NOT NULL DEFAULT 'User',
parent_folder_id UUID REFERENCES user_folders(id) ON DELETE CASCADE,
-- Stats (denormalized for fast UI)
file_count INT NOT NULL DEFAULT 0,
total_size_bytes BIGINT NOT NULL DEFAULT 0,
sort INT NOT NULL DEFAULT 0,
is_shared BOOLEAN NOT NULL DEFAULT FALSE,
share_token TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_folders_user ON user_folders(user_id, parent_folder_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_folders_parent ON user_folders(parent_folder_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_folders_share ON user_folders(share_token) WHERE share_token IS NOT NULL;
CREATE TRIGGER tg_folders_updated_at
BEFORE UPDATE ON user_folders FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- user_files
-- ---------------------------------------------------------------------
CREATE TABLE user_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
user_folder_id UUID REFERENCES user_folders(id) ON DELETE SET NULL,
-- Identity
name TEXT NOT NULL,
original_filename TEXT,
file_extension TEXT,
mime_type TEXT,
file_type file_kind NOT NULL,
-- Storage
minio_bucket TEXT NOT NULL,
minio_key TEXT NOT NULL,
cdn_url TEXT,
file_address TEXT NOT NULL, -- canonical URL
size_bytes BIGINT NOT NULL,
md5_hash TEXT,
sha256_hash TEXT,
-- Media metadata
duration_sec NUMERIC(8,2),
width INT,
height INT,
fps NUMERIC(5,2),
bitrate_kbps INT,
codec TEXT,
has_audio BOOLEAN,
has_video BOOLEAN,
-- Thumbnails
thumbnail_url TEXT,
waveform_data JSONB, -- for audio files
-- Upload state
upload_status upload_status NOT NULL DEFAULT 'Ready',
upload_id TEXT, -- multipart upload ID if used
upload_progress INT NOT NULL DEFAULT 100,
processing_error TEXT,
-- Source / linkage
source TEXT, -- 'upload','export','snapshot','voiceover_record','stock'
export_id UUID, -- references render.exports
parent_file_id UUID REFERENCES user_files(id) ON DELETE SET NULL, -- derived files
-- Lifecycle
last_used_at TIMESTAMPTZ,
use_count INT NOT NULL DEFAULT 0,
-- Sharing
is_public BOOLEAN NOT NULL DEFAULT FALSE,
share_token TEXT,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_files_user_folder ON user_files(user_id, user_folder_id, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_files_tenant ON user_files(tenant_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_files_type ON user_files(user_id, file_type) WHERE deleted_at IS NULL;
CREATE INDEX idx_files_hash ON user_files(md5_hash) WHERE md5_hash IS NOT NULL;
CREATE INDEX idx_files_unused ON user_files(last_used_at) WHERE deleted_at IS NULL;
CREATE INDEX idx_files_name_trgm ON user_files USING gin (name gin_trgm_ops);
CREATE INDEX idx_files_share ON user_files(share_token) WHERE share_token IS NOT NULL;
CREATE TRIGGER tg_files_updated_at
BEFORE UPDATE ON user_files FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- storage_quotas — current usage per user
-- ---------------------------------------------------------------------
CREATE TABLE storage_quotas (
user_id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
plan_quota_bytes BIGINT NOT NULL DEFAULT 0, -- from plan
bonus_quota_bytes BIGINT NOT NULL DEFAULT 0, -- purchased extra
used_bytes BIGINT NOT NULL DEFAULT 0,
-- Cached counts
video_count INT NOT NULL DEFAULT 0,
image_count INT NOT NULL DEFAULT 0,
audio_count INT NOT NULL DEFAULT 0,
video_bytes BIGINT NOT NULL DEFAULT 0,
image_bytes BIGINT NOT NULL DEFAULT 0,
audio_bytes BIGINT NOT NULL DEFAULT 0,
-- Notifications
last_90pct_notified_at TIMESTAMPTZ,
last_100pct_notified_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_quotas_tenant ON storage_quotas(tenant_id);
CREATE TRIGGER tg_quotas_updated_at
BEFORE UPDATE ON storage_quotas FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- cleanup_schedules — track what's queued for auto-delete
-- ---------------------------------------------------------------------
CREATE TABLE cleanup_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID,
user_id UUID,
entity_type cleanup_entity_type NOT NULL,
entity_id UUID NOT NULL,
entity_path TEXT, -- filesystem path for temp folders
scheduled_delete_at TIMESTAMPTZ NOT NULL,
notify_user_at TIMESTAMPTZ, -- send "expires in 3 days" notice
user_notified BOOLEAN NOT NULL DEFAULT FALSE,
user_notified_at TIMESTAMPTZ,
status cleanup_status NOT NULL DEFAULT 'Scheduled',
processed_at TIMESTAMPTZ,
processing_error TEXT,
bytes_freed BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_cleanup_due ON cleanup_schedules(scheduled_delete_at) WHERE status = 'Scheduled';
CREATE INDEX idx_cleanup_notify_due ON cleanup_schedules(notify_user_at) WHERE user_notified = FALSE AND notify_user_at IS NOT NULL;
CREATE INDEX idx_cleanup_entity ON cleanup_schedules(entity_type, entity_id);
CREATE TRIGGER tg_cleanup_updated_at
BEFORE UPDATE ON cleanup_schedules FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- upload_sessions — multipart / chunked upload tracking
-- ---------------------------------------------------------------------
CREATE TABLE upload_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
minio_bucket TEXT NOT NULL,
minio_key TEXT NOT NULL,
minio_upload_id TEXT NOT NULL, -- S3/MinIO multipart ID
filename TEXT NOT NULL,
mime_type TEXT,
total_size_bytes BIGINT NOT NULL,
chunks_received INT NOT NULL DEFAULT 0,
bytes_received BIGINT NOT NULL DEFAULT 0,
chunk_size_bytes INT NOT NULL DEFAULT 5242880, -- 5MB default
target_folder_id UUID REFERENCES user_folders(id) ON DELETE SET NULL,
target_file_id UUID, -- created when complete
status upload_status NOT NULL DEFAULT 'Uploading',
error_message TEXT,
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours',
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_uploads_user ON upload_sessions(user_id, created_at DESC);
CREATE INDEX idx_uploads_expired ON upload_sessions(expires_at) WHERE status = 'Uploading';
CREATE TRIGGER tg_uploads_updated_at
BEFORE UPDATE ON upload_sessions FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- minio_buckets — bucket registry (per region, per purpose)
-- ---------------------------------------------------------------------
CREATE TABLE minio_buckets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
region TEXT NOT NULL,
endpoint TEXT NOT NULL,
purpose TEXT NOT NULL, -- 'templates','user-uploads','exports','snapshots','voiceovers'
is_public BOOLEAN NOT NULL DEFAULT FALSE,
cdn_base_url TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_buckets_region_purpose ON minio_buckets(region, purpose) WHERE is_active = TRUE;
+194
View File
@@ -0,0 +1,194 @@
-- =====================================================================
-- NOTIFICATION SCHEMA — In-app, Push, Email, SMS, Telegram
-- =====================================================================
SET search_path TO notification, public;
CREATE TYPE notification_kind AS ENUM (
'RenderCompleted','RenderFailed','RenderProgress',
'PlanExpiring','PlanExpired','PaymentSuccess','PaymentFailed',
'StorageWarning','StorageFull','ExportExpiring','ExportDeleted',
'GiftEarned','QuestCompleted','LevelUp',
'AccountSecurity','SystemAnnouncement','TenantInvite',
'Marketing','Other'
);
CREATE TYPE notification_priority AS ENUM ('Low','Normal','High','Urgent');
CREATE TYPE delivery_channel AS ENUM ('InApp','Push','Email','SMS','Telegram','Webhook');
CREATE TYPE delivery_status_kind AS ENUM (
'Pending','Sent','Delivered','Failed','Bounced','Suppressed'
);
-- ---------------------------------------------------------------------
-- notifications — in-app notification feed
-- ---------------------------------------------------------------------
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
notification_type notification_kind NOT NULL,
priority notification_priority NOT NULL DEFAULT 'Normal',
title TEXT NOT NULL,
message TEXT NOT NULL,
label TEXT,
signature TEXT,
icon TEXT,
image TEXT,
animation_demo TEXT,
design TEXT,
-- Link target
action_url TEXT,
action_text TEXT,
-- Linked entities (sparse)
render_job_id UUID,
export_id UUID,
payment_id UUID,
gift_id UUID,
earned_gift_id UUID,
-- State
is_emergency BOOLEAN NOT NULL DEFAULT FALSE,
seen BOOLEAN NOT NULL DEFAULT FALSE,
seen_at TIMESTAMPTZ,
clicked BOOLEAN NOT NULL DEFAULT FALSE,
clicked_at TIMESTAMPTZ,
gift_used BOOLEAN NOT NULL DEFAULT FALSE,
-- Lifecycle
expire_date TIMESTAMPTZ,
create_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_notifs_user_feed ON notifications(user_id, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_notifs_unread ON notifications(user_id) WHERE seen = FALSE AND deleted_at IS NULL;
CREATE INDEX idx_notifs_tenant_type ON notifications(tenant_id, notification_type);
CREATE TRIGGER tg_notifs_updated_at
BEFORE UPDATE ON notifications FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- readed_notifications — read receipts (kept for analytics)
-- ---------------------------------------------------------------------
CREATE TABLE readed_notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
notification_id UUID NOT NULL REFERENCES notifications(id) ON DELETE CASCADE,
read_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, notification_id)
);
CREATE INDEX idx_readed_user ON readed_notifications(user_id, read_date DESC);
-- ---------------------------------------------------------------------
-- notification_deliveries — outbound across channels
-- ---------------------------------------------------------------------
CREATE TABLE notification_deliveries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
notification_id UUID REFERENCES notifications(id) ON DELETE SET NULL,
channel delivery_channel NOT NULL,
recipient TEXT NOT NULL, -- email/phone/push endpoint
subject TEXT,
body_text TEXT,
body_html TEXT,
template_id TEXT, -- reference to template engine
template_vars JSONB,
-- Provider tracking
provider TEXT, -- 'web-push','smtp','kavenegar','telegram','firebase'
provider_message_id TEXT,
provider_response JSONB,
status delivery_status_kind NOT NULL DEFAULT 'Pending',
error_message TEXT,
error_code TEXT,
-- Retry
attempt INT NOT NULL DEFAULT 1,
max_attempts INT NOT NULL DEFAULT 3,
next_retry_at TIMESTAMPTZ,
-- Timing
sent_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
failed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_deliveries_user ON notification_deliveries(user_id, created_at DESC);
CREATE INDEX idx_deliveries_pending ON notification_deliveries(next_retry_at)
WHERE status IN ('Pending','Failed') AND next_retry_at IS NOT NULL;
CREATE INDEX idx_deliveries_channel ON notification_deliveries(channel, status);
CREATE TRIGGER tg_deliveries_updated_at
BEFORE UPDATE ON notification_deliveries FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- notification_preferences — per-user opt-in per channel per type
-- ---------------------------------------------------------------------
CREATE TABLE notification_preferences (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
notification_type notification_kind NOT NULL,
channel delivery_channel NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, notification_type, channel)
);
CREATE INDEX idx_notif_prefs_user ON notification_preferences(user_id);
CREATE TRIGGER tg_notif_prefs_updated_at
BEFORE UPDATE ON notification_preferences FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- notification_templates — reusable templates per tenant
-- ---------------------------------------------------------------------
CREATE TABLE notification_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID, -- NULL = default
code TEXT NOT NULL, -- 'render.completed.email'
channel delivery_channel NOT NULL,
locale TEXT NOT NULL DEFAULT 'fa',
subject TEXT,
body_text TEXT,
body_html TEXT,
push_title TEXT,
push_body TEXT,
push_icon TEXT,
variables_schema JSONB, -- expected variables
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, code, channel, locale)
);
CREATE INDEX idx_notif_tpl_lookup ON notification_templates(tenant_id, code, channel, locale) WHERE is_active = TRUE;
CREATE TRIGGER tg_notif_tpl_updated_at
BEFORE UPDATE ON notification_templates FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- Wire back: earned_gifts.notification_id
-- ---------------------------------------------------------------------
ALTER TABLE identity.earned_gifts
ADD CONSTRAINT fk_earned_gifts_notification
FOREIGN KEY (notification_id) REFERENCES notification.notifications(id) ON DELETE SET NULL;
@@ -0,0 +1,8 @@
-- =====================================================================
-- IDENTITY SCHEMA — Part 15: Add Tara and SnapPay payment gateways
-- =====================================================================
-- ALTER TYPE ... ADD VALUE cannot run inside a transaction block in older
-- Postgres; use IF NOT EXISTS to make it idempotent.
ALTER TYPE identity.payment_gateway ADD VALUE IF NOT EXISTS 'Tara';
ALTER TYPE identity.payment_gateway ADD VALUE IF NOT EXISTS 'SnapPay';
@@ -0,0 +1,35 @@
-- 16_fix_inet_to_text.sql
-- The C# domain models all IP/network columns as string / string[]. The original
-- schema declared them as native PostgreSQL INET / INET[], which fails at runtime
-- with: "column ... is of type inet but expression is of type text".
--
-- Rather than add per-property EF value converters across every service, we align
-- the schema with the (string-based) code: convert every inet/inet[] column to
-- text/text[]. This block is idempotent — it only touches columns still typed inet,
-- and alters partitioned parents (which cascade) while skipping partition children.
DO $$
DECLARE r record;
BEGIN
FOR r IN
SELECT n.nspname AS sch, c.relname AS tbl, a.attname AS col, t.typname AS typ
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_type t ON t.oid = a.atttypid
WHERE n.nspname IN ('identity','content','studio','render','notification','file_mgr')
AND a.attnum > 0 AND NOT a.attisdropped
AND c.relkind IN ('r','p') -- ordinary + partitioned parents
AND NOT c.relispartition -- skip partition children (parent cascades)
AND t.typname IN ('inet','_inet')
LOOP
IF r.typ = '_inet' THEN
EXECUTE format('ALTER TABLE %I.%I ALTER COLUMN %I TYPE text[] USING %I::text[];',
r.sch, r.tbl, r.col, r.col);
ELSE
EXECUTE format('ALTER TABLE %I.%I ALTER COLUMN %I TYPE text USING %I::text;',
r.sch, r.tbl, r.col, r.col);
END IF;
RAISE NOTICE 'inet->text: %.%.% (%)', r.sch, r.tbl, r.col, r.typ;
END LOOP;
END $$;