feat(server): schéma PostgreSQL sync-ready (ticket #49)
Ajoute la migration initiale (migrations/0001_initial_schema.sql, incluant la table users nécessaire à l'adapter d'auth du ticket #48), le runner de migration (bin/migrate.dart) et l'accès Postgres (infrastructure/postgres/postgres_database.dart). dart pub get OK, dart analyze clean, dart test vert (le test PostgreSQL réel est skip faute de TEST_DATABASE_URL/Docker disponible dans ce sandbox ; SQL de migration revu manuellement). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
114
server/migrations/0001_initial_schema.sql
Normal file
114
server/migrations/0001_initial_schema.sql
Normal file
@ -0,0 +1,114 @@
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE EXTENSION IF NOT EXISTS citext;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email citext NOT NULL UNIQUE,
|
||||
password_hash text NOT NULL,
|
||||
display_name text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
disabled_at timestamptz,
|
||||
CONSTRAINT users_email_not_blank CHECK (length(trim(email::text)) > 0),
|
||||
CONSTRAINT users_password_hash_not_blank CHECK (length(trim(password_hash)) > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth_sessions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash text NOT NULL UNIQUE,
|
||||
issued_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
revoked_at timestamptz,
|
||||
user_agent text,
|
||||
device_label text,
|
||||
CONSTRAINT auth_sessions_token_hash_not_blank CHECK (length(trim(token_hash)) > 0),
|
||||
CONSTRAINT auth_sessions_expires_after_issued CHECK (expires_at > issued_at)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS synced_resources (
|
||||
server_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
owner_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
resource_type text NOT NULL,
|
||||
client_id text NOT NULL,
|
||||
payload_json jsonb NOT NULL,
|
||||
schema_version integer NOT NULL,
|
||||
client_updated_at timestamptz NOT NULL,
|
||||
server_updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
origin_device_id text,
|
||||
CONSTRAINT synced_resources_type_check CHECK (
|
||||
resource_type IN (
|
||||
'exercise',
|
||||
'program',
|
||||
'workoutTemplate',
|
||||
'workoutHistory',
|
||||
'mediaAsset'
|
||||
)
|
||||
),
|
||||
CONSTRAINT synced_resources_client_id_not_blank CHECK (length(trim(client_id)) > 0),
|
||||
CONSTRAINT synced_resources_schema_version_positive CHECK (schema_version > 0),
|
||||
CONSTRAINT synced_resources_owner_type_client_unique UNIQUE (
|
||||
owner_user_id,
|
||||
resource_type,
|
||||
client_id
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_synced_resources_owner_type_updated
|
||||
ON synced_resources (owner_user_id, resource_type, server_updated_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_synced_resources_owner_updated
|
||||
ON synced_resources (owner_user_id, server_updated_at);
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_synced_resources_server_updated_at()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
NEW.server_updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_synced_resources_set_server_updated_at
|
||||
ON synced_resources;
|
||||
|
||||
CREATE TRIGGER trg_synced_resources_set_server_updated_at
|
||||
BEFORE UPDATE ON synced_resources
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_synced_resources_server_updated_at();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shares (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sender_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
resource_type text NOT NULL,
|
||||
payload_json jsonb NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
revoked_at timestamptz,
|
||||
CONSTRAINT shares_resource_type_check CHECK (
|
||||
resource_type IN ('program', 'workoutTemplate')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS share_recipients (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
share_id uuid NOT NULL REFERENCES shares(id) ON DELETE CASCADE,
|
||||
recipient_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
status text NOT NULL DEFAULT 'pending',
|
||||
responded_at timestamptz,
|
||||
CONSTRAINT share_recipients_status_check CHECK (
|
||||
status IN ('pending', 'accepted', 'declined', 'revoked')
|
||||
),
|
||||
CONSTRAINT share_recipients_share_user_unique UNIQUE (
|
||||
share_id,
|
||||
recipient_user_id
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_auth_sessions_user_id
|
||||
ON auth_sessions (user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_shares_sender_user_id
|
||||
ON shares (sender_user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_share_recipients_recipient_status
|
||||
ON share_recipients (recipient_user_id, status);
|
||||
Reference in New Issue
Block a user