diff --git a/TODO.md b/TODO.md index 9995224..65f75d2 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,45 @@ per line; details live in the roadmap. Cross off as things merge to develop. ## Open +UX pass (2026-07-30, author-scoped after the design pass; steers taken up +front: book-cover library grid, Notes to the right rail only, invites as an +admin-set allowance of single-use codes, all designed in-repo). One branch per +item off develop; all seven merged the same day (#512-#518) and shipped as +v3.17.0: + +- [x] Account auto-save (branch `feat/account-autosave`): the Profile and + Assistant sections drop their save buttons and save on field exit or + change, like the other four sections already did. One-shot actions + (claim handle, enable publishing, test connection, discover models, + everything in Security) keep their buttons. `autosubmitForm` learned to + skip `data-no-autosave` controls (the model filter box), a discovery + result now outlives the next action result, and removing a profile link + saves itself. account.spec reworked for blur-driven saves with + alternating values so repeated runs against a persistent database stay + deterministic. +- [x] Import modal: a shared Modal primitive and drop-zone file picker; the + two-step import (upload, preview collisions, confirm) moves into a + modal opened from the library and the universe settings page, so + importing no longer lands next to the danger zone. +- [x] Library covers: the landing page renders stories as 2:3 book covers + (uploaded cover or the SVG fallback), themed with tokens; the library + load learns cover_asset_id. +- [x] Notes to the right rail: Notes leaves the left mode strip (Write / + Plan / Review remain); the right-rail Notes panel becomes the way in + everywhere, including both Plan views (holding the open entity's + notes), with an open-in-full link to the Notes page. Plus quick note + capture: a floating jot affordance on a shortcut that saves to the + story's notes without leaving the editor (backlog item folded in). +- [x] User invites: admins grant a per-user allowance; users generate + single-use codes from a new account section; codes auto-approve the + invited account. Additive migration, admin control, help article. +- [x] Pen name on publish (backlog item folded in): publishing or claiming a + handle with no pen name set asks for one, display name as the offered + default. +- [x] Public page preview (author request 2026-07-30): the owner can open + their own `/@handle` page as a preview even while it is not public, + marked as visible only to them. + Public surfaces (2026-07-30, branch `feat/design-public-pages`). The fourth and last design-pass brief's result, ported: everything a signed-out visitor sees. `src/lib/styles/public.css` is a new layer loaded last, and the `.landing-*` diff --git a/drizzle/0066_user-invite-allowance.sql b/drizzle/0066_user-invite-allowance.sql new file mode 100644 index 0000000..2bad482 --- /dev/null +++ b/drizzle/0066_user-invite-allowance.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "invite_allowance" integer DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0066_snapshot.json b/drizzle/meta/0066_snapshot.json new file mode 100644 index 0000000..b1bbbdc --- /dev/null +++ b/drizzle/meta/0066_snapshot.json @@ -0,0 +1,4948 @@ +{ + "id": "ca169da5-cdda-4b6d-9966-ad6524196346", + "prevId": "d3833390-c0c5-4675-a9f1-2dcb39f217e1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.app_settings": { + "name": "app_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "universe_id": { + "name": "universe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "assets_owner_id_users_id_fk": { + "name": "assets_owner_id_users_id_fk", + "tableFrom": "assets", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_universe_id_universes_id_fk": { + "name": "assets_universe_id_universes_id_fk", + "tableFrom": "assets", + "tableTo": "universes", + "columnsFrom": ["universe_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assistant_chat_messages": { + "name": "assistant_chat_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "universe_id": { + "name": "universe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assistant_chat_messages_conv_idx": { + "name": "assistant_chat_messages_conv_idx", + "columns": [ + { + "expression": "story_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assistant_chat_messages_universe_idx": { + "name": "assistant_chat_messages_universe_idx", + "columns": [ + { + "expression": "universe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assistant_chat_messages_story_id_stories_id_fk": { + "name": "assistant_chat_messages_story_id_stories_id_fk", + "tableFrom": "assistant_chat_messages", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assistant_chat_messages_universe_id_universes_id_fk": { + "name": "assistant_chat_messages_universe_id_universes_id_fk", + "tableFrom": "assistant_chat_messages", + "tableTo": "universes", + "columnsFrom": ["universe_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assistant_chat_messages_user_id_users_id_fk": { + "name": "assistant_chat_messages_user_id_users_id_fk", + "tableFrom": "assistant_chat_messages", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "assistant_chat_messages_scope_ck": { + "name": "assistant_chat_messages_scope_ck", + "value": "(\"assistant_chat_messages\".\"story_id\" is null) <> (\"assistant_chat_messages\".\"universe_id\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.assistant_usage": { + "name": "assistant_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt_tokens": { + "name": "prompt_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "completion_tokens": { + "name": "completion_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assistant_usage_user_idx": { + "name": "assistant_usage_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assistant_usage_user_id_users_id_fk": { + "name": "assistant_usage_user_id_users_id_fk", + "tableFrom": "assistant_usage", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assistant_usage_story_id_stories_id_fk": { + "name": "assistant_usage_story_id_stories_id_fk", + "tableFrom": "assistant_usage", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_tokens": { + "name": "auth_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_tokens_user_id_users_id_fk": { + "name": "auth_tokens_user_id_users_id_fk", + "tableFrom": "auth_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup_runs": { + "name": "backup_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chapters": { + "name": "chapters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_generated_at": { + "name": "summary_generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chapters_story_idx": { + "name": "chapters_story_idx", + "columns": [ + { + "expression": "story_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chapters_story_id_stories_id_fk": { + "name": "chapters_story_id_stories_id_fk", + "tableFrom": "chapters", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.character_story_memberships": { + "name": "character_story_memberships", + "schema": "", + "columns": { + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "declared_at": { + "name": "declared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "character_story_memberships_character_id_characters_id_fk": { + "name": "character_story_memberships_character_id_characters_id_fk", + "tableFrom": "character_story_memberships", + "tableTo": "characters", + "columnsFrom": ["character_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "character_story_memberships_story_id_stories_id_fk": { + "name": "character_story_memberships_story_id_stories_id_fk", + "tableFrom": "character_story_memberships", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "character_story_memberships_character_id_story_id_pk": { + "name": "character_story_memberships_character_id_story_id_pk", + "columns": ["character_id", "story_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.character_story_notes": { + "name": "character_story_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notes_md": { + "name": "notes_md", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "character_story_notes_character_id_characters_id_fk": { + "name": "character_story_notes_character_id_characters_id_fk", + "tableFrom": "character_story_notes", + "tableTo": "characters", + "columnsFrom": ["character_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "character_story_notes_story_id_stories_id_fk": { + "name": "character_story_notes_story_id_stories_id_fk", + "tableFrom": "character_story_notes", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "character_story_notes_unique": { + "name": "character_story_notes_unique", + "nullsNotDistinct": false, + "columns": ["character_id", "story_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.characters": { + "name": "characters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "universe_id": { + "name": "universe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "auto_detect_mentions": { + "name": "auto_detect_mentions", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "category_id": { + "name": "category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "badge_color": { + "name": "badge_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "badge_asset_id": { + "name": "badge_asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "imported_from": { + "name": "imported_from", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "characters_owner_idx": { + "name": "characters_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "characters_name_trgm_idx": { + "name": "characters_name_trgm_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "characters_universe_idx": { + "name": "characters_universe_idx", + "columns": [ + { + "expression": "universe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "characters_universe_id_universes_id_fk": { + "name": "characters_universe_id_universes_id_fk", + "tableFrom": "characters", + "tableTo": "universes", + "columnsFrom": ["universe_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "characters_owner_id_users_id_fk": { + "name": "characters_owner_id_users_id_fk", + "tableFrom": "characters", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "characters_category_id_entity_categories_id_fk": { + "name": "characters_category_id_entity_categories_id_fk", + "tableFrom": "characters", + "tableTo": "entity_categories", + "columnsFrom": ["category_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "characters_badge_asset_id_assets_id_fk": { + "name": "characters_badge_asset_id_assets_id_fk", + "tableFrom": "characters", + "tableTo": "assets", + "columnsFrom": ["badge_asset_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entity_categories": { + "name": "entity_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "universe_id": { + "name": "universe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "entity_categories_universe_id_universes_id_fk": { + "name": "entity_categories_universe_id_universes_id_fk", + "tableFrom": "entity_categories", + "tableTo": "universes", + "columnsFrom": ["universe_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "entity_categories_owner_id_users_id_fk": { + "name": "entity_categories_owner_id_users_id_fk", + "tableFrom": "entity_categories", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entity_mentions": { + "name": "entity_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "surrounding_text": { + "name": "surrounding_text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "entity_mentions_target_idx": { + "name": "entity_mentions_target_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "entity_mentions_source_idx": { + "name": "entity_mentions_source_idx", + "columns": [ + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entity_relationships": { + "name": "entity_relationships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "universe_id": { + "name": "universe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_type": { + "name": "from_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_id": { + "name": "from_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "to_type": { + "name": "to_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_id": { + "name": "to_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "relation_type_id": { + "name": "relation_type_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notes_md": { + "name": "notes_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "entity_relationships_from_idx": { + "name": "entity_relationships_from_idx", + "columns": [ + { + "expression": "from_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "entity_relationships_to_idx": { + "name": "entity_relationships_to_idx", + "columns": [ + { + "expression": "to_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "entity_relationships_scope_idx": { + "name": "entity_relationships_scope_idx", + "columns": [ + { + "expression": "universe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "story_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "entity_relationships_universe_unique": { + "name": "entity_relationships_universe_unique", + "columns": [ + { + "expression": "relation_type_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"entity_relationships\".\"story_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entity_relationships_universe_id_universes_id_fk": { + "name": "entity_relationships_universe_id_universes_id_fk", + "tableFrom": "entity_relationships", + "tableTo": "universes", + "columnsFrom": ["universe_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "entity_relationships_owner_id_users_id_fk": { + "name": "entity_relationships_owner_id_users_id_fk", + "tableFrom": "entity_relationships", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "entity_relationships_relation_type_id_relation_types_id_fk": { + "name": "entity_relationships_relation_type_id_relation_types_id_fk", + "tableFrom": "entity_relationships", + "tableTo": "relation_types", + "columnsFrom": ["relation_type_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "entity_relationships_story_id_stories_id_fk": { + "name": "entity_relationships_story_id_stories_id_fk", + "tableFrom": "entity_relationships", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entity_suggestions": { + "name": "entity_suggestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_kind": { + "name": "entity_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "field": { + "name": "field", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "entity_suggestions_entity_idx": { + "name": "entity_suggestions_entity_idx", + "columns": [ + { + "expression": "entity_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entity_suggestions_owner_id_users_id_fk": { + "name": "entity_suggestions_owner_id_users_id_fk", + "tableFrom": "entity_suggestions", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.export_artifacts": { + "name": "export_artifacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "publication_id": { + "name": "publication_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "export_artifacts_publication_id_publications_id_fk": { + "name": "export_artifacts_publication_id_publications_id_fk", + "tableFrom": "export_artifacts", + "tableTo": "publications", + "columnsFrom": ["publication_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "export_artifacts_one_per_format": { + "name": "export_artifacts_one_per_format", + "nullsNotDistinct": false, + "columns": ["publication_id", "format"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_codes": { + "name": "invite_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "used_count": { + "name": "used_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invite_codes_created_by_users_id_fk": { + "name": "invite_codes_created_by_users_id_fk", + "tableFrom": "invite_codes", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invite_codes_code_unique": { + "name": "invite_codes_code_unique", + "nullsNotDistinct": false, + "columns": ["code"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lore_entries": { + "name": "lore_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "universe_id": { + "name": "universe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "badge_color": { + "name": "badge_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "badge_asset_id": { + "name": "badge_asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "keywords": { + "name": "keywords", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "activation_mode": { + "name": "activation_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'keyword'" + }, + "auto_detect_mentions": { + "name": "auto_detect_mentions", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "lore_entries_owner_idx": { + "name": "lore_entries_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "lore_entries_title_trgm_idx": { + "name": "lore_entries_title_trgm_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "lore_entries_universe_idx": { + "name": "lore_entries_universe_idx", + "columns": [ + { + "expression": "universe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "lore_entries_universe_id_universes_id_fk": { + "name": "lore_entries_universe_id_universes_id_fk", + "tableFrom": "lore_entries", + "tableTo": "universes", + "columnsFrom": ["universe_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "lore_entries_owner_id_users_id_fk": { + "name": "lore_entries_owner_id_users_id_fk", + "tableFrom": "lore_entries", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "lore_entries_category_id_entity_categories_id_fk": { + "name": "lore_entries_category_id_entity_categories_id_fk", + "tableFrom": "lore_entries", + "tableTo": "entity_categories", + "columnsFrom": ["category_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "lore_entries_badge_asset_id_assets_id_fk": { + "name": "lore_entries_badge_asset_id_assets_id_fk", + "tableFrom": "lore_entries", + "tableTo": "assets", + "columnsFrom": ["badge_asset_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lore_story_notes": { + "name": "lore_story_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lore_entry_id": { + "name": "lore_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notes_md": { + "name": "notes_md", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "lore_story_notes_lore_entry_id_lore_entries_id_fk": { + "name": "lore_story_notes_lore_entry_id_lore_entries_id_fk", + "tableFrom": "lore_story_notes", + "tableTo": "lore_entries", + "columnsFrom": ["lore_entry_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "lore_story_notes_story_id_stories_id_fk": { + "name": "lore_story_notes_story_id_stories_id_fk", + "tableFrom": "lore_story_notes", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "lore_story_notes_unique": { + "name": "lore_story_notes_unique", + "nullsNotDistinct": false, + "columns": ["lore_entry_id", "story_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mention_pins": { + "name": "mention_pins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mention_pins_story_id_stories_id_fk": { + "name": "mention_pins_story_id_stories_id_fk", + "tableFrom": "mention_pins", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mention_pins_story_name": { + "name": "mention_pins_story_name", + "nullsNotDistinct": false, + "columns": ["story_id", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notes": { + "name": "notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "universe_id": { + "name": "universe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chapter_id": { + "name": "chapter_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scene_id": { + "name": "scene_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notes_universe_idx": { + "name": "notes_universe_idx", + "columns": [ + { + "expression": "universe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notes_story_idx": { + "name": "notes_story_idx", + "columns": [ + { + "expression": "story_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notes_owner_id_users_id_fk": { + "name": "notes_owner_id_users_id_fk", + "tableFrom": "notes", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notes_universe_id_universes_id_fk": { + "name": "notes_universe_id_universes_id_fk", + "tableFrom": "notes", + "tableTo": "universes", + "columnsFrom": ["universe_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notes_story_id_stories_id_fk": { + "name": "notes_story_id_stories_id_fk", + "tableFrom": "notes", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notes_chapter_id_chapters_id_fk": { + "name": "notes_chapter_id_chapters_id_fk", + "tableFrom": "notes", + "tableTo": "chapters", + "columnsFrom": ["chapter_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notes_scene_id_scenes_id_fk": { + "name": "notes_scene_id_scenes_id_fk", + "tableFrom": "notes", + "tableTo": "scenes", + "columnsFrom": ["scene_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "in_app": { + "name": "in_app", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_wanted": { + "name": "email_wanted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "emailed_at": { + "name": "emailed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_user_idx": { + "name": "notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.place_story_memberships": { + "name": "place_story_memberships", + "schema": "", + "columns": { + "place_id": { + "name": "place_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "declared_at": { + "name": "declared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "place_story_memberships_place_id_places_id_fk": { + "name": "place_story_memberships_place_id_places_id_fk", + "tableFrom": "place_story_memberships", + "tableTo": "places", + "columnsFrom": ["place_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "place_story_memberships_story_id_stories_id_fk": { + "name": "place_story_memberships_story_id_stories_id_fk", + "tableFrom": "place_story_memberships", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "place_story_memberships_place_id_story_id_pk": { + "name": "place_story_memberships_place_id_story_id_pk", + "columns": ["place_id", "story_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.place_story_notes": { + "name": "place_story_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "place_id": { + "name": "place_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notes_md": { + "name": "notes_md", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "place_story_notes_place_id_places_id_fk": { + "name": "place_story_notes_place_id_places_id_fk", + "tableFrom": "place_story_notes", + "tableTo": "places", + "columnsFrom": ["place_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "place_story_notes_story_id_stories_id_fk": { + "name": "place_story_notes_story_id_stories_id_fk", + "tableFrom": "place_story_notes", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "place_story_notes_unique": { + "name": "place_story_notes_unique", + "nullsNotDistinct": false, + "columns": ["place_id", "story_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.places": { + "name": "places", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "universe_id": { + "name": "universe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "auto_detect_mentions": { + "name": "auto_detect_mentions", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "category_id": { + "name": "category_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "badge_color": { + "name": "badge_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "badge_asset_id": { + "name": "badge_asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "places_owner_idx": { + "name": "places_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "places_name_trgm_idx": { + "name": "places_name_trgm_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "places_universe_idx": { + "name": "places_universe_idx", + "columns": [ + { + "expression": "universe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "places_universe_id_universes_id_fk": { + "name": "places_universe_id_universes_id_fk", + "tableFrom": "places", + "tableTo": "universes", + "columnsFrom": ["universe_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "places_owner_id_users_id_fk": { + "name": "places_owner_id_users_id_fk", + "tableFrom": "places", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "places_category_id_entity_categories_id_fk": { + "name": "places_category_id_entity_categories_id_fk", + "tableFrom": "places", + "tableTo": "entity_categories", + "columnsFrom": ["category_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "places_badge_asset_id_assets_id_fk": { + "name": "places_badge_asset_id_assets_id_fk", + "tableFrom": "places", + "tableTo": "assets", + "columnsFrom": ["badge_asset_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publication_assets": { + "name": "publication_assets", + "schema": "", + "columns": { + "publication_id": { + "name": "publication_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "publication_assets_asset_idx": { + "name": "publication_assets_asset_idx", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "publication_assets_publication_id_publications_id_fk": { + "name": "publication_assets_publication_id_publications_id_fk", + "tableFrom": "publication_assets", + "tableTo": "publications", + "columnsFrom": ["publication_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publication_assets_asset_id_assets_id_fk": { + "name": "publication_assets_asset_id_assets_id_fk", + "tableFrom": "publication_assets", + "tableTo": "assets", + "columnsFrom": ["asset_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "publication_assets_publication_id_asset_id_pk": { + "name": "publication_assets_publication_id_asset_id_pk", + "columns": ["publication_id", "asset_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publications": { + "name": "publications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description_md": { + "name": "description_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_adult": { + "name": "is_adult", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version_label": { + "name": "version_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "downloads_public": { + "name": "downloads_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_current": { + "name": "is_current", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "artifact_errors": { + "name": "artifact_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "publications_one_current_per_story": { + "name": "publications_one_current_per_story", + "columns": [ + { + "expression": "story_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"publications\".\"is_current\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "publications_story_id_stories_id_fk": { + "name": "publications_story_id_stories_id_fk", + "tableFrom": "publications", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publications_owner_id_users_id_fk": { + "name": "publications_owner_id_users_id_fk", + "tableFrom": "publications", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relation_types": { + "name": "relation_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "universe_id": { + "name": "universe_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "forward_label": { + "name": "forward_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reverse_label": { + "name": "reverse_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bidirectional": { + "name": "bidirectional", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "from_type": { + "name": "from_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_type": { + "name": "to_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "relation_types_universe_id_universes_id_fk": { + "name": "relation_types_universe_id_universes_id_fk", + "tableFrom": "relation_types", + "tableTo": "universes", + "columnsFrom": ["universe_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "relation_types_universe_key": { + "name": "relation_types_universe_key", + "nullsNotDistinct": true, + "columns": ["universe_id", "key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_comments": { + "name": "review_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_reviewer_id": { + "name": "author_reviewer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assistant": { + "name": "assistant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_comments_thread_idx": { + "name": "review_comments_thread_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_comments_thread_id_review_threads_id_fk": { + "name": "review_comments_thread_id_review_threads_id_fk", + "tableFrom": "review_comments", + "tableTo": "review_threads", + "columnsFrom": ["thread_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_comments_author_user_id_users_id_fk": { + "name": "review_comments_author_user_id_users_id_fk", + "tableFrom": "review_comments", + "tableTo": "users", + "columnsFrom": ["author_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_comments_author_reviewer_id_reviewers_id_fk": { + "name": "review_comments_author_reviewer_id_reviewers_id_fk", + "tableFrom": "review_comments", + "tableTo": "reviewers", + "columnsFrom": ["author_reviewer_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_invitations": { + "name": "review_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "can_suggest": { + "name": "can_suggest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_invitations_story_idx": { + "name": "review_invitations_story_idx", + "columns": [ + { + "expression": "story_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_invitations_story_id_stories_id_fk": { + "name": "review_invitations_story_id_stories_id_fk", + "tableFrom": "review_invitations", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_invitations_created_by_users_id_fk": { + "name": "review_invitations_created_by_users_id_fk", + "tableFrom": "review_invitations", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "review_invitations_token_hash_unique": { + "name": "review_invitations_token_hash_unique", + "nullsNotDistinct": false, + "columns": ["token_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_suggestions": { + "name": "review_suggestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scene_id": { + "name": "scene_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewer_id": { + "name": "reviewer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assistant": { + "name": "assistant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "base_revision_id": { + "name": "base_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "range_start": { + "name": "range_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "range_end": { + "name": "range_end", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_suggestions_scene_idx": { + "name": "review_suggestions_scene_idx", + "columns": [ + { + "expression": "scene_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_suggestions_story_idx": { + "name": "review_suggestions_story_idx", + "columns": [ + { + "expression": "story_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_suggestions_story_id_stories_id_fk": { + "name": "review_suggestions_story_id_stories_id_fk", + "tableFrom": "review_suggestions", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_suggestions_scene_id_scenes_id_fk": { + "name": "review_suggestions_scene_id_scenes_id_fk", + "tableFrom": "review_suggestions", + "tableTo": "scenes", + "columnsFrom": ["scene_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_suggestions_author_user_id_users_id_fk": { + "name": "review_suggestions_author_user_id_users_id_fk", + "tableFrom": "review_suggestions", + "tableTo": "users", + "columnsFrom": ["author_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_suggestions_reviewer_id_reviewers_id_fk": { + "name": "review_suggestions_reviewer_id_reviewers_id_fk", + "tableFrom": "review_suggestions", + "tableTo": "reviewers", + "columnsFrom": ["reviewer_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_suggestions_base_revision_id_revisions_id_fk": { + "name": "review_suggestions_base_revision_id_revisions_id_fk", + "tableFrom": "review_suggestions", + "tableTo": "revisions", + "columnsFrom": ["base_revision_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_suggestions_decided_by_user_id_users_id_fk": { + "name": "review_suggestions_decided_by_user_id_users_id_fk", + "tableFrom": "review_suggestions", + "tableTo": "users", + "columnsFrom": ["decided_by_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_threads": { + "name": "review_threads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scene_id": { + "name": "scene_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "anchor_start": { + "name": "anchor_start", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "anchor_end": { + "name": "anchor_end", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "base_revision_id": { + "name": "base_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "suggestion_id": { + "name": "suggestion_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_threads_scene_idx": { + "name": "review_threads_scene_idx", + "columns": [ + { + "expression": "scene_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_threads_story_idx": { + "name": "review_threads_story_idx", + "columns": [ + { + "expression": "story_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_threads_story_id_stories_id_fk": { + "name": "review_threads_story_id_stories_id_fk", + "tableFrom": "review_threads", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_threads_scene_id_scenes_id_fk": { + "name": "review_threads_scene_id_scenes_id_fk", + "tableFrom": "review_threads", + "tableTo": "scenes", + "columnsFrom": ["scene_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_threads_base_revision_id_revisions_id_fk": { + "name": "review_threads_base_revision_id_revisions_id_fk", + "tableFrom": "review_threads", + "tableTo": "revisions", + "columnsFrom": ["base_revision_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_threads_suggestion_id_review_suggestions_id_fk": { + "name": "review_threads_suggestion_id_review_suggestions_id_fk", + "tableFrom": "review_threads", + "tableTo": "review_suggestions", + "columnsFrom": ["suggestion_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "review_threads_resolved_by_user_id_users_id_fk": { + "name": "review_threads_resolved_by_user_id_users_id_fk", + "tableFrom": "review_threads", + "tableTo": "users", + "columnsFrom": ["resolved_by_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "review_threads_suggestion_id_unique": { + "name": "review_threads_suggestion_id_unique", + "nullsNotDistinct": false, + "columns": ["suggestion_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reviewers": { + "name": "reviewers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invitation_id": { + "name": "invitation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_notified_at": { + "name": "last_notified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_opt_out_at": { + "name": "email_opt_out_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "reviewers_invitation_idx": { + "name": "reviewers_invitation_idx", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reviewers_invitation_user_unique": { + "name": "reviewers_invitation_user_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "user_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reviewers_invitation_id_review_invitations_id_fk": { + "name": "reviewers_invitation_id_review_invitations_id_fk", + "tableFrom": "reviewers", + "tableTo": "review_invitations", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reviewers_user_id_users_id_fk": { + "name": "reviewers_user_id_users_id_fk", + "tableFrom": "reviewers", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.revisions": { + "name": "revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "revisions_timeline_idx": { + "name": "revisions_timeline_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scene_markers": { + "name": "scene_markers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scene_id": { + "name": "scene_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'todo'" + }, + "anchor_start": { + "name": "anchor_start", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "anchor_end": { + "name": "anchor_end", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scene_markers_scene_idx": { + "name": "scene_markers_scene_idx", + "columns": [ + { + "expression": "scene_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scene_markers_scene_id_scenes_id_fk": { + "name": "scene_markers_scene_id_scenes_id_fk", + "tableFrom": "scene_markers", + "tableTo": "scenes", + "columnsFrom": ["scene_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scene_markers_owner_id_users_id_fk": { + "name": "scene_markers_owner_id_users_id_fk", + "tableFrom": "scene_markers", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenes": { + "name": "scenes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "story_id": { + "name": "story_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chapter_id": { + "name": "chapter_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "position_in_chapter": { + "name": "position_in_chapter", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "global_position": { + "name": "global_position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "pov_character_id": { + "name": "pov_character_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "story_time": { + "name": "story_time", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "characters_present": { + "name": "characters_present", + "type": "uuid[]", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_generated_at": { + "name": "summary_generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "word_count": { + "name": "word_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "mentions_indexed_at": { + "name": "mentions_indexed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scenes_characters_present_gin": { + "name": "scenes_characters_present_gin", + "columns": [ + { + "expression": "characters_present", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "scenes_body_trgm_idx": { + "name": "scenes_body_trgm_idx", + "columns": [ + { + "expression": "body_md", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "scenes_story_idx": { + "name": "scenes_story_idx", + "columns": [ + { + "expression": "story_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenes_story_id_stories_id_fk": { + "name": "scenes_story_id_stories_id_fk", + "tableFrom": "scenes", + "tableTo": "stories", + "columnsFrom": ["story_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenes_chapter_id_chapters_id_fk": { + "name": "scenes_chapter_id_chapters_id_fk", + "tableFrom": "scenes", + "tableTo": "chapters", + "columnsFrom": ["chapter_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "inet", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_hash_unique": { + "name": "sessions_token_hash_unique", + "nullsNotDistinct": false, + "columns": ["token_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stories": { + "name": "stories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "universe_id": { + "name": "universe_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "substr(md5(random()::text), 1, 12)" + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brief": { + "name": "brief", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description_md": { + "name": "description_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "style_notes": { + "name": "style_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position_in_series": { + "name": "position_in_series", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "is_adult": { + "name": "is_adult", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cover_asset_id": { + "name": "cover_asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "llm_config": { + "name": "llm_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "preferences": { + "name": "preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "page_setup": { + "name": "page_setup", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "target_words": { + "name": "target_words", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "stories_owner_slug_idx": { + "name": "stories_owner_slug_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "stories_universe_idx": { + "name": "stories_universe_idx", + "columns": [ + { + "expression": "universe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stories_universe_id_universes_id_fk": { + "name": "stories_universe_id_universes_id_fk", + "tableFrom": "stories", + "tableTo": "universes", + "columnsFrom": ["universe_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "stories_owner_id_users_id_fk": { + "name": "stories_owner_id_users_id_fk", + "tableFrom": "stories", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.totp_recovery_codes": { + "name": "totp_recovery_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "totp_recovery_codes_user_idx": { + "name": "totp_recovery_codes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "totp_recovery_codes_user_id_users_id_fk": { + "name": "totp_recovery_codes_user_id_users_id_fk", + "tableFrom": "totp_recovery_codes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.universes": { + "name": "universes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "substr(md5(random()::text), 1, 12)" + }, + "description_md": { + "name": "description_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "established_setting": { + "name": "established_setting", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "standalone": { + "name": "standalone", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "universes_owner_slug_idx": { + "name": "universes_owner_slug_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "universes_one_standalone_idx": { + "name": "universes_one_standalone_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"universes\".\"standalone\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "universes_owner_id_users_id_fk": { + "name": "universes_owner_id_users_id_fk", + "tableFrom": "universes", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_exports": { + "name": "user_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "byte_size": { + "name": "byte_size", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_exports_owner_idx": { + "name": "user_exports_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_exports_owner_id_users_id_fk": { + "name": "user_exports_owner_id_users_id_fk", + "tableFrom": "user_exports", + "tableTo": "users", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_totp": { + "name": "user_totp", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_step": { + "name": "last_used_step", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_totp_user_id_users_id_fk": { + "name": "user_totp_user_id_users_id_fk", + "tableFrom": "user_totp", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_email": { + "name": "pending_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pen_name": { + "name": "pen_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "citext", + "primaryKey": false, + "notNull": false + }, + "bio_md": { + "name": "bio_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "commissions_open": { + "name": "commissions_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "commissions_md": { + "name": "commissions_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_asset_id": { + "name": "avatar_asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "profile_public": { + "name": "profile_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "public_archive_enabled": { + "name": "public_archive_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "invite_allowance": { + "name": "invite_allowance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_config": { + "name": "llm_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "preferences": { + "name": "preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "page_setup": { + "name": "page_setup", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "plan_id": { + "name": "plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email_verified_at": { + "name": "email_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deletion_scheduled_at": { + "name": "deletion_scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "users_handle_unique": { + "name": "users_handle_unique", + "nullsNotDistinct": false, + "columns": ["handle"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sign_count": { + "name": "sign_count", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webauthn_credentials_user_idx": { + "name": "webauthn_credentials_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webauthn_credentials_credential_id_unique": { + "name": "webauthn_credentials_credential_id_unique", + "nullsNotDistinct": false, + "columns": ["credential_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index fc919c5..6707690 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -463,6 +463,13 @@ "when": 1782216943382, "tag": "0065_pink_landau", "breakpoints": true + }, + { + "idx": 66, + "version": "7", + "when": 1785434997269, + "tag": "0066_user-invite-allowance", + "breakpoints": true } ] } diff --git a/e2e/account-invites.spec.ts b/e2e/account-invites.spec.ts new file mode 100644 index 0000000..9d7d881 --- /dev/null +++ b/e2e/account-invites.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@playwright/test'; +import { gotoReady } from './navigate'; + +// The account Invites section: global-setup grants the e2e user an allowance +// of five and clears unused codes, so the run starts with slots free. The +// test revokes what it makes, keeping repeated runs at the same start. +test('generate, copy-link, and revoke an invite', async ({ page }) => { + await gotoReady(page, '/account'); + await page.getByRole('link', { name: 'Invites' }).click(); + await expect(page).toHaveURL('/account/invites'); + await expect(page.getByRole('heading', { name: 'Invites', level: 1 })).toBeVisible(); + + const rows = page.locator('.list-row'); + const before = await rows.count(); + await page.getByRole('button', { name: 'Generate an invite' }).click(); + await expect(rows).toHaveCount(before + 1); + + // Newest first: the fresh code leads, unused, with its share affordance. + const row = rows.first(); + await expect(row.locator('.list-sub')).toHaveText('Not used yet'); + await expect(row.getByRole('button', { name: 'Copy link' })).toBeVisible(); + + await row.getByRole('button', { name: 'Revoke' }).click(); + await expect(rows).toHaveCount(before); +}); diff --git a/e2e/account.spec.ts b/e2e/account.spec.ts index a09356f..b6bef0b 100644 --- a/e2e/account.spec.ts +++ b/e2e/account.spec.ts @@ -12,10 +12,15 @@ test('account settings: rename and see the current session', async ({ page }) => // The sidebar shows who is signed in. await expect(page.getByRole('complementary').getByText('e2e@example.com')).toBeVisible(); - // Profile is the default section; a fixed name keeps repeated runs idempotent. - await page.getByLabel('Display name').fill('E2E Tester'); - await page.getByLabel('Pen name').fill('E. Tester'); - await page.getByRole('button', { name: 'Save changes' }).click(); + // Profile is the default section. Fields save when they lose focus; the + // name alternates between two values so a repeated run against a + // persistent database still changes it (an unchanged field fires no + // change event and has nothing to save). Both values keep the "Tester" + // substring the core-flow shelf assertion reads for. + const nameField = page.getByLabel('Display name'); + const savedName = await nameField.inputValue(); + await nameField.fill(savedName === 'E2E Tester' ? 'E2E Tester II' : 'E2E Tester'); + await nameField.blur(); await expect(page.getByRole('status')).toContainText('Saved'); // The top-right avatar opens the account menu; Esc closes it. @@ -101,21 +106,22 @@ test('account assistant: kill switch, identity, and endpoint persist', async ({ await expect(status).toHaveText('Assistant on'); await expect(gated).not.toHaveClass(/off/); - // Identity saves a name and tone and reads them back after a reload. Exact - // labels: "Name" otherwise also matches "Display name" and "Pen name". + // Identity saves on change: picking a style submits the form, carrying the + // name typed just before it. selectOption always fires a change event, so + // this saves deterministically even on repeated runs. Exact labels: "Name" + // otherwise also matches "Display name" and "Pen name". await page.getByLabel('Name', { exact: true }).fill('Margin'); await page.getByLabel('Style', { exact: true }).selectOption('concise'); - await page.getByRole('button', { name: 'Save identity' }).click(); await expect(page.getByRole('status')).toContainText('Saved'); await page.reload(); await expect(page.getByLabel('Name', { exact: true })).toHaveValue('Margin'); - // A provider preset prefills and locks the base URL and persists on save. + // A provider preset prefills and locks the base URL, and picking one saves + // at once. await page.getByLabel('Provider', { exact: true }).selectOption('anthropic'); const presetUrl = page.getByLabel('Base URL', { exact: true }); await expect(presetUrl).toHaveValue('https://api.anthropic.com'); await expect(presetUrl).toHaveAttribute('readonly', ''); - await page.getByRole('button', { name: 'Save endpoint' }).click(); await expect(page.getByRole('status')).toContainText('Saved'); await page.reload(); await expect(page.getByLabel('Provider', { exact: true })).toHaveValue('anthropic'); @@ -123,11 +129,20 @@ test('account assistant: kill switch, identity, and endpoint persist', async ({ 'https://api.anthropic.com' ); - // Back to a custom endpoint: the URL field frees up and saves what is typed. + // Back to a custom endpoint: switching the provider saves at once (with an + // empty URL), then typing the URL and leaving the field saves again. The + // reload below must not race that second save, so wait for its response; + // the "Saved." from the provider switch is already on screen. await page.getByLabel('Provider', { exact: true }).selectOption('custom'); - await page.getByLabel('Base URL', { exact: true }).fill('http://ollama.local:11434/v1'); - await page.getByRole('button', { name: 'Save endpoint' }).click(); await expect(page.getByRole('status')).toContainText('Saved'); + const urlField = page.getByLabel('Base URL', { exact: true }); + await urlField.fill('http://ollama.local:11434/v1'); + const endpointSaved = page.waitForResponse( + (response) => + response.url().includes('saveAssistantEndpoint') && response.request().method() === 'POST' + ); + await urlField.blur(); + await endpointSaved; await page.reload(); await expect(page.getByLabel('Base URL', { exact: true })).toHaveValue( 'http://ollama.local:11434/v1' @@ -152,9 +167,15 @@ test('assistant tab: gated by the account switch and muted per story', async ({ await gotoReady(page, '/account/assistant'); if ((await status.textContent())?.trim() === 'Assistant off') await killToggle.click(); await expect(status).toHaveText('Assistant on'); - await page.getByLabel('Base URL', { exact: true }).fill('http://ollama.local:11434/v1'); - await page.getByRole('button', { name: 'Save endpoint' }).click(); - await expect(page.getByRole('status')).toContainText('Saved'); + // The first test normally leaves the custom Ollama endpoint saved; set it + // only when absent, since an unchanged field fires no change event. + const urlField = page.getByLabel('Base URL', { exact: true }); + if ((await urlField.inputValue()) !== 'http://ollama.local:11434/v1') { + await page.getByLabel('Provider', { exact: true }).selectOption('custom'); + await urlField.fill('http://ollama.local:11434/v1'); + await urlField.blur(); + await expect(page.getByRole('status')).toContainText('Saved'); + } // A throwaway story to open the editor against. const universeName = `AI gate ${Date.now()}`; diff --git a/e2e/core-flow.spec.ts b/e2e/core-flow.spec.ts index 6a63b45..b3027f7 100644 --- a/e2e/core-flow.spec.ts +++ b/e2e/core-flow.spec.ts @@ -514,6 +514,14 @@ test('sign in, create a universe and a story, and open it', async ({ page, brows await page.getByRole('button', { name: 'Save visibility' }).click(); await expect(page.getByRole('status')).toHaveText('Saved.'); await page.getByRole('button', { name: 'Publish edition' }).click(); + // The first publish with no pen name set asks what name goes public; + // whether it appears depends on whether the account spec has run, so + // keep the offered display name when it does. The dialog opens + // synchronously from the intercepted submit. + const penPrompt = page.getByRole('dialog', { name: 'Choose your author name' }); + if (await penPrompt.isVisible()) { + await penPrompt.getByRole('button', { name: 'Continue' }).click(); + } await expect(page.getByRole('status')).toContainText('Edition published.'); const anonymous = await browser.newContext({ storageState: { cookies: [], origins: [] } }); diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts index 82c008e..76b0673 100644 --- a/e2e/dashboard.spec.ts +++ b/e2e/dashboard.spec.ts @@ -30,6 +30,8 @@ test('dashboard: universe sections, story cards, and the New story menu', async await expect(card).toHaveCount(1); await expect(card.locator('.story-card-status')).toHaveText(/Outlining/); await expect(card.locator('.story-card-meta')).toContainText('edited just now'); + // With no artwork uploaded, the cover blank carries the title. + await expect(card.locator('.story-card-cover')).toContainText(`Shelved ${stamp}`); await expect( page.locator('.recent-row .story-card', { hasText: `Shelved ${stamp}` }) ).toHaveCount(1); diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index cae62cf..abf8685 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -70,12 +70,19 @@ export default async function globalSetup() { parallelism: 1 }); await pool.query( - `insert into users (email, display_name, password_hash, role, email_verified_at, approved_at, handle, public_archive_enabled) - values ($1, 'E2E Tester', $2, 'user', now(), now(), 'e2e-tester', true) + `insert into users (email, display_name, password_hash, role, email_verified_at, approved_at, handle, public_archive_enabled, invite_allowance) + values ($1, 'E2E Tester', $2, 'user', now(), now(), 'e2e-tester', true, 5) on conflict (email) do update set password_hash = excluded.password_hash, - handle = excluded.handle, public_archive_enabled = excluded.public_archive_enabled`, + handle = excluded.handle, public_archive_enabled = excluded.public_archive_enabled, + invite_allowance = excluded.invite_allowance`, ['e2e@example.com', passwordHash] ); + // The account-invites spec spends and revokes invites; clearing the unused + // ones keeps repeated runs at the same starting point. + await pool.query( + `delete from invite_codes where used_count = 0 + and created_by = (select id from users where email = 'e2e@example.com')` + ); // A separate account for the two-factor journey, so toggling 2FA there never // trips the password-only sign-ins the other specs rely on. await pool.query( diff --git a/e2e/import-document.spec.ts b/e2e/import-document.spec.ts index 0c40811..8e9c93b 100644 --- a/e2e/import-document.spec.ts +++ b/e2e/import-document.spec.ts @@ -57,16 +57,21 @@ test('story import: preview and import a Word manuscript', async ({ page }) => { title ); + // The settings page's Import button is the modal's second entry point; + // import-story.spec covers the library-menu one. Picking a file previews + // it at once. await gotoReady(page, `/universes/docland-${stamp}`); await page.getByRole('link', { name: 'Import and export' }).click(); - await page.locator('input[name="archive"]').setInputFiles({ + await page.getByRole('button', { name: 'Import a story...' }).click(); + const dialog = page.getByRole('dialog', { name: 'Import a story' }); + await expect(dialog).toBeVisible(); + await dialog.locator('input[name="archive"]').setInputFiles({ name: 'manuscript.docx', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', buffer: docx }); - await page.getByRole('button', { name: 'Preview' }).click(); - const report = page.locator('.import-report'); + const report = dialog.locator('.import-report'); await expect(report).toContainText(`"${title}": 2 chapters, 3 scenes`); await page.getByRole('button', { name: 'Import story' }).click(); diff --git a/e2e/import-story.spec.ts b/e2e/import-story.spec.ts index c5ad9a1..12c788f 100644 --- a/e2e/import-story.spec.ts +++ b/e2e/import-story.spec.ts @@ -3,7 +3,7 @@ import { gotoReady } from './navigate'; import { pickFromLibraryMenu, startStoryInUniverse } from './library'; // The story import round trip: write a story, download its export zip, and -// import the zip back through the universe settings preview flow. +// import the zip back through the library's import modal preview flow. test('story import: preview and import a story export zip', async ({ page }) => { await gotoReady(page, '/'); @@ -63,16 +63,23 @@ test('story import: preview and import a story export zip', async ({ page }) => expect(archive.status()).toBe(200); const zipBytes = await archive.body(); - await gotoReady(page, `/universes/importland-${stamp}`); - await page.getByRole('link', { name: 'Import and export' }).click(); - await page.locator('input[name="archive"]').setInputFiles({ + // Feed it back through the import modal, opened from the universe's New + // story menu on the library. Picking a file previews it at once. + await gotoReady(page, '/'); + await page + .locator('.universe-section', { hasText: universeName }) + .getByRole('button', { name: 'New story', exact: true }) + .click(); + await page.getByRole('menuitem', { name: 'Import a story into this universe...' }).click(); + const dialog = page.getByRole('dialog', { name: 'Import a story' }); + await expect(dialog).toBeVisible(); + await dialog.locator('input[name="archive"]').setInputFiles({ name: 'roundtrip.zip', mimeType: 'application/zip', buffer: zipBytes }); - await page.getByRole('button', { name: 'Preview' }).click(); - const report = page.locator('.import-report'); + const report = dialog.locator('.import-report'); await expect(report).toContainText('"Roundtrip": 1 chapter, 1 scene'); await expect(report).toContainText('A story named "Roundtrip" already exists'); await expect(report).toContainText('Bram'); diff --git a/e2e/notes.spec.ts b/e2e/notes.spec.ts index ec926af..d94e5c6 100644 --- a/e2e/notes.spec.ts +++ b/e2e/notes.spec.ts @@ -2,9 +2,9 @@ import { expect, test } from '@playwright/test'; import { gotoReady } from './navigate'; import { pickFromLibraryMenu, startStoryInUniverse } from './library'; -// The Notes view: reach it from the editor's Notes toggle, write a note that -// persists, pin it, and confirm a story note shows under the universe view's -// reach via the "From the universe" peek. +// The Notes view: reach it from the editor's right-rail Notes panel, write a +// note that persists, pin it, and confirm a story note shows under the +// universe view's reach via the "From the universe" peek. test('write, persist, and pin a story note', async ({ page }) => { page.on('dialog', (dialog) => dialog.accept()); @@ -20,12 +20,17 @@ test('write, persist, and pin a story note', async ({ page }) => { await page.getByRole('button', { name: 'Create story' }).click(); await expect(page).toHaveURL(`/stories/logs-${stamp}`); - // The Notes toggle now works (it used to be disabled). - await page.getByRole('link', { name: 'Notes' }).click(); - await expect(page).toHaveURL(`/stories/logs-${stamp}/notes`); + // Notes lives in the right rail: the panel is the way in, and with no + // notes yet it offers the first one, landing on the Notes page. + await page.getByRole('tab', { name: 'Notes' }).click(); + await expect(page.locator('.panel-body:not([hidden])')).toContainText('No notes in this story'); + await page.getByRole('button', { name: 'New note', exact: true }).click(); + await expect(page).toHaveURL(new RegExp(`/stories/logs-${stamp}/notes\\?note=`)); - await page.getByRole('button', { name: 'New note' }).click(); - await expect(page).toHaveURL(/note=/); + // The strip on the Notes page lights nothing: Notes is not a mode. + await expect(page.locator('.mode-strip .seg-btn')).toHaveCount(3); + await expect(page.locator('.mode-strip .seg-btn.active')).toHaveCount(0); + await expect(page.locator('.mode-note')).toContainText('Notes sit beside every mode'); await page.getByPlaceholder('Untitled note').fill('Table talk'); await page.locator('.cm-content').click(); @@ -77,10 +82,10 @@ test('a note on the open scene, from the Notes panel in Write', async ({ page }) await page.getByRole('button', { name: 'New scene' }).click(); await expect(page).toHaveURL(/scene=/); - // Four panels on Write, in the one order, and Notes starts empty. + // The Notes panel is always on the strip, and starts empty. await expect(page.getByRole('tab', { name: 'Reference' })).toBeVisible(); await page.getByRole('tab', { name: 'Notes' }).click(); - await expect(page.locator('.panel-body:not([hidden])')).toContainText('No notes on this scene'); + await expect(page.locator('.panel-body:not([hidden])')).toContainText('No notes in this story'); // The action makes the note and opens it in Notes mode, where notes live. await page.getByRole('button', { name: 'New note on this scene' }).click(); @@ -103,4 +108,17 @@ test('a note on the open scene, from the Notes panel in Write', async ({ page }) 'true' ); await expect(notesTab).toHaveAttribute('aria-selected', 'false'); + + // The quick-note jot: Ctrl+Alt+N over the editor, Ctrl+Enter to save. The + // note attaches to the open scene and the panel picks it up. + await page.keyboard.press('ControlOrMeta+Alt+KeyN'); + const jot = page.locator('.quick-note'); + await expect(jot).toBeVisible(); + await expect(jot).toContainText('on this scene'); + await page.keyboard.type('Ledger follow-up'); + await page.keyboard.press('ControlOrMeta+Enter'); + await expect(page.locator('.qn-saved')).toContainText('Saved.'); + await expect(notesTab.locator('.seg-count')).toHaveText('2'); + await notesTab.click(); + await expect(page.locator('.panel-body:not([hidden])')).toContainText('Ledger follow-up'); }); diff --git a/e2e/public-page-preview.spec.ts b/e2e/public-page-preview.spec.ts new file mode 100644 index 0000000..1632ef7 --- /dev/null +++ b/e2e/public-page-preview.spec.ts @@ -0,0 +1,43 @@ +import { expect, test } from '@playwright/test'; +import { gotoReady } from './navigate'; + +// Sets the profile visibility toggle on the account page, working whether the +// form saves with a button or on change. +async function setVisibility(page: import('@playwright/test').Page, on: boolean) { + await gotoReady(page, '/account'); + const toggle = page.locator('input[name="profilePublic"]'); + if ((await toggle.isChecked()) === on) return; + // The input hides behind the toggle-track skin, so click its label. + await page.locator('label.toggle', { has: toggle }).click(); + await expect(toggle).toBeChecked({ checked: on }); + const save = page.getByRole('button', { name: 'Save public page' }); + if (await save.isVisible()) await save.click(); + await expect(page.getByRole('status')).toContainText('Saved'); +} + +// The owner can open their own page as a preview while it is still hidden; +// visitors get no such view. Ends with the profile public, a state no other +// spec depends on. +test('the owner previews their page before it is public', async ({ page, browser }) => { + await setVisibility(page, false); + + // The owner sees the full header plus the only-you note. + await gotoReady(page, '/@e2e-tester'); + await expect(page.locator('.preview-note')).toContainText('Only you can see this page'); + await expect(page.locator('.shelf-name')).not.toContainText('@'); + + // A visitor gets the page without the preview and without the header + // details the visibility toggle guards. + const anonymous = await browser.newContext({ storageState: { cookies: [], origins: [] } }); + const visitor = await anonymous.newPage(); + await gotoReady(visitor, '/@e2e-tester'); + await expect(visitor.locator('.shelf-handle')).toContainText('@e2e-tester'); + await expect(visitor.locator('.preview-note')).toHaveCount(0); + await expect(visitor.locator('.shelf-bio')).toHaveCount(0); + await anonymous.close(); + + // Turning visibility on removes the note for the owner too. + await setVisibility(page, true); + await gotoReady(page, '/@e2e-tester'); + await expect(page.locator('.preview-note')).toHaveCount(0); +}); diff --git a/e2e/review.spec.ts b/e2e/review.spec.ts index 6ff30b3..3cfaa12 100644 --- a/e2e/review.spec.ts +++ b/e2e/review.spec.ts @@ -60,11 +60,11 @@ test('guest review: invite, comment as a guest, reply and resolve as the author' await expect(guest.getByRole('button', { name: 'Account menu' })).toHaveCount(0); await expect(guest.locator('.review-prose')).toContainText('opinions about this gate'); - // A guest cannot leave review mode: the strip keeps all four modes, three + // A guest cannot leave review mode: the strip keeps all three modes, two // switched off in place, and one line under it says why. const modes = guest.locator('.mode-strip .seg-btn'); - await expect(modes).toHaveCount(4); - for (const mode of ['Write', 'Plan', 'Notes']) { + await expect(modes).toHaveCount(3); + for (const mode of ['Write', 'Plan']) { await expect(modes.filter({ hasText: mode })).toHaveAttribute('aria-disabled', 'true'); } await expect(guest.locator('.mode-note')).toContainText('belong to the author'); diff --git a/package-lock.json b/package-lock.json index 720b817..220b6b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codex", - "version": "3.16.0", + "version": "3.17.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codex", - "version": "3.16.0", + "version": "3.17.0", "dependencies": { "@aws-sdk/client-s3": "^3.1097.0", "@aws-sdk/lib-storage": "^3.1097.0", diff --git a/package.json b/package.json index 31c1e0b..dedaa81 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codex", "private": true, - "version": "3.16.0", + "version": "3.17.0", "type": "module", "scripts": { "dev": "vite dev", diff --git a/src/lib/autosave-form.ts b/src/lib/autosave-form.ts index ec6a192..1668671 100644 --- a/src/lib/autosave-form.ts +++ b/src/lib/autosave-form.ts @@ -13,8 +13,12 @@ export const autosaveSubmit: SubmitFunction = async ({ update }) => update({ reset: false }); -// Use as a form's onchange handler: any control change submits the form. +// Use as a form's onchange handler: any control change submits the form. A +// control marked data-no-autosave only changes the view (a filter box), so its +// changes do not save. export function autosubmitForm(event: Event & { currentTarget: HTMLFormElement }) { + const control = event.target as HTMLElement | null; + if (control?.dataset && 'noAutosave' in control.dataset) return; event.currentTarget.requestSubmit(); } diff --git a/src/lib/chrome.ts b/src/lib/chrome.ts index 5d37106..c03e80c 100644 --- a/src/lib/chrome.ts +++ b/src/lib/chrome.ts @@ -19,7 +19,12 @@ export const INSIGHTS_MODE_NOTE = // The line under the mode strip for an invited reviewer. export const GUEST_MODE_NOTE = - 'You were invited to review. Write, Plan and Notes belong to the author, so they are here but switched off.'; + 'You were invited to review. Write and Plan belong to the author, so they are here but switched off.'; + +// The line under the mode strip on the Notes pages, which sit beside the +// modes rather than being one. +export const NOTES_MODE_NOTE = + 'Notes sit beside every mode; the Notes panel on the right of Write and Plan brings you here. Pick a mode to go back to the work.'; export type CrumbMenuItem = { label: string; href: string; current?: boolean }; diff --git a/src/lib/components/CommandPalette.svelte b/src/lib/components/CommandPalette.svelte index 7022d35..ebf50bd 100644 --- a/src/lib/components/CommandPalette.svelte +++ b/src/lib/components/CommandPalette.svelte @@ -6,6 +6,7 @@ import { focusMode } from '$lib/focus-mode.svelte'; import { assistantIntent } from '$lib/assistant.svelte'; import { openReviewModal } from '$lib/review-modal.svelte'; + import { openQuickNote } from '$lib/quick-note.svelte'; import { startSummariesJob } from '$lib/assistant-actions'; import type { SearchResult } from '$lib/wire-types'; @@ -84,6 +85,15 @@ closePalette(); } }); + list.push({ + label: 'Quick note', + sublabel: 'Jot a note without leaving the editor', + kind: 'Command', + run: () => { + closePalette(); + openQuickNote(); + } + }); // The Assistant's quick actions, gated like its other surfaces; the // write page's data carries the gate and the open story. const assistant = page.data.assistant as @@ -168,6 +178,12 @@ kind: 'Go to', run: navigate(`/stories/${storyRef}/plan`) }, + { + label: 'Story notes', + sublabel: null, + kind: 'Go to', + run: navigate(`/stories/${storyRef}/notes`) + }, { label: 'Story settings', sublabel: null, @@ -191,6 +207,12 @@ kind: 'Go to', run: navigate(`/universes/${universeRef}/plan`) }, + { + label: 'Universe notes', + sublabel: null, + kind: 'Go to', + run: navigate(`/universes/${universeRef}/notes`) + }, { label: 'Universe insights', sublabel: null, diff --git a/src/lib/components/ImportStoryModal.svelte b/src/lib/components/ImportStoryModal.svelte new file mode 100644 index 0000000..dea7c77 --- /dev/null +++ b/src/lib/components/ImportStoryModal.svelte @@ -0,0 +1,354 @@ + + +{#if universe} +
+ {universe.standalone + ? 'The story comes in as a new standalone story.' + : `The story comes in as a new story in ${universe.name}.`} +
+{message}
+ {/if} + {#if busy} +{busyLabel}
+ {/if} + + {#if imported} ++ Imported {imported.sceneCount} + {imported.sceneCount === 1 ? 'scene' : 'scenes'}{imported.notesAttached > 0 + ? ` and ${imported.notesAttached} ${imported.notesAttached === 1 ? 'story note' : 'story notes'}` + : ''}{imported.entitiesCreated > 0 + ? `, creating ${imported.entitiesCreated} ${imported.entitiesCreated === 1 ? 'new entry' : 'new entries'}` + : ''}. +
+ {#each imported.problems as problem (problem)} +{problem}
+ {/each} ++ "{preview.storyTitle}": {preview.chapterCount} + {preview.chapterCount === 1 ? 'chapter' : 'chapters'}, + {preview.sceneCount} + {preview.sceneCount === 1 ? 'scene' : 'scenes'}, + {formatNumber(preview.words)} words{preview.assetCount > 0 + ? `, ${preview.assetCount} ${preview.assetCount === 1 ? 'image' : 'images'}` + : ''}. +
+ {#if preview.titleTaken} ++ A story named "{preview.storyTitle}" already exists here; this import creates a + second one. +
+ {/if} + {#if preview.assetCount > 0 && !preview.assetsConfigured} ++ Image storage is not configured, so the bundled images will not be imported. +
+ {/if} + {#if preview.notes.length > 0} +{problem}
+ {/each} +