-- STL Library Manager schema -- A "model" is one logical download (a Thingiverse thing, a Printables model, a -- single uploaded STL). A model owns one or more files (the actual .stl/.3mf/...). -- Per-license commercial-use policy. Auto-registered as licenses are seen; -- editable in Settings. commercial: true=sellable, false=non-commercial, null=unknown. CREATE TABLE IF NOT EXISTS license_rules ( name TEXT PRIMARY KEY, commercial BOOLEAN ); -- App users (email/password auth). CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- One-time password-reset tokens (token stored hashed). CREATE TABLE IF NOT EXISTS password_resets ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL, expires_at TIMESTAMPTZ NOT NULL, used BOOLEAN NOT NULL DEFAULT false ); CREATE INDEX IF NOT EXISTS idx_password_resets_token ON password_resets(token_hash); -- Simple key/value app settings (API tokens, site auth). Editable from the UI. CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS designers ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, source_site TEXT, -- thingiverse | printables | makerworld | ... profile_url TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (name, source_site) ); CREATE TABLE IF NOT EXISTS projects ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', date_created TIMESTAMPTZ NOT NULL DEFAULT now(), date_modified TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS tags ( id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE ); CREATE TABLE IF NOT EXISTS models ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', source_site TEXT, -- where it came from, null for manual upload source_url TEXT, -- original page URL (for attribution) source_id TEXT, -- the site's id for the thing/model license TEXT, designer_id INTEGER REFERENCES designers(id) ON DELETE SET NULL, thumbnail_path TEXT, print_settings JSONB NOT NULL DEFAULT '{}'::jsonb, date_added TIMESTAMPTZ NOT NULL DEFAULT now(), date_modified TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS files ( id SERIAL PRIMARY KEY, model_id INTEGER NOT NULL REFERENCES models(id) ON DELETE CASCADE, file_name TEXT NOT NULL, -- on-disk name (unique suffix) original_name TEXT NOT NULL, -- name as downloaded/uploaded file_path TEXT NOT NULL, -- path on disk format TEXT NOT NULL, -- stl | 3mf | obj | step | gcode | zip | ... size BIGINT NOT NULL DEFAULT 0, volume DOUBLE PRECISION, -- cm^3 (from node-stl), when computable bbox JSONB, -- {x,y,z} bounding box facets INTEGER, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS model_projects ( model_id INTEGER NOT NULL REFERENCES models(id) ON DELETE CASCADE, project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, PRIMARY KEY (model_id, project_id) ); CREATE TABLE IF NOT EXISTS model_tags ( model_id INTEGER NOT NULL REFERENCES models(id) ON DELETE CASCADE, tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (model_id, tag_id) ); -- Cached GLB conversion of a 3MF, for in-browser 3D preview. ALTER TABLE files ADD COLUMN IF NOT EXISTS glb_path TEXT; -- Whether the model's license permits commercial use (selling). true/false/null. ALTER TABLE models ADD COLUMN IF NOT EXISTS commercial_use BOOLEAN; CREATE INDEX IF NOT EXISTS idx_files_model ON files(model_id); CREATE INDEX IF NOT EXISTS idx_model_projects_p ON model_projects(project_id); CREATE INDEX IF NOT EXISTS idx_model_tags_t ON model_tags(tag_id); CREATE INDEX IF NOT EXISTS idx_models_source ON models(source_site, source_id); -- Content + geometry fingerprints for the duplicate finder (filled in the -- background by lib/fingerprint.js). ALTER TABLE files ADD COLUMN IF NOT EXISTS sha256 TEXT; ALTER TABLE files ADD COLUMN IF NOT EXISTS geometry_hash TEXT; -- null = not checked yet; ok | error | missing | na (format has no geometry fingerprint) ALTER TABLE files ADD COLUMN IF NOT EXISTS geometry_status TEXT; ALTER TABLE files ADD COLUMN IF NOT EXISTS fingerprinted_at TIMESTAMPTZ; CREATE INDEX IF NOT EXISTS idx_files_sha256 ON files(sha256); CREATE INDEX IF NOT EXISTS idx_files_geometry ON files(geometry_hash); -- Free-form notes per model. ALTER TABLE models ADD COLUMN IF NOT EXISTS notes TEXT NOT NULL DEFAULT ''; -- Soft delete: set when a model is moved to the trash. ALTER TABLE models ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; CREATE INDEX IF NOT EXISTS idx_models_deleted ON models(deleted_at); -- Archive files (zip/7z/rar/tar) are indexed in the background: -- null = not yet, ok | error (with archive_error). ALTER TABLE files ADD COLUMN IF NOT EXISTS archive_status TEXT; ALTER TABLE files ADD COLUMN IF NOT EXISTS archive_error TEXT; -- Files inside archives, so they can be browsed, previewed and matched as duplicates. CREATE TABLE IF NOT EXISTS archive_entries ( id SERIAL PRIMARY KEY, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, path TEXT NOT NULL, -- path inside the archive, as the archive stores it format TEXT NOT NULL, size BIGINT NOT NULL DEFAULT 0, sha256 TEXT, geometry_hash TEXT, geometry_status TEXT, -- same states as files.geometry_status fingerprinted_at TIMESTAMPTZ, UNIQUE (file_id, path) ); CREATE INDEX IF NOT EXISTS idx_entries_sha256 ON archive_entries(sha256); CREATE INDEX IF NOT EXISTS idx_entries_geometry ON archive_entries(geometry_hash); -- Background thumbnails: null = not tried yet, none = nothing to make one from -- (left to the browser), error. Cleared when files are added. ALTER TABLE models ADD COLUMN IF NOT EXISTS thumbnail_status TEXT; -- Duplicate groups marked "not duplicates" (signature = sorted model ids). CREATE TABLE IF NOT EXISTS duplicate_dismissals ( signature TEXT PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Where a model's tag came from: user (typed in), rule (automatic tagging) or ai. -- Rule and AI passes only add or remove their own tags; yours are never touched. ALTER TABLE model_tags ADD COLUMN IF NOT EXISTS source TEXT NOT NULL DEFAULT 'user'; -- Automatic tagging: fingerprint of the rules and names the rule tags were last -- worked out from, so a model is re-checked only when one of those changes. ALTER TABLE models ADD COLUMN IF NOT EXISTS autotag_key TEXT; -- Semantic search: a local embedding of the model's text, and the fingerprint of -- the text (and embedding model) it was made from. ALTER TABLE models ADD COLUMN IF NOT EXISTS embedding REAL[]; ALTER TABLE models ADD COLUMN IF NOT EXISTS embedding_key TEXT; -- AI tagging runs, kept so they can be watched, stopped and resumed. ALTER TABLE models ADD COLUMN IF NOT EXISTS ai_tagged_at TIMESTAMPTZ; CREATE TABLE IF NOT EXISTS ai_runs ( id SERIAL PRIMARY KEY, status TEXT NOT NULL DEFAULT 'running', -- running | paused | stopped | done | failed provider TEXT, model TEXT, options JSONB NOT NULL DEFAULT '{}', -- { overwriteDescriptions, maxCost, pricesKnown } total INTEGER NOT NULL DEFAULT 0, done INTEGER NOT NULL DEFAULT 0, failed INTEGER NOT NULL DEFAULT 0, prompt_tokens BIGINT NOT NULL DEFAULT 0, completion_tokens BIGINT NOT NULL DEFAULT 0, cost_usd NUMERIC(12, 6) NOT NULL DEFAULT 0, error TEXT, note TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), finished_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS ai_run_items ( run_id INTEGER NOT NULL REFERENCES ai_runs(id) ON DELETE CASCADE, model_id INTEGER NOT NULL REFERENCES models(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'pending', -- pending | done | error error TEXT, tags TEXT[], updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (run_id, model_id) ); CREATE INDEX IF NOT EXISTS idx_ai_run_items_status ON ai_run_items(run_id, status); -- Your own automatic tagging rules: a word or phrase in a name -> a tag. CREATE TABLE IF NOT EXISTS tag_rules ( id SERIAL PRIMARY KEY, phrase TEXT NOT NULL, tag TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (phrase, tag) ); -- AI renaming: the name a model had before AI first renamed it (for undo), and -- what each run renamed. ALTER TABLE models ADD COLUMN IF NOT EXISTS original_name TEXT; ALTER TABLE ai_run_items ADD COLUMN IF NOT EXISTS renamed_from TEXT; ALTER TABLE ai_run_items ADD COLUMN IF NOT EXISTS renamed_to TEXT; -- Collections the AI suggested for a model, waiting for you to accept or dismiss. -- Dismissed ones are kept so the same idea isn't suggested for that model again. CREATE TABLE IF NOT EXISTS collection_suggestions ( model_id INTEGER NOT NULL REFERENCES models(id) ON DELETE CASCADE, name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', -- pending | accepted | dismissed created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_collection_suggestions_model_name ON collection_suggestions(model_id, lower(name)); ALTER TABLE ai_run_items ADD COLUMN IF NOT EXISTS collections TEXT[]; -- MakerWorld collections watched for new models (lib/collectionWatch.js). CREATE TABLE IF NOT EXISTS watched_collections ( id SERIAL PRIMARY KEY, site TEXT NOT NULL DEFAULT 'makerworld', source_id TEXT NOT NULL, -- the collection's id on the site url TEXT NOT NULL, title TEXT NOT NULL DEFAULT '', -- its name there, refreshed on each check project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL, -- where new models land ai_tag BOOLEAN NOT NULL DEFAULT false, interval_hours INTEGER NOT NULL DEFAULT 6, enabled BOOLEAN NOT NULL DEFAULT true, last_checked_at TIMESTAMPTZ, next_check_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_status TEXT, -- ok | error last_error TEXT, last_error_reason TEXT, -- makerworld_token: offer the token prompt last_added INTEGER NOT NULL DEFAULT 0, listed INTEGER, -- models the page listed at the last check total INTEGER, -- models the site says the collection holds created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (site, source_id) ); -- What a watch has already seen, so a model isn't imported (or retried) twice. CREATE TABLE IF NOT EXISTS watched_collection_items ( watch_id INTEGER NOT NULL REFERENCES watched_collections(id) ON DELETE CASCADE, design_id TEXT NOT NULL, model_id INTEGER REFERENCES models(id) ON DELETE SET NULL, title TEXT NOT NULL DEFAULT '', status TEXT NOT NULL, -- imported | partial (no files yet) | existing | error error TEXT, seen_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (watch_id, design_id) );