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>
115 lines
3.8 KiB
PL/PgSQL
115 lines
3.8 KiB
PL/PgSQL
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);
|