diff --git a/db/migrations/001_initial_schema.sql b/db/migrations/001_initial_schema.sql new file mode 100644 index 0000000..d097d8e --- /dev/null +++ b/db/migrations/001_initial_schema.sql @@ -0,0 +1,220 @@ +-- migrate:up + +-- Enums +CREATE TYPE org_role AS ENUM ('owner', 'admin', 'member'); +CREATE TYPE passkey_device_type AS ENUM ('singleDevice', 'multiDevice'); + +-- Users (authentication) +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + email_verified_at TIMESTAMPTZ, -- NULL until verified + full_name TEXT, -- Legal/full name (set in profile setup) + display_name TEXT, -- Preferred display name (set in profile setup) + phone_number TEXT, -- E.164 format, validated with libphonenumber-js + avatar_url TEXT, + password_hash TEXT, -- NULL if using passkey only + require_passkey BOOLEAN NOT NULL DEFAULT false, -- If true, must use passkey to login + is_superuser BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Email verification tokens +CREATE TABLE email_verifications ( + id BIGSERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token TEXT UNIQUE NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Password reset tokens +CREATE TABLE password_resets ( + id BIGSERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token TEXT UNIQUE NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + used_at TIMESTAMPTZ, -- Set when token is used + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- WebAuthn challenges (short-lived, for passkey registration/authentication) +CREATE TABLE webauthn_challenges ( + id BIGSERIAL PRIMARY KEY, + options JSONB NOT NULL, -- Full options object from simplewebauthn + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Passkeys (WebAuthn credentials) +CREATE TABLE passkeys ( + id BIGSERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + credential_id BYTEA UNIQUE NOT NULL, + public_key BYTEA NOT NULL, + webauthn_user_id TEXT UNIQUE NOT NULL, + counter BIGINT NOT NULL DEFAULT 0, + device_type passkey_device_type NOT NULL, + backup_eligible BOOLEAN NOT NULL, + backup_status BOOLEAN NOT NULL, + transports JSONB, -- ['usb', 'ble', 'nfc', 'internal'] + rpid TEXT NOT NULL, + name TEXT NOT NULL, -- e.g. "MacBook Touch ID" + last_used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- User devices (tracks all devices a user has logged in from) +-- Note: device_fingerprint is NOT unique - multiple users can share the same +-- browser/device (e.g., shared family computer, work terminal) +CREATE TABLE user_devices ( + id BIGSERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + device_fingerprint TEXT NOT NULL, -- Random ID stored in cookie + name TEXT, -- e.g. "Chrome on MacOS" + is_trusted BOOLEAN NOT NULL DEFAULT false, + user_agent TEXT NOT NULL, + ip_address TEXT, + city TEXT, + region TEXT, + country TEXT, + last_used_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(user_id, device_fingerprint) -- One device record per user+fingerprint +); + +-- Sessions (token_hash stores SHA-256 hash of token, not raw token) +-- Sessions are kept for audit history, not deleted by cron +CREATE TABLE sessions ( + id BIGSERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + device_id BIGINT REFERENCES user_devices(id) ON DELETE SET NULL, + token_hash TEXT UNIQUE NOT NULL, -- SHA-256 hash of session token + trusted_mode BOOLEAN NOT NULL, -- True if passkey or email-confirmed login + ip_address TEXT, + city TEXT, + region TEXT, + country TEXT, + user_agent TEXT, + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ, -- NULL if active, set when logged out + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- API tokens (for CLI and programmatic access) +CREATE TABLE api_tokens ( + id BIGSERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, -- e.g. "CLI token", "CI/CD" + token_hash TEXT UNIQUE NOT NULL, -- SHA-256 hash of token + last_used_at TIMESTAMPTZ, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Login requests (tracks login attempts for polling and email confirmation) +-- Deleted after successful login via loginIfRequestIsCompleted() +-- Note: Only created for existing users. Non-existent users get a fake token (anti-enumeration). +CREATE TABLE login_requests ( + id BIGSERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + email TEXT NOT NULL, -- Email entered by user (normalized lowercase) + token TEXT UNIQUE, -- Token sent in email link (NULL until email sent) + device_fingerprint TEXT, -- From cookie at login request time + ip_address TEXT, + city TEXT, + region TEXT, + country TEXT, + user_agent TEXT, + completed_at TIMESTAMPTZ, -- NULL until login completed + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Orgs (companies/accounts) +CREATE TABLE orgs ( + id SERIAL PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + display_name TEXT NOT NULL, + logo_url TEXT, -- Cloudflare R2 URL + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Org members (users belonging to orgs) +CREATE TABLE org_members ( + id SERIAL PRIMARY KEY, + org_id INTEGER NOT NULL REFERENCES orgs(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role org_role NOT NULL DEFAULT 'member', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(org_id, user_id) +); + +-- Org sites (maps orgs to domains for report access) +CREATE TABLE org_sites ( + id SERIAL PRIMARY KEY, + org_id INTEGER NOT NULL REFERENCES orgs(id) ON DELETE CASCADE, + domain TEXT NOT NULL, -- key used to filter reports + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(domain) +); + +-- Org invites (pending invitations) +CREATE TABLE org_invites ( + id SERIAL PRIMARY KEY, + org_id INTEGER NOT NULL REFERENCES orgs(id) ON DELETE CASCADE, + email TEXT NOT NULL, + role org_role NOT NULL DEFAULT 'member', + invited_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token TEXT UNIQUE NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(org_id, email) +); + +-- Indexes (UNIQUE constraints already create indexes, so we skip those) +-- Note: orgs.slug has an index via UNIQUE constraint (used for all org API lookups) +-- Lookup indexes +CREATE INDEX idx_passkeys_user ON passkeys(user_id); +CREATE INDEX idx_user_devices_user ON user_devices(user_id); +CREATE INDEX idx_user_devices_fingerprint ON user_devices(device_fingerprint); +CREATE INDEX idx_sessions_user ON sessions(user_id); +CREATE INDEX idx_sessions_device ON sessions(device_id); +CREATE INDEX idx_api_tokens_user ON api_tokens(user_id); +CREATE INDEX idx_login_requests_user ON login_requests(user_id); +CREATE INDEX idx_password_resets_user ON password_resets(user_id); +CREATE INDEX idx_org_members_user ON org_members(user_id); +CREATE INDEX idx_org_members_org ON org_members(org_id); +CREATE INDEX idx_org_sites_org ON org_sites(org_id); +CREATE INDEX idx_org_invites_email ON org_invites(email); + +-- Active session lookup (excludes revoked sessions) +CREATE INDEX idx_sessions_active ON sessions(token_hash) WHERE revoked_at IS NULL; + +-- Cleanup job indexes (for deleting expired rows) +-- Note: sessions are NOT deleted, kept for audit history (use revoked_at instead) +CREATE INDEX idx_api_tokens_expires ON api_tokens(expires_at); +CREATE INDEX idx_email_verifications_expires ON email_verifications(expires_at); +CREATE INDEX idx_password_resets_expires ON password_resets(expires_at); +CREATE INDEX idx_webauthn_challenges_created ON webauthn_challenges(created_at); +CREATE INDEX idx_org_invites_expires ON org_invites(expires_at); +CREATE INDEX idx_login_requests_expires ON login_requests(expires_at); + +-- migrate:down + +DROP TABLE IF EXISTS org_invites; +DROP TABLE IF EXISTS org_sites; +DROP TABLE IF EXISTS org_members; +DROP TABLE IF EXISTS orgs; +DROP TABLE IF EXISTS login_requests; +DROP TABLE IF EXISTS api_tokens; +DROP TABLE IF EXISTS sessions; +DROP TABLE IF EXISTS user_devices; +DROP TABLE IF EXISTS passkeys; +DROP TABLE IF EXISTS webauthn_challenges; +DROP TABLE IF EXISTS password_resets; +DROP TABLE IF EXISTS email_verifications; +DROP TABLE IF EXISTS users; +DROP TYPE IF EXISTS passkey_device_type; +DROP TYPE IF EXISTS org_role; diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..33b8964 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,1080 @@ +\restrict 5Klr8Uwvte9CcdsPN03iAhTDLu4g5tjWlB2lrsUnXPKBpmXjzpUTcpbpWJ6LSli + +-- Dumped from database version 17.7 +-- Dumped by pg_dump version 17.7 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: org_role; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.org_role AS ENUM ( + 'owner', + 'admin', + 'member' +); + + +-- +-- Name: passkey_device_type; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.passkey_device_type AS ENUM ( + 'singleDevice', + 'multiDevice' +); + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: api_tokens; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.api_tokens ( + id bigint NOT NULL, + user_id integer NOT NULL, + name text NOT NULL, + token_hash text NOT NULL, + last_used_at timestamp with time zone, + expires_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: api_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.api_tokens_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: api_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.api_tokens_id_seq OWNED BY public.api_tokens.id; + + +-- +-- Name: email_verifications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.email_verifications ( + id bigint NOT NULL, + user_id integer NOT NULL, + token text NOT NULL, + expires_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: email_verifications_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.email_verifications_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: email_verifications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.email_verifications_id_seq OWNED BY public.email_verifications.id; + + +-- +-- Name: login_requests; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.login_requests ( + id bigint NOT NULL, + user_id integer NOT NULL, + email text NOT NULL, + token text, + device_fingerprint text, + ip_address text, + city text, + region text, + country text, + user_agent text, + completed_at timestamp with time zone, + expires_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: login_requests_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.login_requests_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: login_requests_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.login_requests_id_seq OWNED BY public.login_requests.id; + + +-- +-- Name: org_invites; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.org_invites ( + id integer NOT NULL, + org_id integer NOT NULL, + email text NOT NULL, + role public.org_role DEFAULT 'member'::public.org_role NOT NULL, + invited_by integer NOT NULL, + token text NOT NULL, + expires_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: org_invites_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.org_invites_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: org_invites_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.org_invites_id_seq OWNED BY public.org_invites.id; + + +-- +-- Name: org_members; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.org_members ( + id integer NOT NULL, + org_id integer NOT NULL, + user_id integer NOT NULL, + role public.org_role DEFAULT 'member'::public.org_role NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: org_members_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.org_members_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: org_members_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.org_members_id_seq OWNED BY public.org_members.id; + + +-- +-- Name: org_sites; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.org_sites ( + id integer NOT NULL, + org_id integer NOT NULL, + domain text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: org_sites_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.org_sites_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: org_sites_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.org_sites_id_seq OWNED BY public.org_sites.id; + + +-- +-- Name: orgs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.orgs ( + id integer NOT NULL, + slug text NOT NULL, + display_name text NOT NULL, + logo_url text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: orgs_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.orgs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: orgs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.orgs_id_seq OWNED BY public.orgs.id; + + +-- +-- Name: passkeys; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.passkeys ( + id bigint NOT NULL, + user_id integer NOT NULL, + credential_id bytea NOT NULL, + public_key bytea NOT NULL, + webauthn_user_id text NOT NULL, + counter bigint DEFAULT 0 NOT NULL, + device_type public.passkey_device_type NOT NULL, + backup_eligible boolean NOT NULL, + backup_status boolean NOT NULL, + transports jsonb, + rpid text NOT NULL, + name text NOT NULL, + last_used_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: passkeys_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.passkeys_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: passkeys_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.passkeys_id_seq OWNED BY public.passkeys.id; + + +-- +-- Name: password_resets; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.password_resets ( + id bigint NOT NULL, + user_id integer NOT NULL, + token text NOT NULL, + expires_at timestamp with time zone NOT NULL, + used_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: password_resets_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.password_resets_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: password_resets_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.password_resets_id_seq OWNED BY public.password_resets.id; + + +-- +-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.schema_migrations ( + version character varying NOT NULL +); + + +-- +-- Name: sessions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.sessions ( + id bigint NOT NULL, + user_id integer NOT NULL, + device_id bigint, + token_hash text NOT NULL, + trusted_mode boolean NOT NULL, + ip_address text, + city text, + region text, + country text, + user_agent text, + expires_at timestamp with time zone NOT NULL, + revoked_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: sessions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.sessions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: sessions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.sessions_id_seq OWNED BY public.sessions.id; + + +-- +-- Name: user_devices; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_devices ( + id bigint NOT NULL, + user_id integer NOT NULL, + device_fingerprint text NOT NULL, + name text, + is_trusted boolean DEFAULT false NOT NULL, + user_agent text NOT NULL, + ip_address text, + city text, + region text, + country text, + last_used_at timestamp with time zone DEFAULT now() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: user_devices_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_devices_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_devices_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_devices_id_seq OWNED BY public.user_devices.id; + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.users ( + id integer NOT NULL, + email text NOT NULL, + email_verified_at timestamp with time zone, + full_name text, + display_name text, + phone_number text, + avatar_url text, + password_hash text, + require_passkey boolean DEFAULT false NOT NULL, + is_superuser boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + + +-- +-- Name: webauthn_challenges; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.webauthn_challenges ( + id bigint NOT NULL, + options jsonb NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: webauthn_challenges_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.webauthn_challenges_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: webauthn_challenges_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.webauthn_challenges_id_seq OWNED BY public.webauthn_challenges.id; + + +-- +-- Name: api_tokens id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_tokens ALTER COLUMN id SET DEFAULT nextval('public.api_tokens_id_seq'::regclass); + + +-- +-- Name: email_verifications id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.email_verifications ALTER COLUMN id SET DEFAULT nextval('public.email_verifications_id_seq'::regclass); + + +-- +-- Name: login_requests id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.login_requests ALTER COLUMN id SET DEFAULT nextval('public.login_requests_id_seq'::regclass); + + +-- +-- Name: org_invites id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_invites ALTER COLUMN id SET DEFAULT nextval('public.org_invites_id_seq'::regclass); + + +-- +-- Name: org_members id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_members ALTER COLUMN id SET DEFAULT nextval('public.org_members_id_seq'::regclass); + + +-- +-- Name: org_sites id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_sites ALTER COLUMN id SET DEFAULT nextval('public.org_sites_id_seq'::regclass); + + +-- +-- Name: orgs id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.orgs ALTER COLUMN id SET DEFAULT nextval('public.orgs_id_seq'::regclass); + + +-- +-- Name: passkeys id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.passkeys ALTER COLUMN id SET DEFAULT nextval('public.passkeys_id_seq'::regclass); + + +-- +-- Name: password_resets id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.password_resets ALTER COLUMN id SET DEFAULT nextval('public.password_resets_id_seq'::regclass); + + +-- +-- Name: sessions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sessions ALTER COLUMN id SET DEFAULT nextval('public.sessions_id_seq'::regclass); + + +-- +-- Name: user_devices id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_devices ALTER COLUMN id SET DEFAULT nextval('public.user_devices_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + + +-- +-- Name: webauthn_challenges id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.webauthn_challenges ALTER COLUMN id SET DEFAULT nextval('public.webauthn_challenges_id_seq'::regclass); + + +-- +-- Name: api_tokens api_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_pkey PRIMARY KEY (id); + + +-- +-- Name: api_tokens api_tokens_token_hash_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_token_hash_key UNIQUE (token_hash); + + +-- +-- Name: email_verifications email_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.email_verifications + ADD CONSTRAINT email_verifications_pkey PRIMARY KEY (id); + + +-- +-- Name: email_verifications email_verifications_token_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.email_verifications + ADD CONSTRAINT email_verifications_token_key UNIQUE (token); + + +-- +-- Name: login_requests login_requests_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.login_requests + ADD CONSTRAINT login_requests_pkey PRIMARY KEY (id); + + +-- +-- Name: login_requests login_requests_token_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.login_requests + ADD CONSTRAINT login_requests_token_key UNIQUE (token); + + +-- +-- Name: org_invites org_invites_org_id_email_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_invites + ADD CONSTRAINT org_invites_org_id_email_key UNIQUE (org_id, email); + + +-- +-- Name: org_invites org_invites_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_invites + ADD CONSTRAINT org_invites_pkey PRIMARY KEY (id); + + +-- +-- Name: org_invites org_invites_token_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_invites + ADD CONSTRAINT org_invites_token_key UNIQUE (token); + + +-- +-- Name: org_members org_members_org_id_user_id_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_members + ADD CONSTRAINT org_members_org_id_user_id_key UNIQUE (org_id, user_id); + + +-- +-- Name: org_members org_members_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_members + ADD CONSTRAINT org_members_pkey PRIMARY KEY (id); + + +-- +-- Name: org_sites org_sites_domain_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_sites + ADD CONSTRAINT org_sites_domain_key UNIQUE (domain); + + +-- +-- Name: org_sites org_sites_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_sites + ADD CONSTRAINT org_sites_pkey PRIMARY KEY (id); + + +-- +-- Name: orgs orgs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.orgs + ADD CONSTRAINT orgs_pkey PRIMARY KEY (id); + + +-- +-- Name: orgs orgs_slug_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.orgs + ADD CONSTRAINT orgs_slug_key UNIQUE (slug); + + +-- +-- Name: passkeys passkeys_credential_id_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.passkeys + ADD CONSTRAINT passkeys_credential_id_key UNIQUE (credential_id); + + +-- +-- Name: passkeys passkeys_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.passkeys + ADD CONSTRAINT passkeys_pkey PRIMARY KEY (id); + + +-- +-- Name: passkeys passkeys_webauthn_user_id_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.passkeys + ADD CONSTRAINT passkeys_webauthn_user_id_key UNIQUE (webauthn_user_id); + + +-- +-- Name: password_resets password_resets_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.password_resets + ADD CONSTRAINT password_resets_pkey PRIMARY KEY (id); + + +-- +-- Name: password_resets password_resets_token_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.password_resets + ADD CONSTRAINT password_resets_token_key UNIQUE (token); + + +-- +-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.schema_migrations + ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); + + +-- +-- Name: sessions sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sessions + ADD CONSTRAINT sessions_pkey PRIMARY KEY (id); + + +-- +-- Name: sessions sessions_token_hash_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sessions + ADD CONSTRAINT sessions_token_hash_key UNIQUE (token_hash); + + +-- +-- Name: user_devices user_devices_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_devices + ADD CONSTRAINT user_devices_pkey PRIMARY KEY (id); + + +-- +-- Name: user_devices user_devices_user_id_device_fingerprint_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_devices + ADD CONSTRAINT user_devices_user_id_device_fingerprint_key UNIQUE (user_id, device_fingerprint); + + +-- +-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_email_key UNIQUE (email); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- Name: webauthn_challenges webauthn_challenges_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.webauthn_challenges + ADD CONSTRAINT webauthn_challenges_pkey PRIMARY KEY (id); + + +-- +-- Name: idx_api_tokens_expires; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_api_tokens_expires ON public.api_tokens USING btree (expires_at); + + +-- +-- Name: idx_api_tokens_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_api_tokens_user ON public.api_tokens USING btree (user_id); + + +-- +-- Name: idx_email_verifications_expires; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_email_verifications_expires ON public.email_verifications USING btree (expires_at); + + +-- +-- Name: idx_login_requests_expires; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_login_requests_expires ON public.login_requests USING btree (expires_at); + + +-- +-- Name: idx_login_requests_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_login_requests_user ON public.login_requests USING btree (user_id); + + +-- +-- Name: idx_org_invites_email; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_org_invites_email ON public.org_invites USING btree (email); + + +-- +-- Name: idx_org_invites_expires; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_org_invites_expires ON public.org_invites USING btree (expires_at); + + +-- +-- Name: idx_org_members_org; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_org_members_org ON public.org_members USING btree (org_id); + + +-- +-- Name: idx_org_members_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_org_members_user ON public.org_members USING btree (user_id); + + +-- +-- Name: idx_org_sites_org; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_org_sites_org ON public.org_sites USING btree (org_id); + + +-- +-- Name: idx_passkeys_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_passkeys_user ON public.passkeys USING btree (user_id); + + +-- +-- Name: idx_password_resets_expires; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_password_resets_expires ON public.password_resets USING btree (expires_at); + + +-- +-- Name: idx_password_resets_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_password_resets_user ON public.password_resets USING btree (user_id); + + +-- +-- Name: idx_sessions_active; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_sessions_active ON public.sessions USING btree (token_hash) WHERE (revoked_at IS NULL); + + +-- +-- Name: idx_sessions_device; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_sessions_device ON public.sessions USING btree (device_id); + + +-- +-- Name: idx_sessions_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_sessions_user ON public.sessions USING btree (user_id); + + +-- +-- Name: idx_user_devices_fingerprint; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_user_devices_fingerprint ON public.user_devices USING btree (device_fingerprint); + + +-- +-- Name: idx_user_devices_user; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_user_devices_user ON public.user_devices USING btree (user_id); + + +-- +-- Name: idx_webauthn_challenges_created; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_webauthn_challenges_created ON public.webauthn_challenges USING btree (created_at); + + +-- +-- Name: api_tokens api_tokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.api_tokens + ADD CONSTRAINT api_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: email_verifications email_verifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.email_verifications + ADD CONSTRAINT email_verifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: login_requests login_requests_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.login_requests + ADD CONSTRAINT login_requests_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: org_invites org_invites_invited_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_invites + ADD CONSTRAINT org_invites_invited_by_fkey FOREIGN KEY (invited_by) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: org_invites org_invites_org_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_invites + ADD CONSTRAINT org_invites_org_id_fkey FOREIGN KEY (org_id) REFERENCES public.orgs(id) ON DELETE CASCADE; + + +-- +-- Name: org_members org_members_org_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_members + ADD CONSTRAINT org_members_org_id_fkey FOREIGN KEY (org_id) REFERENCES public.orgs(id) ON DELETE CASCADE; + + +-- +-- Name: org_members org_members_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_members + ADD CONSTRAINT org_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: org_sites org_sites_org_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.org_sites + ADD CONSTRAINT org_sites_org_id_fkey FOREIGN KEY (org_id) REFERENCES public.orgs(id) ON DELETE CASCADE; + + +-- +-- Name: passkeys passkeys_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.passkeys + ADD CONSTRAINT passkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: password_resets password_resets_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.password_resets + ADD CONSTRAINT password_resets_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: sessions sessions_device_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sessions + ADD CONSTRAINT sessions_device_id_fkey FOREIGN KEY (device_id) REFERENCES public.user_devices(id) ON DELETE SET NULL; + + +-- +-- Name: sessions sessions_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.sessions + ADD CONSTRAINT sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: user_devices user_devices_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_devices + ADD CONSTRAINT user_devices_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict 5Klr8Uwvte9CcdsPN03iAhTDLu4g5tjWlB2lrsUnXPKBpmXjzpUTcpbpWJ6LSli + + +-- +-- Dbmate schema migrations +-- + +INSERT INTO public.schema_migrations (version) VALUES + ('001'); diff --git a/packages/db-schema/eslint.config.js b/packages/db-schema/eslint.config.js new file mode 100644 index 0000000..ee789e3 --- /dev/null +++ b/packages/db-schema/eslint.config.js @@ -0,0 +1,12 @@ +import { configs } from "@macalinao/eslint-config"; + +export default [ + ...configs.fast, + { + languageOptions: { + parserOptions: { + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages/db-schema/package.json b/packages/db-schema/package.json new file mode 100644 index 0000000..256dfaa --- /dev/null +++ b/packages/db-schema/package.json @@ -0,0 +1,26 @@ +{ + "name": "@reviq/db-schema", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "tsc", + "clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache", + "lint": "eslint . --cache", + "generate": "kysely-codegen --dialect postgres --url $DATABASE_URL --out-file src/types.ts" + }, + "dependencies": { + "kysely": "^0.28.9", + "pg": "^8.13.1" + }, + "devDependencies": { + "kysely-codegen": "^0.19.0", + "@types/node": "^25.0.3", + "@types/pg": "^8.11.10", + "@macalinao/tsconfig": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/db-schema/src/index.ts b/packages/db-schema/src/index.ts new file mode 100644 index 0000000..9246719 --- /dev/null +++ b/packages/db-schema/src/index.ts @@ -0,0 +1,10 @@ +/** + * Database schema types generated from PostgreSQL schema + * + * @module @reviq/db-schema + */ + +export * from "./types.js"; + +// Re-export DB as Database for convenience +export type { DB as Database } from "./types.js"; diff --git a/packages/db-schema/src/types.ts b/packages/db-schema/src/types.ts new file mode 100644 index 0000000..81c2e45 --- /dev/null +++ b/packages/db-schema/src/types.ts @@ -0,0 +1,199 @@ +/** + * This file was generated by kysely-codegen. + * Please do not edit it manually. + */ + +import type { ColumnType } from "kysely"; + +export type Generated = + T extends ColumnType + ? ColumnType + : ColumnType; + +export type Int8 = ColumnType; + +export type Json = JsonValue; + +export type JsonArray = JsonValue[]; + +export type JsonObject = { + [x: string]: JsonValue | undefined; +}; + +export type JsonPrimitive = boolean | number | string | null; + +export type JsonValue = JsonArray | JsonObject | JsonPrimitive; + +export type OrgRole = "admin" | "member" | "owner"; + +export type PasskeyDeviceType = "multiDevice" | "singleDevice"; + +export type Timestamp = ColumnType; + +export interface ApiTokens { + created_at: Generated; + expires_at: Timestamp; + id: Generated; + last_used_at: Timestamp | null; + name: string; + token_hash: string; + user_id: number; +} + +export interface EmailVerifications { + created_at: Generated; + expires_at: Timestamp; + id: Generated; + token: string; + user_id: number; +} + +export interface LoginRequests { + city: string | null; + completed_at: Timestamp | null; + country: string | null; + created_at: Generated; + device_fingerprint: string | null; + email: string; + expires_at: Timestamp; + id: Generated; + ip_address: string | null; + region: string | null; + token: string | null; + user_agent: string | null; + user_id: number; +} + +export interface OrgInvites { + created_at: Generated; + email: string; + expires_at: Timestamp; + id: Generated; + invited_by: number; + org_id: number; + role: Generated; + token: string; +} + +export interface OrgMembers { + created_at: Generated; + id: Generated; + org_id: number; + role: Generated; + user_id: number; +} + +export interface Orgs { + created_at: Generated; + display_name: string; + id: Generated; + logo_url: string | null; + slug: string; + updated_at: Generated; +} + +export interface OrgSites { + created_at: Generated; + domain: string; + id: Generated; + org_id: number; +} + +export interface Passkeys { + backup_eligible: boolean; + backup_status: boolean; + counter: Generated; + created_at: Generated; + credential_id: Buffer; + device_type: PasskeyDeviceType; + id: Generated; + last_used_at: Timestamp | null; + name: string; + public_key: Buffer; + rpid: string; + transports: Json | null; + user_id: number; + webauthn_user_id: string; +} + +export interface PasswordResets { + created_at: Generated; + expires_at: Timestamp; + id: Generated; + token: string; + used_at: Timestamp | null; + user_id: number; +} + +export interface SchemaMigrations { + version: string; +} + +export interface Sessions { + city: string | null; + country: string | null; + created_at: Generated; + device_id: Int8 | null; + expires_at: Timestamp; + id: Generated; + ip_address: string | null; + region: string | null; + revoked_at: Timestamp | null; + token_hash: string; + trusted_mode: boolean; + user_agent: string | null; + user_id: number; +} + +export interface UserDevices { + city: string | null; + country: string | null; + created_at: Generated; + device_fingerprint: string; + id: Generated; + ip_address: string | null; + is_trusted: Generated; + last_used_at: Generated; + name: string | null; + region: string | null; + user_agent: string; + user_id: number; +} + +export interface Users { + avatar_url: string | null; + created_at: Generated; + display_name: string | null; + email: string; + email_verified_at: Timestamp | null; + full_name: string | null; + id: Generated; + is_superuser: Generated; + password_hash: string | null; + phone_number: string | null; + require_passkey: Generated; + updated_at: Generated; +} + +export interface WebauthnChallenges { + created_at: Generated; + id: Generated; + options: Json; +} + +export interface DB { + api_tokens: ApiTokens; + email_verifications: EmailVerifications; + login_requests: LoginRequests; + org_invites: OrgInvites; + org_members: OrgMembers; + org_sites: OrgSites; + orgs: Orgs; + passkeys: Passkeys; + password_resets: PasswordResets; + schema_migrations: SchemaMigrations; + sessions: Sessions; + user_devices: UserDevices; + users: Users; + webauthn_challenges: WebauthnChallenges; +} diff --git a/packages/db-schema/tsconfig.json b/packages/db-schema/tsconfig.json new file mode 100644 index 0000000..a7c1e4d --- /dev/null +++ b/packages/db-schema/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@macalinao/tsconfig/tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "declarationMap": true, + "composite": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/db/eslint.config.js b/packages/db/eslint.config.js new file mode 100644 index 0000000..ee789e3 --- /dev/null +++ b/packages/db/eslint.config.js @@ -0,0 +1,12 @@ +import { configs } from "@macalinao/eslint-config"; + +export default [ + ...configs.fast, + { + languageOptions: { + parserOptions: { + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages/db/package.json b/packages/db/package.json new file mode 100644 index 0000000..338f673 --- /dev/null +++ b/packages/db/package.json @@ -0,0 +1,25 @@ +{ + "name": "@reviq/db", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "tsc", + "clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache", + "lint": "eslint . --cache" + }, + "dependencies": { + "@reviq/db-schema": "workspace:*", + "kysely": "^0.28.9", + "pg": "^8.13.1" + }, + "devDependencies": { + "@types/node": "^25.0.3", + "@types/pg": "^8.11.10", + "@macalinao/tsconfig": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts new file mode 100644 index 0000000..fae7d8b --- /dev/null +++ b/packages/db/src/client.ts @@ -0,0 +1,37 @@ +/** + * Kysely database client + * + * @module @reviq/db + */ + +import { Kysely, PostgresDialect } from "kysely"; +import type { Database } from "@reviq/db-schema"; +import pg from "pg"; + +const { Pool } = pg; + +/** + * Creates a new Kysely database client + * + * @param connectionString - PostgreSQL connection string (defaults to DATABASE_URL env var) + * @returns Kysely database instance + */ +export const createDb = ( + connectionString: string = process.env.DATABASE_URL || "" +): Kysely => { + if (!connectionString) { + throw new Error( + "Database connection string is required. Set DATABASE_URL environment variable." + ); + } + + const dialect = new PostgresDialect({ + pool: new Pool({ + connectionString, + }), + }); + + return new Kysely({ + dialect, + }); +}; diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts new file mode 100644 index 0000000..56ac155 --- /dev/null +++ b/packages/db/src/index.ts @@ -0,0 +1,26 @@ +/** + * Database client for RevIQ Publisher Dashboard + * + * @module @reviq/db + */ + +import type { Kysely } from "kysely"; +import type { Database } from "@reviq/db-schema"; +import { createDb } from "./client.js"; + +/** + * Default database instance + * + * Uses DATABASE_URL environment variable for connection + */ +export const db: Kysely = createDb(); + +/** + * Export createDb for creating custom database instances + */ +export { createDb } from "./client.js"; + +/** + * Re-export database types from db-schema + */ +export type { Database } from "@reviq/db-schema"; diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json new file mode 100644 index 0000000..80b44d1 --- /dev/null +++ b/packages/db/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@macalinao/tsconfig/tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "declarationMap": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}