From 43d397aa0b65adff8de318a6b48cb0c51361c368 Mon Sep 17 00:00:00 2001 From: Rajat Date: Sat, 7 Nov 2026 15:13:03 +0530 Subject: [PATCH 1/3] feat(changelog): add managed product updates - Replace GitHub changelog imports with managed product updates, RSS, and MCP support - Normalize existing changelog metadata and document the new workflow --- apps/api/drizzle/0015_changelog_cleanup.sql | 46 + apps/api/drizzle/meta/0015_snapshot.json | 10172 ++++++++++++++++ apps/api/drizzle/meta/_journal.json | 7 + .../auth/operation-policy.coverage.test.ts | 2 +- apps/api/src/auth/operation-policy.ts | 16 - .../db/changelog-cleanup-migration.test.ts | 96 + apps/api/src/db/schema.ts | 8 +- apps/api/src/documentation/chunking.test.ts | 36 +- apps/api/src/documentation/chunking.ts | 28 +- apps/api/src/documentation/queries.ts | 8 +- apps/api/src/documentation/worker.ts | 2 + apps/api/src/documents/changelogs-routes.ts | 212 +- .../product-updates-contract.test.ts | 59 + apps/api/src/documents/queries.test.ts | 92 + apps/api/src/documents/queries.ts | 69 +- apps/api/src/integrations/github.ts | 98 - apps/api/src/integrations/queries.ts | 46 - apps/api/src/mcp/policy.ts | 5 - apps/api/src/mcp/server.test.ts | 14 +- apps/api/src/mcp/server.ts | 2 - apps/api/src/mcp/tools/changelogs.ts | 177 - apps/api/src/public/routes.ts | 34 +- apps/sites/AGENTS.md | 2 +- .../(site)/changelog/rss.xml/route.test.ts | 53 + .../(site)/changelog/rss.xml/route.ts | 9 +- apps/sites/components/content-feed.tsx | 7 +- apps/sites/components/themed-content-feed.tsx | 109 +- apps/sites/lib/api.ts | 4 + apps/sites/lib/markdown.test.ts | 27 +- apps/sites/lib/markdown.ts | 19 +- apps/sites/lib/rss.ts | 7 +- apps/web/AGENTS.md | 2 +- .../content/editors/product-update-editor.tsx | 65 +- .../content/product-updates-view.tsx | 118 +- apps/web/lib/api.ts | 33 +- docs/architecture.md | 43 +- docs/changelog-management.md | 221 + packages/api-contract/src/content-paths.ts | 5 +- packages/api-contract/src/contract.ts | 50 +- .../src/schemas/product-updates.ts | 57 +- 40 files changed, 11112 insertions(+), 948 deletions(-) create mode 100644 apps/api/drizzle/0015_changelog_cleanup.sql create mode 100644 apps/api/drizzle/meta/0015_snapshot.json create mode 100644 apps/api/src/db/changelog-cleanup-migration.test.ts create mode 100644 apps/api/src/documents/product-updates-contract.test.ts delete mode 100644 apps/api/src/integrations/github.ts delete mode 100644 apps/api/src/mcp/tools/changelogs.ts create mode 100644 apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.test.ts create mode 100644 docs/changelog-management.md diff --git a/apps/api/drizzle/0015_changelog_cleanup.sql b/apps/api/drizzle/0015_changelog_cleanup.sql new file mode 100644 index 00000000..0b24f7b1 --- /dev/null +++ b/apps/api/drizzle/0015_changelog_cleanup.sql @@ -0,0 +1,46 @@ +-- Changelog management (docs/changelog-management.md). +-- 1. Remove the product-update GitHub import's stored personal access tokens. +-- Only `integrations` rows for the removed "github" provider are deleted; +-- the docs/pages GitHub App lives in documentation_git_* tables and the +-- SendLit key ("sendlit" rows) is untouched. +DELETE FROM "integrations" WHERE "provider" = 'github'; +--> statement-breakpoint +-- 2. Normalize changelog meta (published and draft) to the new contract: +-- labels limited to the Keep a Changelog types ("improved" -> "changed", +-- unknown labels such as "prerelease" dropped, duplicates removed), +-- releaseDate reduced to YYYY-MM-DD (GitHub imports stored a timestamp), +-- and the import-only "github" key removed. +CREATE OR REPLACE FUNCTION pg_temp.frontlit_changelog_meta(m jsonb) RETURNS jsonb +LANGUAGE sql IMMUTABLE AS $$ + SELECT (coalesce(m, '{}'::jsonb) - 'github' - 'labels' - 'releaseDate') + || jsonb_build_object( + 'labels', + coalesce( + ( + SELECT jsonb_agg(label ORDER BY first_seen) + FROM ( + SELECT + CASE WHEN value = 'improved' THEN 'changed' ELSE value END AS label, + min(ordinality) AS first_seen + FROM jsonb_array_elements_text( + CASE WHEN jsonb_typeof(m -> 'labels') = 'array' THEN m -> 'labels' ELSE '[]'::jsonb END + ) WITH ORDINALITY + GROUP BY 1 + ) mapped + WHERE label IN ('added', 'changed', 'deprecated', 'removed', 'fixed', 'security') + ), + '[]'::jsonb + ) + ) + || CASE + WHEN m ->> 'releaseDate' ~ '^\d{4}-\d{2}-\d{2}' + THEN jsonb_build_object('releaseDate', left(m ->> 'releaseDate', 10)) + ELSE '{}'::jsonb + END +$$; +--> statement-breakpoint +UPDATE "documents" +SET + "meta" = pg_temp.frontlit_changelog_meta("meta"), + "draft_meta" = pg_temp.frontlit_changelog_meta("draft_meta") +WHERE "kind" = 'changelog'; diff --git a/apps/api/drizzle/meta/0015_snapshot.json b/apps/api/drizzle/meta/0015_snapshot.json new file mode 100644 index 00000000..bf28331e --- /dev/null +++ b/apps/api/drizzle/meta/0015_snapshot.json @@ -0,0 +1,10172 @@ +{ + "id": "3bff1603-33f4-4a96-bf9e-6aab979134e3", + "prevId": "025c8492-cbcd-4fc8-a86b-c9881b4842ea", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "auth_user_id": { + "name": "auth_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_auth_user_id_auth_user_id_fk": { + "name": "accounts_auth_user_id_auth_user_id_fk", + "tableFrom": "accounts", + "columnsFrom": [ + "auth_user_id" + ], + "tableTo": "auth_user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "accounts_auth_user_id_unique": { + "name": "accounts_auth_user_id_unique", + "columns": [ + "auth_user_id" + ], + "nullsNotDistinct": false + }, + "accounts_email_unique": { + "name": "accounts_email_unique", + "columns": [ + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_settings": { + "name": "ai_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_settings": { + "name": "model_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "azure_endpoint": { + "name": "azure_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding_provider": { + "name": "embedding_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_embedding_api_key": { + "name": "encrypted_embedding_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding_deployment": { + "name": "embedding_deployment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding_endpoint": { + "name": "embedding_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "index_articles": { + "name": "index_articles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "index_changelog": { + "name": "index_changelog", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ai_settings_team_id_teams_id_fk": { + "name": "ai_settings_team_id_teams_id_fk", + "tableFrom": "ai_settings", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ai_settings_team_id_unique": { + "name": "ai_settings_team_id_unique", + "columns": [ + "team_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_usage_daily": { + "name": "ai_usage_daily", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_date": { + "name": "usage_date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reserved_tokens": { + "name": "reserved_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "used_tokens": { + "name": "used_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_usage_daily_team_date_idx": { + "name": "ai_usage_daily_team_date_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "ai_usage_daily_team_id_teams_id_fk": { + "name": "ai_usage_daily_team_id_teams_id_fk", + "tableFrom": "ai_usage_daily", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_keys_team_idx": { + "name": "api_keys_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "api_keys_team_id_teams_id_fk": { + "name": "api_keys_team_id_teams_id_fk", + "tableFrom": "api_keys", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "columns": [ + "key_hash" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_account": { + "name": "auth_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_account_user_id_idx": { + "name": "auth_account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "auth_account_user_id_auth_user_id_fk": { + "name": "auth_account_user_id_auth_user_id_fk", + "tableFrom": "auth_account", + "columnsFrom": [ + "user_id" + ], + "tableTo": "auth_user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_access_token": { + "name": "auth_oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_code_id": { + "name": "authorization_code_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmation": { + "name": "confirmation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_access_token_client_id_idx": { + "name": "auth_oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "auth_oauth_access_token_session_id_idx": { + "name": "auth_oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "auth_oauth_access_token_user_id_idx": { + "name": "auth_oauth_access_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "auth_oauth_access_token_authorization_code_id_idx": { + "name": "auth_oauth_access_token_authorization_code_id_idx", + "columns": [ + { + "expression": "authorization_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "auth_oauth_access_token_refresh_id_idx": { + "name": "auth_oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "auth_oauth_access_token_client_id_auth_oauth_client_client_id_fk": { + "name": "auth_oauth_access_token_client_id_auth_oauth_client_client_id_fk", + "tableFrom": "auth_oauth_access_token", + "columnsFrom": [ + "client_id" + ], + "tableTo": "auth_oauth_client", + "columnsTo": [ + "client_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "auth_oauth_access_token_session_id_auth_session_id_fk": { + "name": "auth_oauth_access_token_session_id_auth_session_id_fk", + "tableFrom": "auth_oauth_access_token", + "columnsFrom": [ + "session_id" + ], + "tableTo": "auth_session", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "auth_oauth_access_token_user_id_auth_user_id_fk": { + "name": "auth_oauth_access_token_user_id_auth_user_id_fk", + "tableFrom": "auth_oauth_access_token", + "columnsFrom": [ + "user_id" + ], + "tableTo": "auth_user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "auth_oauth_access_token_refresh_id_auth_oauth_refresh_token_id_fk": { + "name": "auth_oauth_access_token_refresh_id_auth_oauth_refresh_token_id_fk", + "tableFrom": "auth_oauth_access_token", + "columnsFrom": [ + "refresh_id" + ], + "tableTo": "auth_oauth_refresh_token", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_oauth_access_token_token_unique": { + "name": "auth_oauth_access_token_token_unique", + "columns": [ + "token" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_client": { + "name": "auth_oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_discovery_id": { + "name": "client_discovery_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "client_credentials_scopes": { + "name": "client_credentials_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_type": { + "name": "application_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "dpop_bound_access_tokens": { + "name": "dpop_bound_access_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_client_user_id_idx": { + "name": "auth_oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "auth_oauth_client_user_id_auth_user_id_fk": { + "name": "auth_oauth_client_user_id_auth_user_id_fk", + "tableFrom": "auth_oauth_client", + "columnsFrom": [ + "user_id" + ], + "tableTo": "auth_user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_oauth_client_client_id_unique": { + "name": "auth_oauth_client_client_id_unique", + "columns": [ + "client_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_client_assertion": { + "name": "auth_oauth_client_assertion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_client_resource": { + "name": "auth_oauth_client_resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_client_resource_client_id_idx": { + "name": "auth_oauth_client_resource_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "auth_oauth_client_resource_resource_id_idx": { + "name": "auth_oauth_client_resource_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "auth_oauth_client_resource_client_id_resource_id_idx": { + "name": "auth_oauth_client_resource_client_id_resource_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "auth_oauth_client_resource_client_id_auth_oauth_client_client_id_fk": { + "name": "auth_oauth_client_resource_client_id_auth_oauth_client_client_id_fk", + "tableFrom": "auth_oauth_client_resource", + "columnsFrom": [ + "client_id" + ], + "tableTo": "auth_oauth_client", + "columnsTo": [ + "client_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "auth_oauth_client_resource_resource_id_auth_oauth_resource_identifier_fk": { + "name": "auth_oauth_client_resource_resource_id_auth_oauth_resource_identifier_fk", + "tableFrom": "auth_oauth_client_resource", + "columnsFrom": [ + "resource_id" + ], + "tableTo": "auth_oauth_resource", + "columnsTo": [ + "identifier" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_consent": { + "name": "auth_oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_oauth_consent_client_id_idx": { + "name": "auth_oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "auth_oauth_consent_user_id_idx": { + "name": "auth_oauth_consent_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "auth_oauth_consent_client_id_auth_oauth_client_client_id_fk": { + "name": "auth_oauth_consent_client_id_auth_oauth_client_client_id_fk", + "tableFrom": "auth_oauth_consent", + "columnsFrom": [ + "client_id" + ], + "tableTo": "auth_oauth_client", + "columnsTo": [ + "client_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "auth_oauth_consent_user_id_auth_user_id_fk": { + "name": "auth_oauth_consent_user_id_auth_user_id_fk", + "tableFrom": "auth_oauth_consent", + "columnsFrom": [ + "user_id" + ], + "tableTo": "auth_user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_refresh_token": { + "name": "auth_oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_code_id": { + "name": "authorization_code_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotation_replay_response": { + "name": "rotation_replay_response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rotation_replay_expires_at": { + "name": "rotation_replay_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmation": { + "name": "confirmation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_oauth_refresh_token_client_id_idx": { + "name": "auth_oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "auth_oauth_refresh_token_authorization_code_id_idx": { + "name": "auth_oauth_refresh_token_authorization_code_id_idx", + "columns": [ + { + "expression": "authorization_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "auth_oauth_refresh_token_session_id_idx": { + "name": "auth_oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "auth_oauth_refresh_token_user_id_idx": { + "name": "auth_oauth_refresh_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "auth_oauth_refresh_token_client_id_auth_oauth_client_client_id_fk": { + "name": "auth_oauth_refresh_token_client_id_auth_oauth_client_client_id_fk", + "tableFrom": "auth_oauth_refresh_token", + "columnsFrom": [ + "client_id" + ], + "tableTo": "auth_oauth_client", + "columnsTo": [ + "client_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "auth_oauth_refresh_token_session_id_auth_session_id_fk": { + "name": "auth_oauth_refresh_token_session_id_auth_session_id_fk", + "tableFrom": "auth_oauth_refresh_token", + "columnsFrom": [ + "session_id" + ], + "tableTo": "auth_session", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "auth_oauth_refresh_token_user_id_auth_user_id_fk": { + "name": "auth_oauth_refresh_token_user_id_auth_user_id_fk", + "tableFrom": "auth_oauth_refresh_token", + "columnsFrom": [ + "user_id" + ], + "tableTo": "auth_user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_oauth_refresh_token_token_unique": { + "name": "auth_oauth_refresh_token_token_unique", + "columns": [ + "token" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_resource": { + "name": "auth_oauth_resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_ttl": { + "name": "access_token_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refresh_token_ttl": { + "name": "refresh_token_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signing_algorithm": { + "name": "signing_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signing_key_id": { + "name": "signing_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_scopes": { + "name": "allowed_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "custom_claims": { + "name": "custom_claims", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dpop_bound_access_tokens_required": { + "name": "dpop_bound_access_tokens_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "policy_version": { + "name": "policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_oauth_resource_identifier_unique": { + "name": "auth_oauth_resource_identifier_unique", + "columns": [ + "identifier" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_session": { + "name": "auth_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reauthenticated_at": { + "name": "reauthenticated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_session_user_id_idx": { + "name": "auth_session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "auth_session_user_id_auth_user_id_fk": { + "name": "auth_session_user_id_auth_user_id_fk", + "tableFrom": "auth_session", + "columnsFrom": [ + "user_id" + ], + "tableTo": "auth_user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_session_token_unique": { + "name": "auth_session_token_unique", + "columns": [ + "token" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_user": { + "name": "auth_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "columns": [ + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_verification": { + "name": "auth_verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.authorization_audit_events": { + "name": "authorization_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_public_id": { + "name": "team_public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_member_id": { + "name": "target_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "before_permissions": { + "name": "before_permissions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "after_permissions": { + "name": "after_permissions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "authorization_audit_team_created_idx": { + "name": "authorization_audit_team_created_idx", + "columns": [ + { + "expression": "team_public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "authorization_audit_expires_idx": { + "name": "authorization_audit_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "authorization_audit_events_actor_account_id_accounts_id_fk": { + "name": "authorization_audit_events_actor_account_id_accounts_id_fk", + "tableFrom": "authorization_audit_events", + "columnsFrom": [ + "actor_account_id" + ], + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chatbot_settings": { + "name": "chatbot_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Assistant'" + }, + "greeting": { + "name": "greeting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "escalation_enabled": { + "name": "escalation_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'bubble'" + }, + "widget_style": { + "name": "widget_style", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "chatbot_settings_team_id_teams_id_fk": { + "name": "chatbot_settings_team_id_teams_id_fk", + "tableFrom": "chatbot_settings", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chatbot_settings_team_id_unique": { + "name": "chatbot_settings_team_id_unique", + "columns": [ + "team_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_domains": { + "name": "custom_domains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary": { + "name": "primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "routing_satisfied": { + "name": "routing_satisfied", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_domains_team_idx": { + "name": "custom_domains_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "custom_domains_team_id_teams_id_fk": { + "name": "custom_domains_team_id_teams_id_fk", + "tableFrom": "custom_domains", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "custom_domains_hostname_unique": { + "name": "custom_domains_hostname_unique", + "columns": [ + "hostname" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "custom_domains_verified_at_check": { + "name": "custom_domains_verified_at_check", + "value": "(\"status\" <> 'verified' OR \"verified_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.doc_sections": { + "name": "doc_sections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_sections_team_slug_idx": { + "name": "doc_sections_team_slug_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "doc_sections_team_idx": { + "name": "doc_sections_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "doc_sections_team_id_teams_id_fk": { + "name": "doc_sections_team_id_teams_id_fk", + "tableFrom": "doc_sections", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "doc_sections_section_id_unique": { + "name": "doc_sections_section_id_unique", + "columns": [ + "section_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "section_id_prefix_check": { + "name": "section_id_prefix_check", + "value": "\"section_id\" LIKE 'sec\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.document_chunks": { + "name": "document_chunks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heading": { + "name": "heading", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "anchor": { + "name": "anchor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_chunks_team_idx": { + "name": "document_chunks_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "document_chunks_embedding_idx": { + "name": "document_chunks_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "hnsw", + "concurrently": false + } + }, + "foreignKeys": { + "document_chunks_team_id_teams_id_fk": { + "name": "document_chunks_team_id_teams_id_fk", + "tableFrom": "document_chunks", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_chunks_document_id_documents_id_fk": { + "name": "document_chunks_document_id_documents_id_fk", + "tableFrom": "document_chunks", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_index_jobs": { + "name": "document_index_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "document_index_jobs_document_idx": { + "name": "document_index_jobs_document_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "document_index_jobs_poll_idx": { + "name": "document_index_jobs_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "document_index_jobs_team_id_teams_id_fk": { + "name": "document_index_jobs_team_id_teams_id_fk", + "tableFrom": "document_index_jobs", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_index_jobs_document_id_documents_id_fk": { + "name": "document_index_jobs_document_id_documents_id_fk", + "tableFrom": "document_index_jobs", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_api_operations": { + "name": "documentation_api_operations", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group": { + "name": "group", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deprecated": { + "name": "deprecated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "slice": { + "name": "slice", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunks": { + "name": "chunks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "documentation_api_operations_route_idx": { + "name": "documentation_api_operations_route_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "route", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_api_operations_team_idx": { + "name": "documentation_api_operations_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_api_operations_spec_id_documentation_api_specs_spec_id_fk": { + "name": "documentation_api_operations_spec_id_documentation_api_specs_spec_id_fk", + "tableFrom": "documentation_api_operations", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "documentation_api_specs", + "columnsTo": [ + "spec_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_api_operations_team_id_teams_id_fk": { + "name": "documentation_api_operations_team_id_teams_id_fk", + "tableFrom": "documentation_api_operations", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_api_specs": { + "name": "documentation_api_specs", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_blob_sha": { + "name": "source_blob_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mount": { + "name": "mount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "info": { + "name": "info", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "diagnostics": { + "name": "diagnostics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_api_specs_team_idx": { + "name": "documentation_api_specs_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_api_specs_team_id_teams_id_fk": { + "name": "documentation_api_specs_team_id_teams_id_fk", + "tableFrom": "documentation_api_specs", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_delivery_outbox": { + "name": "documentation_delivery_outbox", + "schema": "", + "columns": { + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publication_id": { + "name": "publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy_epoch": { + "name": "policy_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "destinations": { + "name": "destinations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"local\"]'::jsonb" + }, + "acknowledgments": { + "name": "acknowledgments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "documentation_delivery_outbox_team_id_teams_id_fk": { + "name": "documentation_delivery_outbox_team_id_teams_id_fk", + "tableFrom": "documentation_delivery_outbox", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_embedding_configs": { + "name": "documentation_embedding_configs", + "schema": "", + "columns": { + "config_hash": { + "name": "config_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment": { + "name": "deployment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "documentation_embedding_configs_team_id_teams_id_fk": { + "name": "documentation_embedding_configs_team_id_teams_id_fk", + "tableFrom": "documentation_embedding_configs", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_embeddings": { + "name": "documentation_embeddings", + "schema": "", + "columns": { + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_hash": { + "name": "config_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vectors": { + "name": "vectors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_embeddings_revision_config_idx": { + "name": "documentation_embeddings_revision_config_idx", + "columns": [ + { + "expression": "revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_embeddings_revision_id_documentation_revisions_revision_id_fk": { + "name": "documentation_embeddings_revision_id_documentation_revisions_revision_id_fk", + "tableFrom": "documentation_embeddings", + "columnsFrom": [ + "revision_id" + ], + "tableTo": "documentation_revisions", + "columnsTo": [ + "revision_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_evidence": { + "name": "documentation_evidence", + "schema": "", + "columns": { + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retrieved": { + "name": "retrieved", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "documentation_evidence_dedupe_idx": { + "name": "documentation_evidence_dedupe_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_evidence_team_id_teams_id_fk": { + "name": "documentation_evidence_team_id_teams_id_fk", + "tableFrom": "documentation_evidence", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_evidence_item_id_documentation_improvements_item_id_fk": { + "name": "documentation_evidence_item_id_documentation_improvements_item_id_fk", + "tableFrom": "documentation_evidence", + "columnsFrom": [ + "item_id" + ], + "tableTo": "documentation_improvements", + "columnsTo": [ + "item_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_evidence_ticket_id_tickets_id_fk": { + "name": "documentation_evidence_ticket_id_tickets_id_fk", + "tableFrom": "documentation_evidence", + "columnsFrom": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_evidence_message_id_ticket_messages_id_fk": { + "name": "documentation_evidence_message_id_ticket_messages_id_fk", + "tableFrom": "documentation_evidence", + "columnsFrom": [ + "message_id" + ], + "tableTo": "ticket_messages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_bindings": { + "name": "documentation_git_bindings", + "schema": "", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_login": { + "name": "owner_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_name": { + "name": "repository_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_private": { + "name": "repository_private", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory": { + "name": "directory", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "docs_enabled": { + "name": "docs_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "pages_directory": { + "name": "pages_directory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pages_source_key": { + "name": "pages_source_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docs_source_key": { + "name": "docs_source_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pages_diagnostics": { + "name": "pages_diagnostics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "api_spec_path": { + "name": "api_spec_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_mount": { + "name": "api_mount", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'api-reference'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "requested_generation": { + "name": "requested_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "grant_version": { + "name": "grant_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "grant_actor_account_id": { + "name": "grant_actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_github_user_id": { + "name": "grant_github_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_admin_verified": { + "name": "repository_admin_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "observed_sha": { + "name": "observed_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "validated_sha": { + "name": "validated_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "live_sha": { + "name": "live_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_diagnostic": { + "name": "last_diagnostic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_receipt": { + "name": "connection_receipt", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_reconcile_at": { + "name": "next_reconcile_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_pr_reconcile_at": { + "name": "next_pr_reconcile_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_pr_reconcile_error": { + "name": "last_pr_reconcile_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_git_bindings_installation_idx": { + "name": "documentation_git_bindings_installation_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_git_bindings_reconcile_idx": { + "name": "documentation_git_bindings_reconcile_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_reconcile_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_git_bindings_pr_reconcile_idx": { + "name": "documentation_git_bindings_pr_reconcile_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_pr_reconcile_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_git_bindings_team_id_teams_id_fk": { + "name": "documentation_git_bindings_team_id_teams_id_fk", + "tableFrom": "documentation_git_bindings", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_checks": { + "name": "documentation_git_checks", + "schema": "", + "columns": { + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_generation": { + "name": "connection_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pull_request_number": { + "name": "pull_request_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_run_id": { + "name": "sync_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publication_result": { + "name": "publication_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "check_run_id": { + "name": "check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_phase": { + "name": "error_phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_checks_poll_idx": { + "name": "documentation_git_checks_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_git_checks_sync_run_idx": { + "name": "documentation_git_checks_sync_run_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sync_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_git_checks_active_pr_idx": { + "name": "documentation_git_checks_active_pr_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pull_request_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"documentation_git_checks\".\"status\" in ('queued', 'validating', 'reporting')", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_git_checks_team_id_teams_id_fk": { + "name": "documentation_git_checks_team_id_teams_id_fk", + "tableFrom": "documentation_git_checks", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_deliveries": { + "name": "documentation_git_deliveries", + "schema": "", + "columns": { + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ignored'" + }, + "run_ids": { + "name": "run_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_files": { + "name": "documentation_git_files", + "schema": "", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob_sha": { + "name": "blob_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "frontmatter": { + "name": "frontmatter", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_files_path_idx": { + "name": "documentation_git_files_path_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_git_files_document_idx": { + "name": "documentation_git_files_document_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"documentation_git_files\".\"removed_at\" is null", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_git_files_team_id_teams_id_fk": { + "name": "documentation_git_files_team_id_teams_id_fk", + "tableFrom": "documentation_git_files", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_grants": { + "name": "documentation_git_grants", + "schema": "", + "columns": { + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory": { + "name": "directory", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_version": { + "name": "grant_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "connection_generation": { + "name": "connection_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "repository_admin_verified": { + "name": "repository_admin_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_git_grants_idempotency_idx": { + "name": "documentation_git_grants_idempotency_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_git_grants_version_idx": { + "name": "documentation_git_grants_version_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "grant_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_git_grants_team_id_teams_id_fk": { + "name": "documentation_git_grants_team_id_teams_id_fk", + "tableFrom": "documentation_git_grants", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_jobs": { + "name": "documentation_git_jobs", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_generation": { + "name": "connection_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "request_generation": { + "name": "request_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reconcile'" + }, + "reviewed_sha": { + "name": "reviewed_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "observed_sha": { + "name": "observed_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publication_id": { + "name": "publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "diagnostic": { + "name": "diagnostic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "diagnostic_report": { + "name": "diagnostic_report", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_jobs_generation_idx": { + "name": "documentation_git_jobs_generation_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_git_jobs_poll_idx": { + "name": "documentation_git_jobs_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_git_jobs_team_id_teams_id_fk": { + "name": "documentation_git_jobs_team_id_teams_id_fk", + "tableFrom": "documentation_git_jobs", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_migration_prs": { + "name": "documentation_git_migration_prs", + "schema": "", + "columns": { + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setup_id": { + "name": "setup_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_credential": { + "name": "actor_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy_epoch": { + "name": "policy_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "purge_generation": { + "name": "purge_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "initialize_empty_repository": { + "name": "initialize_empty_repository", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tree_sha": { + "name": "tree_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bootstrap_sha": { + "name": "bootstrap_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pull_request_number": { + "name": "pull_request_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pull_request_url": { + "name": "pull_request_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_commit_sha": { + "name": "merged_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "diagnostic": { + "name": "diagnostic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_migration_prs_idempotency_idx": { + "name": "documentation_git_migration_prs_idempotency_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_git_migration_prs_work_idx": { + "name": "documentation_git_migration_prs_work_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_git_migration_prs_plan_id_documentation_git_migrations_plan_id_fk": { + "name": "documentation_git_migration_prs_plan_id_documentation_git_migrations_plan_id_fk", + "tableFrom": "documentation_git_migration_prs", + "columnsFrom": [ + "plan_id" + ], + "tableTo": "documentation_git_migrations", + "columnsTo": [ + "plan_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_git_migration_prs_team_id_teams_id_fk": { + "name": "documentation_git_migration_prs_team_id_teams_id_fk", + "tableFrom": "documentation_git_migration_prs", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_git_migration_prs_setup_id_documentation_git_setups_setup_id_fk": { + "name": "documentation_git_migration_prs_setup_id_documentation_git_setups_setup_id_fk", + "tableFrom": "documentation_git_migration_prs", + "columnsFrom": [ + "setup_id" + ], + "tableTo": "documentation_git_setups", + "columnsTo": [ + "setup_id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_migrations": { + "name": "documentation_git_migrations", + "schema": "", + "columns": { + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setup_id": { + "name": "setup_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preflight_id": { + "name": "preflight_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docs_draft_version": { + "name": "docs_draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "active_publication_id": { + "name": "active_publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_hash": { + "name": "plan_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selection": { + "name": "selection", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "verified_sha": { + "name": "verified_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_migrations_team_idx": { + "name": "documentation_git_migrations_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_git_migrations_team_id_teams_id_fk": { + "name": "documentation_git_migrations_team_id_teams_id_fk", + "tableFrom": "documentation_git_migrations", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_git_migrations_setup_id_documentation_git_setups_setup_id_fk": { + "name": "documentation_git_migrations_setup_id_documentation_git_setups_setup_id_fk", + "tableFrom": "documentation_git_migrations", + "columnsFrom": [ + "setup_id" + ], + "tableTo": "documentation_git_setups", + "columnsTo": [ + "setup_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_git_migrations_preflight_id_documentation_git_preflights_preflight_id_fk": { + "name": "documentation_git_migrations_preflight_id_documentation_git_preflights_preflight_id_fk", + "tableFrom": "documentation_git_migrations", + "columnsFrom": [ + "preflight_id" + ], + "tableTo": "documentation_git_preflights", + "columnsTo": [ + "preflight_id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_preflights": { + "name": "documentation_git_preflights", + "schema": "", + "columns": { + "preflight_id": { + "name": "preflight_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setup_id": { + "name": "setup_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_login": { + "name": "owner_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_name": { + "name": "repository_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_private": { + "name": "repository_private", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory": { + "name": "directory", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docs_draft_version": { + "name": "docs_draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "manifest_hash": { + "name": "manifest_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_git_preflights_team_idx": { + "name": "documentation_git_preflights_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_git_preflights_team_id_teams_id_fk": { + "name": "documentation_git_preflights_team_id_teams_id_fk", + "tableFrom": "documentation_git_preflights", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_git_preflights_setup_id_documentation_git_setups_setup_id_fk": { + "name": "documentation_git_preflights_setup_id_documentation_git_setups_setup_id_fk", + "tableFrom": "documentation_git_preflights", + "columnsFrom": [ + "setup_id" + ], + "tableTo": "documentation_git_setups", + "columnsTo": [ + "setup_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_proposal_documents": { + "name": "documentation_git_proposal_documents", + "schema": "", + "columns": { + "proposal_id": { + "name": "proposal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_path": { + "name": "target_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "open": { + "name": "open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "documentation_git_proposal_documents_identity_idx": { + "name": "documentation_git_proposal_documents_identity_idx", + "columns": [ + { + "expression": "proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_git_proposal_documents_open_idx": { + "name": "documentation_git_proposal_documents_open_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"documentation_git_proposal_documents\".\"open\" = true", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_git_proposal_documents_proposal_id_documentation_git_proposals_proposal_id_fk": { + "name": "documentation_git_proposal_documents_proposal_id_documentation_git_proposals_proposal_id_fk", + "tableFrom": "documentation_git_proposal_documents", + "columnsFrom": [ + "proposal_id" + ], + "tableTo": "documentation_git_proposals", + "columnsTo": [ + "proposal_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_git_proposal_documents_team_id_teams_id_fk": { + "name": "documentation_git_proposal_documents_team_id_teams_id_fk", + "tableFrom": "documentation_git_proposal_documents", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_proposals": { + "name": "documentation_git_proposals", + "schema": "", + "columns": { + "proposal_id": { + "name": "proposal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_credential": { + "name": "actor_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source_context": { + "name": "source_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "connection_generation": { + "name": "connection_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_version": { + "name": "grant_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updates_proposal_id": { + "name": "updates_proposal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changes": { + "name": "changes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tree_sha": { + "name": "tree_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_commit_sha": { + "name": "merged_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pull_request_number": { + "name": "pull_request_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pull_request_url": { + "name": "pull_request_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "diagnostics": { + "name": "diagnostics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_proposals_idempotency_idx": { + "name": "documentation_git_proposals_idempotency_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_git_proposals_poll_idx": { + "name": "documentation_git_proposals_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_git_proposals_team_id_teams_id_fk": { + "name": "documentation_git_proposals_team_id_teams_id_fk", + "tableFrom": "documentation_git_proposals", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_setups": { + "name": "documentation_git_setups", + "schema": "", + "columns": { + "setup_id": { + "name": "setup_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_verifier": { + "name": "encrypted_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_user_token": { + "name": "encrypted_user_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "return_path": { + "name": "return_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_git_setups_expiry_idx": { + "name": "documentation_git_setups_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_git_setups_team_id_teams_id_fk": { + "name": "documentation_git_setups_team_id_teams_id_fk", + "tableFrom": "documentation_git_setups", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_git_setups_actor_account_id_accounts_id_fk": { + "name": "documentation_git_setups_actor_account_id_accounts_id_fk", + "tableFrom": "documentation_git_setups", + "columnsFrom": [ + "actor_account_id" + ], + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "documentation_git_setups_state_hash_unique": { + "name": "documentation_git_setups_state_hash_unique", + "columns": [ + "state_hash" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_improvement_drafts": { + "name": "documentation_improvement_drafts", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_version": { + "name": "approved_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_version": { + "name": "item_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "documentation_improvement_drafts_version_idx": { + "name": "documentation_improvement_drafts_version_idx", + "columns": [ + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "approved_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_improvement_drafts_item_id_documentation_improvements_item_id_fk": { + "name": "documentation_improvement_drafts_item_id_documentation_improvements_item_id_fk", + "tableFrom": "documentation_improvement_drafts", + "columnsFrom": [ + "item_id" + ], + "tableTo": "documentation_improvements", + "columnsTo": [ + "item_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_improvement_reviews": { + "name": "documentation_improvement_reviews", + "schema": "", + "columns": { + "review_id": { + "name": "review_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_version": { + "name": "item_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_improvement_reviews_item_idx": { + "name": "documentation_improvement_reviews_item_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_improvement_reviews_team_id_teams_id_fk": { + "name": "documentation_improvement_reviews_team_id_teams_id_fk", + "tableFrom": "documentation_improvement_reviews", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_improvement_reviews_item_id_documentation_improvements_item_id_fk": { + "name": "documentation_improvement_reviews_item_id_documentation_improvements_item_id_fk", + "tableFrom": "documentation_improvement_reviews", + "columnsFrom": [ + "item_id" + ], + "tableTo": "documentation_improvements", + "columnsTo": [ + "item_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_improvements": { + "name": "documentation_improvements", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_document_id": { + "name": "target_document_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "predecessor_id": { + "name": "predecessor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_into_id": { + "name": "merged_into_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved": { + "name": "approved", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "draft_document_id": { + "name": "draft_document_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "draft_hash": { + "name": "draft_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publication_id": { + "name": "publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_improvements_team_idx": { + "name": "documentation_improvements_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_improvements_team_id_teams_id_fk": { + "name": "documentation_improvements_team_id_teams_id_fk", + "tableFrom": "documentation_improvements", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_memberships": { + "name": "documentation_memberships", + "schema": "", + "columns": { + "publication_id": { + "name": "publication_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "documentation_memberships_route_idx": { + "name": "documentation_memberships_route_idx", + "columns": [ + { + "expression": "publication_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "route", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_memberships_document_idx": { + "name": "documentation_memberships_document_idx", + "columns": [ + { + "expression": "publication_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_memberships_publication_id_documentation_publications_publication_id_fk": { + "name": "documentation_memberships_publication_id_documentation_publications_publication_id_fk", + "tableFrom": "documentation_memberships", + "columnsFrom": [ + "publication_id" + ], + "tableTo": "documentation_publications", + "columnsTo": [ + "publication_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documentation_memberships_revision_id_documentation_revisions_revision_id_fk": { + "name": "documentation_memberships_revision_id_documentation_revisions_revision_id_fk", + "tableFrom": "documentation_memberships", + "columnsFrom": [ + "revision_id" + ], + "tableTo": "documentation_revisions", + "columnsTo": [ + "revision_id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_publications": { + "name": "documentation_publications", + "schema": "", + "columns": { + "publication_id": { + "name": "publication_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_publication_id": { + "name": "base_publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admission_type": { + "name": "admission_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'explicit'" + }, + "git_context": { + "name": "git_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "purge_generation": { + "name": "purge_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "docs_draft_version": { + "name": "docs_draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "indexing": { + "name": "indexing", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "navigation": { + "name": "navigation", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "diagnostics": { + "name": "diagnostics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_publications_idempotency_idx": { + "name": "documentation_publications_idempotency_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documentation_publications_poll_idx": { + "name": "documentation_publications_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_publications_team_id_teams_id_fk": { + "name": "documentation_publications_team_id_teams_id_fk", + "tableFrom": "documentation_publications", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_purges": { + "name": "documentation_purges", + "schema": "", + "columns": { + "purge_id": { + "name": "purge_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_hash": { + "name": "plan_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preflight'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_purges_idempotency_idx": { + "name": "documentation_purges_idempotency_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_purges_team_id_teams_id_fk": { + "name": "documentation_purges_team_id_teams_id_fk", + "tableFrom": "documentation_purges", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_revisions": { + "name": "documentation_revisions", + "schema": "", + "columns": { + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'docs-v1'" + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headings": { + "name": "headings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "chunks": { + "name": "chunks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_revisions_team_idx": { + "name": "documentation_revisions_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documentation_revisions_team_id_teams_id_fk": { + "name": "documentation_revisions_team_id_teams_id_fk", + "tableFrom": "documentation_revisions", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_settings": { + "name": "documentation_settings", + "schema": "", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'editor'" + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "active_publication_id": { + "name": "active_publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_api_spec_id": { + "name": "active_api_spec_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_generation": { + "name": "requested_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "policy_epoch": { + "name": "policy_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "purge_generation": { + "name": "purge_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexing": { + "name": "indexing", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"enabled\":true}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "documentation_settings_team_id_teams_id_fk": { + "name": "documentation_settings_team_id_teams_id_fk", + "tableFrom": "documentation_settings", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excerpt": { + "name": "excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "featured_image": { + "name": "featured_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "draft_slug": { + "name": "draft_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_markdown": { + "name": "draft_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_hash": { + "name": "draft_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_git_source": { + "name": "draft_git_source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "draft_title": { + "name": "draft_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_content": { + "name": "draft_content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "draft_excerpt": { + "name": "draft_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_featured_image": { + "name": "draft_featured_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "draft_meta": { + "name": "draft_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "author_account_id": { + "name": "author_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documents_team_kind_slug_idx": { + "name": "documents_team_kind_slug_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"documents\".\"kind\" <> 'doc'", + "concurrently": false + }, + "documents_team_kind_idx": { + "name": "documents_team_kind_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documents_published_idx": { + "name": "documents_published_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documents_team_id_teams_id_fk": { + "name": "documents_team_id_teams_id_fk", + "tableFrom": "documents", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documents_author_account_id_accounts_id_fk": { + "name": "documents_author_account_id_accounts_id_fk", + "tableFrom": "documents", + "columnsFrom": [ + "author_account_id" + ], + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "documents_document_id_unique": { + "name": "documents_document_id_unique", + "columns": [ + "document_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "document_id_prefix_check": { + "name": "document_id_prefix_check", + "value": "\"document_id\" LIKE 'doc\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.email_reply_tokens": { + "name": "email_reply_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_email": { + "name": "requester_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_reply_tokens_ticket_requester_idx": { + "name": "email_reply_tokens_ticket_requester_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "email_reply_tokens_team_id_teams_id_fk": { + "name": "email_reply_tokens_team_id_teams_id_fk", + "tableFrom": "email_reply_tokens", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "email_reply_tokens_ticket_id_tickets_id_fk": { + "name": "email_reply_tokens_ticket_id_tickets_id_fk", + "tableFrom": "email_reply_tokens", + "columnsFrom": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_reply_tokens_token_unique": { + "name": "email_reply_tokens_token_unique", + "columns": [ + "token" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_assets": { + "name": "git_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob_sha": { + "name": "blob_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "git_assets_team_blob_idx": { + "name": "git_assets_team_blob_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blob_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "git_assets_team_id_teams_id_fk": { + "name": "git_assets_team_id_teams_id_fk", + "tableFrom": "git_assets", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbound_emails": { + "name": "inbound_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inbound_emails_team_id_teams_id_fk": { + "name": "inbound_emails_team_id_teams_id_fk", + "tableFrom": "inbound_emails", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "inbound_emails_ticket_id_tickets_id_fk": { + "name": "inbound_emails_ticket_id_tickets_id_fk", + "tableFrom": "inbound_emails", + "columnsFrom": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "inbound_emails_message_id_unique": { + "name": "inbound_emails_message_id_unique", + "columns": [ + "message_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_ai_runs": { + "name": "inbox_ai_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_message_id": { + "name": "trigger_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stop_reason": { + "name": "stop_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retrieval": { + "name": "retrieval", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_ai_runs_ticket_trigger_idx": { + "name": "inbox_ai_runs_ticket_trigger_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "inbox_ai_runs_poll_idx": { + "name": "inbox_ai_runs_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "inbox_ai_runs_team_id_teams_id_fk": { + "name": "inbox_ai_runs_team_id_teams_id_fk", + "tableFrom": "inbox_ai_runs", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "inbox_ai_runs_ticket_id_tickets_id_fk": { + "name": "inbox_ai_runs_ticket_id_tickets_id_fk", + "tableFrom": "inbox_ai_runs", + "columnsFrom": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "inbox_ai_runs_trigger_message_id_ticket_messages_id_fk": { + "name": "inbox_ai_runs_trigger_message_id_ticket_messages_id_fk", + "tableFrom": "inbox_ai_runs", + "columnsFrom": [ + "trigger_message_id" + ], + "tableTo": "ticket_messages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_ai_settings": { + "name": "inbox_ai_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auto_reply": { + "name": "auto_reply", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "escalate_on_visitor_request": { + "name": "escalate_on_visitor_request", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "escalate_when_unsure": { + "name": "escalate_when_unsure", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "escalate_topics": { + "name": "escalate_topics", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "escalate_after_bot_turns": { + "name": "escalate_after_bot_turns", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_ai_settings_team_channel_idx": { + "name": "inbox_ai_settings_team_channel_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "inbox_ai_settings_team_id_teams_id_fk": { + "name": "inbox_ai_settings_team_id_teams_id_fk", + "tableFrom": "inbox_ai_settings", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrations": { + "name": "integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integrations_team_provider_idx": { + "name": "integrations_team_provider_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "integrations_team_id_teams_id_fk": { + "name": "integrations_team_id_teams_id_fk", + "tableFrom": "integrations", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_jwks": { + "name": "auth_jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "crv": { + "name": "crv", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_lit_id": { + "name": "media_lit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_team_idx": { + "name": "media_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "media_medialit_idx": { + "name": "media_medialit_idx", + "columns": [ + { + "expression": "media_lit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "media_team_id_teams_id_fk": { + "name": "media_team_id_teams_id_fk", + "tableFrom": "media", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "media_media_id_unique": { + "name": "media_media_id_unique", + "columns": [ + "media_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "media_id_prefix_check": { + "name": "media_id_prefix_check", + "value": "\"media_id\" LIKE 'med\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.outbox_jobs": { + "name": "outbox_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "outbox_jobs_poll_idx": { + "name": "outbox_jobs_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "outbox_jobs_team_id_teams_id_fk": { + "name": "outbox_jobs_team_id_teams_id_fk", + "tableFrom": "outbox_jobs", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_pull_requests": { + "name": "page_pull_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "directory": { + "name": "directory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "page_pull_requests_team_idx": { + "name": "page_pull_requests_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "page_pull_requests_team_id_teams_id_fk": { + "name": "page_pull_requests_team_id_teams_id_fk", + "tableFrom": "page_pull_requests", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "page_pull_requests_page_id_pages_id_fk": { + "name": "page_pull_requests_page_id_pages_id_fk", + "tableFrom": "page_pull_requests", + "columnsFrom": [ + "page_id" + ], + "tableTo": "pages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_revisions": { + "name": "page_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "page_revisions_team_hash_idx": { + "name": "page_revisions_team_hash_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "page_revisions_team_id_teams_id_fk": { + "name": "page_revisions_team_id_teams_id_fk", + "tableFrom": "page_revisions", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pages": { + "name": "pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deletable": { + "name": "deletable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "draft_layout": { + "name": "draft_layout", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "draft_title": { + "name": "draft_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_description": { + "name": "draft_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_social_image": { + "name": "draft_social_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "draft_robots_allowed": { + "name": "draft_robots_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pages_team_idx": { + "name": "pages_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pages_team_slug_idx": { + "name": "pages_team_slug_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pages_team_id_teams_id_fk": { + "name": "pages_team_id_teams_id_fk", + "tableFrom": "pages", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pages_page_id_unique": { + "name": "pages_page_id_unique", + "columns": [ + "page_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "page_id_prefix_check": { + "name": "page_id_prefix_check", + "value": "\"page_id\" LIKE 'pg\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.rate_limit_buckets": { + "name": "rate_limit_buckets", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reset_at": { + "name": "reset_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redirect_domains": { + "name": "redirect_domains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path_prefix": { + "name": "path_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "rules": { + "name": "rules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "txt_satisfied": { + "name": "txt_satisfied", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "routing_satisfied": { + "name": "routing_satisfied", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "redirect_domains_team_idx": { + "name": "redirect_domains_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "redirect_domains_team_id_teams_id_fk": { + "name": "redirect_domains_team_id_teams_id_fk", + "tableFrom": "redirect_domains", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redirect_domains_hostname_unique": { + "name": "redirect_domains_hostname_unique", + "columns": [ + "hostname" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "redirect_domains_verified_at_check": { + "name": "redirect_domains_verified_at_check", + "value": "(\"status\" <> 'verified' OR \"verified_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.site_cache_outbox": { + "name": "site_cache_outbox", + "schema": "", + "columns": { + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "invalidate_hosts": { + "name": "invalidate_hosts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "destinations": { + "name": "destinations", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "acknowledgments": { + "name": "acknowledgments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "site_cache_outbox_poll_idx": { + "name": "site_cache_outbox_poll_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "site_cache_outbox_team_id_teams_id_fk": { + "name": "site_cache_outbox_team_id_teams_id_fk", + "tableFrom": "site_cache_outbox", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_release_pages": { + "name": "site_release_pages", + "schema": "", + "columns": { + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "site_release_pages_slug_idx": { + "name": "site_release_pages_slug_idx", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "site_release_pages_page_idx": { + "name": "site_release_pages_page_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "site_release_pages_release_id_site_releases_id_fk": { + "name": "site_release_pages_release_id_site_releases_id_fk", + "tableFrom": "site_release_pages", + "columnsFrom": [ + "release_id" + ], + "tableTo": "site_releases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "site_release_pages_page_id_pages_id_fk": { + "name": "site_release_pages_page_id_pages_id_fk", + "tableFrom": "site_release_pages", + "columnsFrom": [ + "page_id" + ], + "tableTo": "pages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "site_release_pages_revision_id_page_revisions_id_fk": { + "name": "site_release_pages_revision_id_page_revisions_id_fk", + "tableFrom": "site_release_pages", + "columnsFrom": [ + "revision_id" + ], + "tableTo": "page_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_releases": { + "name": "site_releases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docs_publication_id": { + "name": "docs_publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "site": { + "name": "site", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "site_releases_team_idx": { + "name": "site_releases_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "site_releases_team_id_teams_id_fk": { + "name": "site_releases_team_id_teams_id_fk", + "tableFrom": "site_releases", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "site_releases_release_id_unique": { + "name": "site_releases_release_id_unique", + "columns": [ + "release_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "release_id_prefix_check": { + "name": "release_id_prefix_check", + "value": "\"release_id\" LIKE 'rel\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.team_invitations": { + "name": "team_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "invited_by_account_id": { + "name": "invited_by_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_invitations_pending_team_email_idx": { + "name": "team_invitations_pending_team_email_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"team_invitations\".\"status\" = 'pending'", + "concurrently": false + }, + "team_invitations_team_created_idx": { + "name": "team_invitations_team_created_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "team_invitations_team_email_idx": { + "name": "team_invitations_team_email_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "team_invitations_expires_idx": { + "name": "team_invitations_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "team_invitations_team_id_teams_id_fk": { + "name": "team_invitations_team_id_teams_id_fk", + "tableFrom": "team_invitations", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "team_invitations_invited_by_account_id_accounts_id_fk": { + "name": "team_invitations_invited_by_account_id_accounts_id_fk", + "tableFrom": "team_invitations", + "columnsFrom": [ + "invited_by_account_id" + ], + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_invitations_invitation_id_unique": { + "name": "team_invitations_invitation_id_unique", + "columns": [ + "invitation_id" + ], + "nullsNotDistinct": false + }, + "team_invitations_token_hash_unique": { + "name": "team_invitations_token_hash_unique", + "columns": [ + "token_hash" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "invitation_id_prefix_check": { + "name": "invitation_id_prefix_check", + "value": "\"invitation_id\" LIKE 'tinv\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_members_team_account_idx": { + "name": "team_members_team_account_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "team_members_account_id_accounts_id_fk": { + "name": "team_members_account_id_accounts_id_fk", + "tableFrom": "team_members", + "columnsFrom": [ + "account_id" + ], + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_members_member_id_unique": { + "name": "team_members_member_id_unique", + "columns": [ + "member_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "member_id_prefix_check": { + "name": "member_id_prefix_check", + "value": "\"member_id\" LIKE 'tmem\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.team_settings": { + "name": "team_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "theme_id": { + "name": "theme_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_injection_head": { + "name": "code_injection_head", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_injection_body": { + "name": "code_injection_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_shared_widgets": { + "name": "draft_shared_widgets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"header\":{\"widgetId\":\"shared-header\",\"name\":\"header\",\"deletable\":false,\"moveable\":false,\"shared\":true,\"settings\":{\"verticalPadding\":\"py-4\"}},\"footer\":{\"widgetId\":\"shared-footer\",\"name\":\"footer\",\"deletable\":false,\"moveable\":false,\"shared\":true,\"settings\":{}}}'::jsonb" + }, + "active_release_id": { + "name": "active_release_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_settings_team_id_teams_id_fk": { + "name": "team_settings_team_id_teams_id_fk", + "tableFrom": "team_settings", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_settings_team_id_unique": { + "name": "team_settings_team_id_unique", + "columns": [ + "team_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_subdomain": { + "name": "redirect_subdomain", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "hide_from_search": { + "name": "hide_from_search", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "custom_domain_cname_target": { + "name": "custom_domain_cname_target", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_domain_txt_record_name": { + "name": "custom_domain_txt_record_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_domain_verification_token": { + "name": "custom_domain_verification_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_domain_txt_satisfied": { + "name": "custom_domain_txt_satisfied", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_account_id_accounts_id_fk": { + "name": "teams_owner_account_id_accounts_id_fk", + "tableFrom": "teams", + "columnsFrom": [ + "owner_account_id" + ], + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_team_id_unique": { + "name": "teams_team_id_unique", + "columns": [ + "team_id" + ], + "nullsNotDistinct": false + }, + "teams_external_id_unique": { + "name": "teams_external_id_unique", + "columns": [ + "external_id" + ], + "nullsNotDistinct": false + }, + "teams_subdomain_unique": { + "name": "teams_subdomain_unique", + "columns": [ + "subdomain" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "team_id_prefix_check": { + "name": "team_id_prefix_check", + "value": "\"team_id\" LIKE 'team\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.themes": { + "name": "themes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "theme_id": { + "name": "theme_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "style": { + "name": "style", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "themes_team_idx": { + "name": "themes_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "themes_team_id_teams_id_fk": { + "name": "themes_team_id_teams_id_fk", + "tableFrom": "themes", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "themes_theme_id_unique": { + "name": "themes_theme_id_unique", + "columns": [ + "theme_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "theme_id_prefix_check": { + "name": "theme_id_prefix_check", + "value": "\"theme_id\" LIKE 'thm\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.ticket_access_grants": { + "name": "ticket_access_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ticket_access_grants_ticket_idx": { + "name": "ticket_access_grants_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "ticket_access_grants_team_id_teams_id_fk": { + "name": "ticket_access_grants_team_id_teams_id_fk", + "tableFrom": "ticket_access_grants", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "ticket_access_grants_ticket_id_tickets_id_fk": { + "name": "ticket_access_grants_ticket_id_tickets_id_fk", + "tableFrom": "ticket_access_grants", + "columnsFrom": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ticket_access_grants_token_hash_unique": { + "name": "ticket_access_grants_token_hash_unique", + "columns": [ + "token_hash" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ticket_attachments": { + "name": "ticket_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "attachment_id": { + "name": "attachment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "media_lit_id": { + "name": "media_lit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ticket_attachments_ticket_idx": { + "name": "ticket_attachments_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "ticket_attachments_message_idx": { + "name": "ticket_attachments_message_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "ticket_attachments_team_id_teams_id_fk": { + "name": "ticket_attachments_team_id_teams_id_fk", + "tableFrom": "ticket_attachments", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "ticket_attachments_ticket_id_tickets_id_fk": { + "name": "ticket_attachments_ticket_id_tickets_id_fk", + "tableFrom": "ticket_attachments", + "columnsFrom": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "ticket_attachments_message_id_ticket_messages_id_fk": { + "name": "ticket_attachments_message_id_ticket_messages_id_fk", + "tableFrom": "ticket_attachments", + "columnsFrom": [ + "message_id" + ], + "tableTo": "ticket_messages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ticket_attachments_attachment_id_unique": { + "name": "ticket_attachments_attachment_id_unique", + "columns": [ + "attachment_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "attachment_id_prefix_check": { + "name": "attachment_id_prefix_check", + "value": "\"attachment_id\" LIKE 'att\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.ticket_mail_outbox": { + "name": "ticket_mail_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ticket_mail_outbox_ticket_seq_idx": { + "name": "ticket_mail_outbox_ticket_seq_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "ticket_mail_outbox_poll_idx": { + "name": "ticket_mail_outbox_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "ticket_mail_outbox_team_id_teams_id_fk": { + "name": "ticket_mail_outbox_team_id_teams_id_fk", + "tableFrom": "ticket_mail_outbox", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "ticket_mail_outbox_ticket_id_tickets_id_fk": { + "name": "ticket_mail_outbox_ticket_id_tickets_id_fk", + "tableFrom": "ticket_mail_outbox", + "columnsFrom": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ticket_messages": { + "name": "ticket_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_account_id": { + "name": "author_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ticket_messages_ticket_idx": { + "name": "ticket_messages_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "ticket_messages_team_id_teams_id_fk": { + "name": "ticket_messages_team_id_teams_id_fk", + "tableFrom": "ticket_messages", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "ticket_messages_ticket_id_tickets_id_fk": { + "name": "ticket_messages_ticket_id_tickets_id_fk", + "tableFrom": "ticket_messages", + "columnsFrom": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "ticket_messages_author_account_id_accounts_id_fk": { + "name": "ticket_messages_author_account_id_accounts_id_fk", + "tableFrom": "ticket_messages", + "columnsFrom": [ + "author_account_id" + ], + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ticket_messages_message_id_unique": { + "name": "ticket_messages_message_id_unique", + "columns": [ + "message_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "message_id_prefix_check": { + "name": "message_id_prefix_check", + "value": "\"message_id\" LIKE 'tmsg\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.tickets": { + "name": "tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "requester_email": { + "name": "requester_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requester_name": { + "name": "requester_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'form'" + }, + "access_token_hash": { + "name": "access_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assignee_account_id": { + "name": "assignee_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_state": { + "name": "ai_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "ai_generation": { + "name": "ai_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ai_trigger_message_id": { + "name": "ai_trigger_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_stop_reason": { + "name": "ai_stop_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_stopped_at": { + "name": "ai_stopped_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tickets_team_status_idx": { + "name": "tickets_team_status_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tickets_team_id_teams_id_fk": { + "name": "tickets_team_id_teams_id_fk", + "tableFrom": "tickets", + "columnsFrom": [ + "team_id" + ], + "tableTo": "teams", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tickets_assignee_account_id_accounts_id_fk": { + "name": "tickets_assignee_account_id_accounts_id_fk", + "tableFrom": "tickets", + "columnsFrom": [ + "assignee_account_id" + ], + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tickets_ticket_id_unique": { + "name": "tickets_ticket_id_unique", + "columns": [ + "ticket_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "ticket_id_prefix_check": { + "name": "ticket_id_prefix_check", + "value": "\"ticket_id\" LIKE 'tkt\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index 9f804cb6..ef4ee3f6 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -106,6 +106,13 @@ "when": 1793602449001, "tag": "0014_drop_oauth_team_selection", "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1794041047457, + "tag": "0015_changelog_cleanup", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/auth/operation-policy.coverage.test.ts b/apps/api/src/auth/operation-policy.coverage.test.ts index be35ada8..ac654c12 100644 --- a/apps/api/src/auth/operation-policy.coverage.test.ts +++ b/apps/api/src/auth/operation-policy.coverage.test.ts @@ -116,7 +116,7 @@ describe("fail-closed operation policy coverage", () => { it("registers every MCP tool with an explicit policy", () => { const policies = listMcpToolPolicies(); - expect(Object.keys(policies).length).toBe(139); + expect(Object.keys(policies).length).toBe(135); for (const policy of Object.values(policies)) { expect(policy.oauthScope || policy.scope).toBeTruthy(); expect(["allow", "deny"]).toContain(policy.apiKeyAccess); diff --git a/apps/api/src/auth/operation-policy.ts b/apps/api/src/auth/operation-policy.ts index e129c8f3..230affd5 100644 --- a/apps/api/src/auth/operation-policy.ts +++ b/apps/api/src/auth/operation-policy.ts @@ -254,22 +254,6 @@ export const REST_OPERATION_POLICIES = { "product-updates:write", MCP_SCOPES.productUpdatesWrite, ), - "content.productUpdates.connectGithub": teamPermission( - "product-updates:write", - MCP_SCOPES.productUpdatesWrite, - ), - "content.productUpdates.getGithubIntegration": teamPermission( - "product-updates:read", - MCP_SCOPES.productUpdatesRead, - ), - "content.productUpdates.disconnectGithub": teamPermission( - "product-updates:write", - MCP_SCOPES.productUpdatesWrite, - ), - "content.productUpdates.importGithubReleases": teamPermission( - "product-updates:write", - MCP_SCOPES.productUpdatesWrite, - ), "content.documentationSections.validate": teamPermission( "documentation:read", diff --git a/apps/api/src/db/changelog-cleanup-migration.test.ts b/apps/api/src/db/changelog-cleanup-migration.test.ts new file mode 100644 index 00000000..b500fdbc --- /dev/null +++ b/apps/api/src/db/changelog-cleanup-migration.test.ts @@ -0,0 +1,96 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { beforeEach, describe, expect, it } from "vitest"; +import { eq, sql } from "drizzle-orm"; +import { makeTestDb, seedTeam, type TestDb } from "../test/db"; +import { documents, integrations } from "./schema"; + +/** + * 0015_changelog_cleanup removes the product-update GitHub import's stored + * tokens and normalizes changelog meta. The test database already applied it + * to an empty schema; re-run its statements against seeded legacy rows. + */ +const MIGRATION = readFileSync( + path.resolve(process.cwd(), "drizzle/0015_changelog_cleanup.sql"), + "utf8", +); + +async function runMigration(db: TestDb) { + for (const statement of MIGRATION.split("--> statement-breakpoint")) { + if (statement.trim()) await db.execute(sql.raw(statement)); + } +} + +describe("0015_changelog_cleanup migration", () => { + let db: TestDb; + let teamId: string; + + beforeEach(async () => { + db = await makeTestDb(); + teamId = (await seedTeam(db)).team.id; + }); + + it("deletes only the changelog GitHub token rows", async () => { + // "github" is no longer a valid provider in the schema; legacy rows exist in the DB only. + await db.execute( + sql`INSERT INTO integrations (id, team_id, provider, config) VALUES + ('gh', ${teamId}, 'github', '{"repo":"o/r","token":"enc"}'::jsonb), + ('sl', ${teamId}, 'sendlit', '{"apiKey":"enc"}'::jsonb)`, + ); + + await runMigration(db); + + const rows = await db.select().from(integrations); + expect(rows.map((row) => row.provider)).toEqual(["sendlit"]); + }); + + it("normalizes changelog labels, release dates and drops GitHub meta", async () => { + const legacy = { + version: "v1.0.0", + labels: ["improved", "prerelease", "fixed", "changed"], + releaseDate: "2026-03-04T10:20:30Z", + github: { releaseId: 1, releaseUrl: "https://github.com/o/r", tagName: "v1.0.0" }, + indexable: false, + }; + await db.insert(documents).values([ + { + teamId, + kind: "changelog", + slug: "legacy", + draftTitle: "Legacy", + draftContent: { type: "doc", content: [] }, + meta: legacy, + draftMeta: { ...legacy, releaseDate: "not a date" }, + }, + { + teamId, + kind: "blog", + slug: "post", + draftTitle: "Post", + draftContent: { type: "doc", content: [] }, + meta: { labels: ["improved"], github: { releaseId: 2 } }, + }, + ]); + + await runMigration(db); + + const [changelog] = await db + .select() + .from(documents) + .where(eq(documents.slug, "legacy")); + expect(changelog.meta).toEqual({ + version: "v1.0.0", + labels: ["changed", "fixed"], + releaseDate: "2026-03-04", + indexable: false, + }); + expect(changelog.draftMeta).toEqual({ + version: "v1.0.0", + labels: ["changed", "fixed"], + indexable: false, + }); + + const [blog] = await db.select().from(documents).where(eq(documents.slug, "post")); + expect(blog.meta).toEqual({ labels: ["improved"], github: { releaseId: 2 } }); + }); +}); diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 08abef8e..7620271c 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -660,12 +660,12 @@ export const media = pgTable( ); // --------------------------------------------------------------------------- -// Integrations (architecture.md §8.1): per-team third-party connections. -// v1 has one provider ("github"), config is provider-shaped jsonb holding -// secrets (PAT) — never returned verbatim over the API (see routes.ts). +// Integrations: per-team third-party credentials. Today only SendLit (email); +// config is provider-shaped jsonb holding encrypted secrets, never returned +// over the API. The docs/pages GitHub App uses the documentation_git_* tables. // --------------------------------------------------------------------------- -export const integrationProviders = ["github", "sendlit"] as const; +export const integrationProviders = ["sendlit"] as const; export type IntegrationProvider = (typeof integrationProviders)[number]; export const integrations = pgTable( diff --git a/apps/api/src/documentation/chunking.test.ts b/apps/api/src/documentation/chunking.test.ts index 62e58718..8add3cef 100644 --- a/apps/api/src/documentation/chunking.test.ts +++ b/apps/api/src/documentation/chunking.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { extractChunks } from "./chunking"; +import { changelogChunkContext, extractChunks } from "./chunking"; import type { TextEditorContentRow } from "../db/schema"; function heading(text: string) { @@ -192,3 +192,37 @@ describe("extractChunks", () => { ); }); }); + +describe("changelog chunks", () => { + it("never anchors to section headings, so citations link to the entry", () => { + const content = { + type: "doc", + content: [heading("Fixed"), paragraph("Exports include archived items.")], + } as TextEditorContentRow; + const [chunk] = extractChunks(content, "changelog"); + expect(chunk.heading).toBe("Fixed"); + expect(chunk.anchor).toBeNull(); + }); + + it("builds release context from version and release date", () => { + expect( + changelogChunkContext({ + title: "Dark mode", + slug: "dark-mode", + meta: { version: "1.2.0", releaseDate: "2026-11-07" }, + publishedAt: new Date("2026-11-08T10:00:00Z"), + }), + ).toBe("Dark mode — 1.2.0, 2026-11-07"); + }); + + it("falls back to the publish date and slug", () => { + expect( + changelogChunkContext({ + title: null, + slug: "dark-mode", + meta: {}, + publishedAt: new Date("2026-11-08T23:30:00Z"), + }), + ).toBe("dark-mode — 2026-11-08"); + }); +}); diff --git a/apps/api/src/documentation/chunking.ts b/apps/api/src/documentation/chunking.ts index aa69717d..fb850c90 100644 --- a/apps/api/src/documentation/chunking.ts +++ b/apps/api/src/documentation/chunking.ts @@ -1,3 +1,4 @@ +import { changelogDate } from "@frontlit/api-contract"; import type { TextEditorContentRow } from "../db/schema"; import { slugify } from "../utils/slug"; import { createHeadingIdGenerator, docsNodeText } from "@frontlit/text-editor/headings"; @@ -123,7 +124,14 @@ export function extractChunks( if (node.type === "heading") { flush(); heading = text; - anchor = kind === "doc" ? nextHeadingId(text) : slugify(text); + // A changelog entry is one anchor (`/changelog#`); its section + // headings ("Fixed", "Added") are not linkable on their own. + anchor = + kind === "doc" + ? nextHeadingId(text) + : kind === "changelog" + ? null + : slugify(text); continue; } @@ -150,3 +158,21 @@ export function extractChunks( return chunks; } + +/** + * Release context prepended to every changelog chunk ("Dark mode — 1.2.0, + * 2026-11-07"), so retrieval never presents an old fix without its release. + */ +export function changelogChunkContext(doc: { + title: string | null; + slug: string; + meta: Record | null; + publishedAt: Date | null; +}): string { + const meta = (doc.meta ?? {}) as { version?: string; releaseDate?: string }; + const details = [meta.version, changelogDate({ meta, publishedAt: doc.publishedAt })] + .filter(Boolean) + .join(", "); + const title = doc.title || doc.slug; + return details ? `${title} — ${details}` : title; +} diff --git a/apps/api/src/documentation/queries.ts b/apps/api/src/documentation/queries.ts index 414f09ab..e2e9de39 100644 --- a/apps/api/src/documentation/queries.ts +++ b/apps/api/src/documentation/queries.ts @@ -25,8 +25,14 @@ export async function reindexDocument(input: { documentId: string; content: unknown; kind?: "doc" | "blog" | "changelog"; + /** Prepended to every chunk, e.g. a changelog entry's release line. */ + context?: string; }) { - const chunks = extractChunks(input.content as any, input.kind); + const chunks = extractChunks(input.content as any, input.kind).map((chunk) => + input.context + ? { ...chunk, content: `${input.context}\n\n${chunk.content}` } + : chunk, + ); const embeddingConfig = await getResolvedEmbeddingConfig(input.teamId); const embeddings = chunks.length diff --git a/apps/api/src/documentation/worker.ts b/apps/api/src/documentation/worker.ts index 3e42d5ac..7a5bc6dd 100644 --- a/apps/api/src/documentation/worker.ts +++ b/apps/api/src/documentation/worker.ts @@ -7,6 +7,7 @@ import { deleteChunksForDocument, reindexDocument } from "./queries"; import { isDocIndexable } from "./index-status"; import { teamIndexesKind } from "./index-policy"; import { claimNextDocumentIndexJob, finishDocumentIndexJob } from "./index-jobs"; +import { changelogChunkContext } from "./chunking"; export async function processOneDocumentIndexJob(): Promise { const job = await claimNextDocumentIndexJob(); @@ -38,6 +39,7 @@ export async function processOneDocumentIndexJob(): Promise { documentId: doc.id, content: doc.content, kind: doc.kind, + context: doc.kind === "changelog" ? changelogChunkContext(doc) : undefined, }); await finishDocumentIndexJob(job.id, job.generation, { status: "done", diff --git a/apps/api/src/documents/changelogs-routes.ts b/apps/api/src/documents/changelogs-routes.ts index d108cb94..80d8ba78 100644 --- a/apps/api/src/documents/changelogs-routes.ts +++ b/apps/api/src/documents/changelogs-routes.ts @@ -1,211 +1,9 @@ -import { Router } from "express"; -import { createExpressEndpoints, initServer } from "@ts-rest/express"; import { contract } from "@frontlit/api-contract"; -import { requireAuth } from "../auth/middleware"; -import { requireTeam } from "../auth/require-team"; -import { authorizeHandlers } from "../auth/authorize"; -import { - countDocuments, - createDocument, - createDocumentWithContent, - deleteDocument, - deriveDocumentStatus, - discardDocumentDraft, - findChangelogByGithubReleaseId, - getDocumentByDocumentId, - listDocuments, - publishDocument, - updateDocument, -} from "./queries"; -import { omitInternal, serializeDates } from "../utils/public"; -import { - deleteIntegration, - getGithubIntegration, - upsertGithubIntegration, -} from "../integrations/queries"; -import { listReleases, markdownToTipTap } from "../integrations/github"; +import { createDocumentsRouter } from "./routes"; -const KIND = "changelog" as const; - -function toPublicDocument>(doc: T) { - return serializeDates( - omitInternal( - { ...doc, status: deriveDocumentStatus(doc as any) }, - "teamId", - "kind", - ), - ); -} - -/** - * Product updates reuse the shared documents engine (stored internally as - * `kind: "changelog"`) plus a GitHub Releases importer. Standard - * CRUD/draft-publish endpoints are implemented inline here - * (rather than via `createDocumentsRouter`) because `contract.content.productUpdates` - * also carries the GitHub-specific endpoints in the same router. - */ -const router = Router(); -router.use("/content/product-updates", requireAuth, requireTeam); - -const s = initServer(); - -// Key order here is the actual Express route-registration order (ts-rest's -// `recursivelyApplyExpressRouter` walks this `impl` object, not the -// contract) — the literal GitHub routes must come before the generic -// `/content/product-updates/:documentId` routes -// (`get`/`update`/`publish`/`discardDraft`/`remove`), or Express's -// first-match routing would shadow them with the wildcard. -const impl = s.router( +/** Product updates are documents stored internally as `kind: "changelog"`. */ +export default createDocumentsRouter( + "changelog", + "/content/product-updates", contract.content.productUpdates, - authorizeHandlers( - { - connectGithub: async ({ body, req }) => { - await upsertGithubIntegration((req as any).teamId, body); - return { - status: 200, - body: { connected: true, repo: body.repo }, - }; - }, - getGithubIntegration: async ({ req }) => { - const integration = await getGithubIntegration((req as any).teamId); - return { - status: 200, - body: { - connected: Boolean(integration), - repo: integration?.repo ?? null, - }, - }; - }, - disconnectGithub: async ({ req }) => { - await deleteIntegration((req as any).teamId, "github"); - return { status: 204, body: undefined }; - }, - importGithubReleases: async ({ req }) => { - const teamId = (req as any).teamId; - const integration = await getGithubIntegration(teamId); - if (!integration) - return { - status: 400, - body: { - error: "No GitHub integration connected for this team", - }, - }; - - const releases = await listReleases(integration.token, integration.repo); - let imported = 0; - let skipped = 0; - - for (const release of releases) { - if (release.draft) { - skipped++; - continue; - } - const existing = await findChangelogByGithubReleaseId(teamId, release.id); - if (existing) { - skipped++; - continue; - } - - const title = release.name || release.tag_name; - await createDocumentWithContent(KIND, { - teamId, - title, - content: markdownToTipTap(release.body || ""), - meta: { - version: release.tag_name, - labels: release.prerelease ? ["prerelease"] : [], - releaseDate: release.published_at ?? undefined, - github: { - releaseId: release.id, - releaseUrl: release.html_url, - tagName: release.tag_name, - }, - }, - }); - imported++; - } - - return { status: 200, body: { imported, skipped } }; - }, - create: async ({ body, req }) => { - const doc = await createDocument(KIND, { - teamId: (req as any).teamId, - ...body, - }); - return { status: 201, body: toPublicDocument(doc) }; - }, - list: async ({ query, req }) => { - const teamId = (req as any).teamId; - const [items, total] = await Promise.all([ - listDocuments(KIND, { - teamId, - q: query.q, - offset: query.offset, - itemsPerPage: query.itemsPerPage, - }), - countDocuments(KIND, { teamId, q: query.q }), - ]); - return { - status: 200, - body: { items: items.map(toPublicDocument), total }, - }; - }, - get: async ({ params, req }) => { - const doc = await getDocumentByDocumentId(KIND, params.documentId); - if (!doc || doc.teamId !== (req as any).teamId) - return { status: 404, body: { error: "Not found" } }; - return { status: 200, body: toPublicDocument(doc) }; - }, - update: async ({ params, body, req }) => { - const doc = await updateDocument( - KIND, - (req as any).teamId, - params.documentId, - body, - ); - if (!doc) return { status: 404, body: { error: "Not found" } }; - return { status: 200, body: toPublicDocument(doc) }; - }, - publish: async ({ params, req }) => { - const doc = await publishDocument(KIND, (req as any).teamId, params.documentId); - if (!doc) return { status: 404, body: { error: "Not found" } }; - return { status: 200, body: toPublicDocument(doc) }; - }, - discardDraft: async ({ params, req }) => { - const doc = await discardDocumentDraft( - KIND, - (req as any).teamId, - params.documentId, - ); - if (!doc) return { status: 404, body: { error: "Not found" } }; - return { status: 200, body: toPublicDocument(doc) }; - }, - remove: async ({ params, req }) => { - const result = await deleteDocument( - KIND, - (req as any).teamId, - params.documentId, - ); - if (!result.ok) return { status: 404, body: { error: "Not found" } }; - return { status: 204, body: undefined }; - }, - }, - { - connectGithub: "content.productUpdates.connectGithub", - getGithubIntegration: "content.productUpdates.getGithubIntegration", - disconnectGithub: "content.productUpdates.disconnectGithub", - importGithubReleases: "content.productUpdates.importGithubReleases", - create: "content.productUpdates.create", - list: "content.productUpdates.list", - get: "content.productUpdates.get", - update: "content.productUpdates.update", - publish: "content.productUpdates.publish", - discardDraft: "content.productUpdates.discardDraft", - remove: "content.productUpdates.remove", - }, - ), ); - -createExpressEndpoints(contract.content.productUpdates, impl, router); - -export default router; diff --git a/apps/api/src/documents/product-updates-contract.test.ts b/apps/api/src/documents/product-updates-contract.test.ts new file mode 100644 index 00000000..ae25468c --- /dev/null +++ b/apps/api/src/documents/product-updates-contract.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { + changelogDate, + contract, + productUpdateMetaSchema, + publicContentPath, +} from "@frontlit/api-contract"; + +describe("product update contract", () => { + it("accepts Keep a Changelog labels and calendar dates", () => { + expect( + productUpdateMetaSchema.parse({ + version: "v1.2.0", + labels: ["added", "security"], + releaseDate: "2026-11-07", + }), + ).toEqual({ + version: "v1.2.0", + labels: ["added", "security"], + releaseDate: "2026-11-07", + }); + }); + + it.each([ + [{ labels: ["improved"] }], + [{ labels: ["fixed", "fixed"] }], + [{ releaseDate: "2026-11-07T10:00:00Z" }], + [{ releaseDate: "07/11/2026" }], + [{ version: "v".repeat(65) }], + ])("rejects invalid meta %j", (meta) => { + expect(productUpdateMetaSchema.safeParse(meta).success).toBe(false); + }); + + it("caps the public page size", () => { + const query = contract.public.listProductUpdates.query; + expect(query.safeParse({ itemsPerPage: "1000" }).success).toBe(true); + expect(query.safeParse({ itemsPerPage: "1001" }).success).toBe(false); + }); + + it("links every changelog entry to its anchor, ignoring section anchors", () => { + expect(publicContentPath("changelog", "dark-mode")).toBe("/changelog#dark-mode"); + expect(publicContentPath("changelog", "dark-mode", "fixed")).toBe( + "/changelog#dark-mode", + ); + }); + + it("dates entries by release date, else the UTC publish date", () => { + expect( + changelogDate({ + meta: { releaseDate: "2026-01-02" }, + publishedAt: "2026-05-05T00:00:00Z", + }), + ).toBe("2026-01-02"); + expect(changelogDate({ meta: {}, publishedAt: "2026-05-05T23:30:00Z" })).toBe( + "2026-05-05", + ); + expect(changelogDate({ meta: {}, publishedAt: null })).toBeNull(); + }); +}); diff --git a/apps/api/src/documents/queries.test.ts b/apps/api/src/documents/queries.test.ts index fb066cd8..a244479c 100644 --- a/apps/api/src/documents/queries.test.ts +++ b/apps/api/src/documents/queries.test.ts @@ -18,6 +18,8 @@ import { updateDocument, } from "./queries"; import { listMedia } from "../media/queries"; +import { documents } from "../db/schema"; +import { eq } from "drizzle-orm"; describe("documents engine (blog kind)", () => { const db = testDb as unknown as TestDb; @@ -310,3 +312,93 @@ describe("documents engine (blog kind)", () => { }); }); }); + +describe("documents engine (changelog kind)", () => { + const db = testDb as unknown as TestDb; + let teamId: string; + + beforeEach(async () => { + teamId = (await seedTeam(db)).team.id; + }); + + async function publishedEntry( + title: string, + meta: Record, + publishedAt: string, + ) { + const created = await createDocument("changelog", { teamId, title, meta }); + await publishDocument("changelog", teamId, created.documentId); + await db + .update(documents) + .set({ publishedAt: new Date(publishedAt) }) + .where(eq(documents.documentId, created.documentId)); + return created; + } + + it("orders the public feed by release date, falling back to the publish date", async () => { + await publishedEntry( + "Old release", + { releaseDate: "2026-01-10" }, + "2026-11-01T09:00:00Z", + ); + await publishedEntry("No release date", {}, "2026-06-15T09:00:00Z"); + await publishedEntry( + "New release", + { releaseDate: "2026-09-01" }, + "2026-09-02T09:00:00Z", + ); + + const items = await listPublishedDocuments("changelog", { + teamId, + itemsPerPage: 10, + }); + expect(items.map((item) => item.title)).toEqual([ + "New release", + "No release date", + "Old release", + ]); + }); + + it("breaks release-date ties by publish time, newest first", async () => { + await publishedEntry( + "Morning", + { releaseDate: "2026-09-01" }, + "2026-09-01T08:00:00Z", + ); + await publishedEntry( + "Evening", + { releaseDate: "2026-09-01" }, + "2026-09-01T20:00:00Z", + ); + + const items = await listPublishedDocuments("changelog", { + teamId, + itemsPerPage: 10, + }); + expect(items.map((item) => item.title)).toEqual(["Evening", "Morning"]); + }); + + it("ignores unpublished draft meta when ordering", async () => { + const first = await publishedEntry( + "First", + { releaseDate: "2026-02-01" }, + "2026-02-01T00:00:00Z", + ); + await publishedEntry( + "Second", + { releaseDate: "2026-03-01" }, + "2026-03-01T00:00:00Z", + ); + // A draft-only date change must not move the published entry. + await updateDocument("changelog", teamId, first.documentId, { + meta: { releaseDate: "2026-12-31" }, + }); + + const items = await listPublishedDocuments("changelog", { + teamId, + itemsPerPage: 10, + }); + expect(items.map((item) => item.title)).toEqual(["Second", "First"]); + expect(items[1].meta).toMatchObject({ releaseDate: "2026-02-01" }); + }); +}); diff --git a/apps/api/src/documents/queries.ts b/apps/api/src/documents/queries.ts index 88b4ecc1..0af9c59f 100644 --- a/apps/api/src/documents/queries.ts +++ b/apps/api/src/documents/queries.ts @@ -9,7 +9,7 @@ import { getDocsSettings, } from "../documentation/authoring"; import { DocsContentError } from "@frontlit/text-editor/docs"; -import { and, count, eq, ilike, isNotNull, ne } from "drizzle-orm"; +import { and, count, desc, eq, ilike, isNotNull, ne, sql } from "drizzle-orm"; import { db } from "../db/client"; import { documents, type DocumentKind, type TextEditorContentRow } from "../db/schema"; import { @@ -537,10 +537,18 @@ export async function listPublishedDocuments( ), limit, offset, - orderBy: (row, { desc }) => desc(row.publishedAt), + orderBy: + kind === "changelog" + ? [desc(changelogDateSql), desc(documents.publishedAt)] + : [desc(documents.publishedAt)], }); } +/** SQL twin of `changelogDate()` from the API contract: the published release + * date, else the publish date (stored as UTC wall time), as `YYYY-MM-DD`. Reads the + * published `meta` only, never `draftMeta`. */ +const changelogDateSql = sql`coalesce(${documents.meta}->>'releaseDate', to_char(${documents.publishedAt}, 'YYYY-MM-DD'))`; + export async function countPublishedDocuments( kind: DocumentKind, input: { teamId: string }, @@ -578,60 +586,3 @@ export async function getPublishedDocumentBySlug( ), }); } - -/** Import idempotency key for GitHub-sourced changelog entries - * (architecture.md §8.1): one draft document per GitHub release id, never - * duplicated on repeat imports. */ -export async function findChangelogByGithubReleaseId( - teamId: string, - releaseId: number, -) { - const all = await db.query.documents.findMany({ - where: and(eq(documents.teamId, teamId), eq(documents.kind, "changelog")), - }); - return all.find( - (doc) => - (doc.draftMeta as any)?.github?.releaseId === releaseId || - (doc.meta as any)?.github?.releaseId === releaseId, - ); -} - -export async function createDocumentWithContent( - kind: DocumentKind, - input: { - teamId: string; - title: string; - content: TextEditorContentRow; - featuredImage?: unknown; - meta?: Record; - }, -) { - if (kind === "doc") { - const { teamId, ...body } = input; - return (await saveDocumentation(teamId, null, body))!; - } - const slug = await uniqueSlug(slugify(input.title), (candidate) => - slugTaken(input.teamId, kind, candidate), - ); - - const normalizedFeaturedImage = normalizeFeaturedImage(input.featuredImage); - - const [doc] = await db - .insert(documents) - .values({ - teamId: input.teamId, - kind, - slug, - draftTitle: input.title, - draftContent: input.content, - draftFeaturedImage: normalizedFeaturedImage, - draftMeta: input.meta ?? {}, - }) - .returning(); - - if (normalizedFeaturedImage) { - await syncFeaturedImageToMediaLibrary(input.teamId, normalizedFeaturedImage); - } - - return doc; -} diff --git a/apps/api/src/integrations/github.ts b/apps/api/src/integrations/github.ts deleted file mode 100644 index 38a052a3..00000000 --- a/apps/api/src/integrations/github.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Minimal GitHub REST client for the changelog importer (architecture.md - * §8.1, v1). Uses `fetch` directly rather than an SDK — the only surface - * needed is "list releases for a repo". - */ - -export type GithubRelease = { - id: number; - tag_name: string; - name: string | null; - body: string | null; - html_url: string; - published_at: string | null; - draft: boolean; - prerelease: boolean; -}; - -export class GithubApiError extends Error {} - -export async function listReleases( - token: string, - repo: string, -): Promise { - const res = await fetch(`https://api.github.com/repos/${repo}/releases?per_page=50`, { - headers: { - Authorization: `Bearer ${token}`, - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - "User-Agent": "FrontLit", - }, - }); - - if (!res.ok) { - const text = await res.text().catch(() => ""); - throw new GithubApiError(`GitHub API error ${res.status}: ${text.slice(0, 300)}`); - } - - return (await res.json()) as GithubRelease[]; -} - -/** Very small markdown -> TipTap JSON converter, good enough for release - * notes: paragraphs and `- ` bullet lists, everything else stays as plain - * paragraphs. Kept intentionally simple for v1 (architecture.md §8.1); a - * full markdown parser can replace this later without touching callers. */ -export function markdownToTipTap(markdown: string): { - type: "doc"; - content: Record[]; -} { - const lines = markdown.replace(/\r\n/g, "\n").split("\n"); - const content: Record[] = []; - let listItems: Record[] = []; - - function flushList() { - if (listItems.length) { - content.push({ type: "bulletList", content: listItems }); - listItems = []; - } - } - - for (const rawLine of lines) { - const line = rawLine.trim(); - if (!line) { - flushList(); - continue; - } - const headingMatch = line.match(/^(#{1,6})\s+(.*)$/); - if (headingMatch) { - flushList(); - content.push({ - type: "heading", - attrs: { level: Math.min(headingMatch[1].length, 3) }, - content: [{ type: "text", text: headingMatch[2] }], - }); - continue; - } - const bulletMatch = line.match(/^[-*]\s+(.*)$/); - if (bulletMatch) { - listItems.push({ - type: "listItem", - content: [ - { - type: "paragraph", - content: [{ type: "text", text: bulletMatch[1] }], - }, - ], - }); - continue; - } - flushList(); - content.push({ - type: "paragraph", - content: [{ type: "text", text: line }], - }); - } - flushList(); - - return { type: "doc", content: content.length ? content : [] }; -} diff --git a/apps/api/src/integrations/queries.ts b/apps/api/src/integrations/queries.ts index 417de627..f28f7cf4 100644 --- a/apps/api/src/integrations/queries.ts +++ b/apps/api/src/integrations/queries.ts @@ -9,52 +9,6 @@ export async function getIntegration(teamId: string, provider: IntegrationProvid }); } -/** Returns the connected repo + decrypted token, or null if not connected. */ -export async function getGithubIntegration( - teamId: string, -): Promise<{ repo: string; token: string } | null> { - const row = await getIntegration(teamId, "github"); - if (!row) return null; - const config = row.config as { repo: string; token: string }; - return { repo: config.repo, token: decryptSecret(config.token) }; -} - -export async function upsertGithubIntegration( - teamId: string, - input: { repo: string; token: string }, -) { - const encryptedToken = encryptSecret(input.token); - const existing = await getIntegration(teamId, "github"); - - if (existing) { - const [updated] = await db - .update(integrations) - .set({ - config: { repo: input.repo, token: encryptedToken }, - updatedAt: new Date(), - }) - .where(eq(integrations.id, existing.id)) - .returning(); - return updated; - } - - const [created] = await db - .insert(integrations) - .values({ - teamId, - provider: "github", - config: { repo: input.repo, token: encryptedToken }, - }) - .returning(); - return created; -} - -export async function deleteIntegration(teamId: string, provider: IntegrationProvider) { - await db - .delete(integrations) - .where(and(eq(integrations.teamId, teamId), eq(integrations.provider, provider))); -} - /** Returns the team's decrypted SendLit API key, or null if the team hasn't * been provisioned on SendLit yet (architecture.md §10.1). */ export async function getSendlitApiKey(teamId: string): Promise { diff --git a/apps/api/src/mcp/policy.ts b/apps/api/src/mcp/policy.ts index a63222a7..53571a2b 100644 --- a/apps/api/src/mcp/policy.ts +++ b/apps/api/src/mcp/policy.ts @@ -142,10 +142,6 @@ const toolScopes = { publish_product_update: MCP_SCOPES.productUpdatesWrite, discard_product_update_draft: MCP_SCOPES.productUpdatesWrite, delete_product_update: MCP_SCOPES.productUpdatesWrite, - get_github_product_updates_integration: MCP_SCOPES.productUpdatesRead, - connect_github_product_updates: MCP_SCOPES.productUpdatesWrite, - disconnect_github_product_updates: MCP_SCOPES.productUpdatesWrite, - import_github_releases: MCP_SCOPES.productUpdatesWrite, get_email_settings: MCP_SCOPES.settingsRead, update_email_settings: MCP_SCOPES.settingsWrite, @@ -238,7 +234,6 @@ export const HIGH_IMPACT_MCP_TOOLS = new Set([ "delete_documentation_page", "publish_product_update", "delete_product_update", - "import_github_releases", "test_esp_config", "remove_esp_config", "send_broadcast", diff --git a/apps/api/src/mcp/server.test.ts b/apps/api/src/mcp/server.test.ts index bad69f21..16d47bc5 100644 --- a/apps/api/src/mcp/server.test.ts +++ b/apps/api/src/mcp/server.test.ts @@ -74,9 +74,9 @@ describe("FrontLit MCP server", () => { const result = await client.listTools(); const policyNames = Object.keys(listMcpToolPolicies()).sort(); - expect(result.tools).toHaveLength(139); + expect(result.tools).toHaveLength(135); expect(result.tools.map((tool) => tool.name).sort()).toEqual(policyNames); - expect(new Set(result.tools.map((tool) => tool.name)).size).toBe(139); + expect(new Set(result.tools.map((tool) => tool.name)).size).toBe(135); expect(result.tools.map((tool) => tool.name)).toEqual( expect.arrayContaining([ "list_articles", @@ -88,7 +88,7 @@ describe("FrontLit MCP server", () => { "review_documentation_improvement", "create_documentation_improvement_draft", "list_product_updates", - "connect_github_product_updates", + "publish_product_update", "get_inbox_ai_settings", "update_inbox_ai_settings", ]), @@ -100,6 +100,14 @@ describe("FrontLit MCP server", () => { "list_changelog_entries", ]), ); + for (const removed of [ + "connect_github_product_updates", + "get_github_product_updates_integration", + "disconnect_github_product_updates", + "import_github_releases", + ]) { + expect(result.tools.map((tool) => tool.name)).not.toContain(removed); + } expect(result.ttlMs).toBe(300_000); expect(result.cacheScope).toBe("private"); for (const tool of result.tools) { diff --git a/apps/api/src/mcp/server.ts b/apps/api/src/mcp/server.ts index 5740a789..7f6e0bb1 100644 --- a/apps/api/src/mcp/server.ts +++ b/apps/api/src/mcp/server.ts @@ -10,7 +10,6 @@ import { registerPageTools } from "./tools/pages"; import { registerThemeTools } from "./tools/themes"; import { registerMediaTools } from "./tools/media"; import { registerDocumentTools } from "./tools/documents"; -import { registerProductUpdateTools } from "./tools/changelogs"; import { registerEmailTools } from "./tools/email"; import { registerDocumentationTools } from "./tools/documentation"; import { registerDocumentationGitHubTools } from "./tools/documentation-github"; @@ -31,7 +30,6 @@ export function registerAllTools(tools: McpToolRegistrar): void { registerDocumentTools(tools, "blog", "article", articleMetaSchema); registerDocumentTools(tools, "doc", "documentation_page", documentationMetaSchema); registerDocumentTools(tools, "changelog", "product_update", productUpdateMetaSchema); - registerProductUpdateTools(tools); registerEmailTools(tools); registerDocumentationTools(tools); registerDocumentationGitHubTools(tools); diff --git a/apps/api/src/mcp/tools/changelogs.ts b/apps/api/src/mcp/tools/changelogs.ts deleted file mode 100644 index bb0d6758..00000000 --- a/apps/api/src/mcp/tools/changelogs.ts +++ /dev/null @@ -1,177 +0,0 @@ -import type { McpToolRegistrar } from "../tool-registry"; -import { z } from "zod"; -import { successMessageSchema } from "@frontlit/api-contract"; -import { - deleteIntegration, - getGithubIntegration, - upsertGithubIntegration, -} from "../../integrations/queries"; -import { listReleases, markdownToTipTap } from "../../integrations/github"; -import { - createDocumentWithContent, - findChangelogByGithubReleaseId, -} from "../../documents/queries"; -import { AUTH_ERROR, INTERNAL_ERROR, jsonResult } from "./responses"; -import { getTeamId } from "./auth"; - -const githubIntegrationSchema = z.object({ - connected: z.boolean(), - repo: z.string().nullable(), -}); - -/** GitHub Releases integration for product updates. The generic document - * tool registration covers standard CRUD/draft-publish; this file adds the - * integration-specific tools. */ -export function registerProductUpdateTools(server: McpToolRegistrar): void { - server.registerTool( - "connect_github_product_updates", - { - description: - "Connects a GitHub repo (via a fine-grained personal access token) for product-update import.", - inputSchema: { - token: z.string().min(1), - repo: z.string().min(1).describe("owner/repo, e.g. courselit/courselit"), - }, - outputSchema: githubIntegrationSchema, - annotations: { - readOnlyHint: false, - destructiveHint: false, - openWorldHint: false, - }, - }, - async (args: any, extra: any) => { - const teamId = getTeamId(extra); - if (!teamId) return AUTH_ERROR; - try { - await upsertGithubIntegration(teamId, args); - return jsonResult({ connected: true, repo: args.repo }); - } catch { - return INTERNAL_ERROR; - } - }, - ); - - server.registerTool( - "get_github_product_updates_integration", - { - description: "Gets the current team's GitHub product-updates integration status.", - inputSchema: {}, - outputSchema: githubIntegrationSchema, - annotations: { - readOnlyHint: true, - idempotentHint: true, - openWorldHint: false, - }, - }, - async (_args: any, extra: any) => { - const teamId = getTeamId(extra); - if (!teamId) return AUTH_ERROR; - try { - const integration = await getGithubIntegration(teamId); - return jsonResult({ - connected: Boolean(integration), - repo: integration?.repo ?? null, - }); - } catch { - return INTERNAL_ERROR; - } - }, - ); - - server.registerTool( - "disconnect_github_product_updates", - { - description: "Disconnects the current team's GitHub product-updates integration.", - inputSchema: {}, - outputSchema: successMessageSchema, - annotations: { - readOnlyHint: false, - destructiveHint: true, - openWorldHint: false, - }, - }, - async (_args: any, extra: any) => { - const teamId = getTeamId(extra); - if (!teamId) return AUTH_ERROR; - try { - await deleteIntegration(teamId, "github"); - return jsonResult({ - message: "GitHub integration disconnected.", - }); - } catch { - return INTERNAL_ERROR; - } - }, - ); - - server.registerTool( - "import_github_releases", - { - description: - "Lists Releases from the connected GitHub repo and creates one draft product update per release, idempotent by release id. Nothing is auto-published — review and publish each update.", - inputSchema: {}, - outputSchema: z.object({ - imported: z.number(), - skipped: z.number(), - }), - annotations: { - readOnlyHint: false, - destructiveHint: false, - openWorldHint: true, - }, - }, - async (_args: any, extra: any) => { - const teamId = getTeamId(extra); - if (!teamId) return AUTH_ERROR; - try { - const integration = await getGithubIntegration(teamId); - if (!integration) - return { - content: [ - { - type: "text" as const, - text: "No GitHub integration connected for this team.", - }, - ], - isError: true, - }; - - const releases = await listReleases(integration.token, integration.repo); - let imported = 0; - let skipped = 0; - - for (const release of releases) { - if (release.draft) { - skipped++; - continue; - } - const existing = await findChangelogByGithubReleaseId(teamId, release.id); - if (existing) { - skipped++; - continue; - } - await createDocumentWithContent("changelog", { - teamId, - title: release.name || release.tag_name, - content: markdownToTipTap(release.body || ""), - meta: { - version: release.tag_name, - labels: release.prerelease ? ["prerelease"] : [], - releaseDate: release.published_at ?? undefined, - github: { - releaseId: release.id, - releaseUrl: release.html_url, - tagName: release.tag_name, - }, - }, - }); - imported++; - } - - return jsonResult({ imported, skipped }); - } catch { - return INTERNAL_ERROR; - } - }, - ); -} diff --git a/apps/api/src/public/routes.ts b/apps/api/src/public/routes.ts index 1e87d45e..fe9be413 100644 --- a/apps/api/src/public/routes.ts +++ b/apps/api/src/public/routes.ts @@ -666,22 +666,26 @@ const impl = s.router(contract.public, { if (!doc) return { status: 404, body: { error: "Not found" } }; return { status: 200, body: toPublicDocumentationProjection(doc) }; }, - listProductUpdates: async ({ params, query }) => { - const team = await findTeamByPublicId(params.teamId); - if (!team) return { status: 404, body: { error: "Team not found" } }; + listProductUpdates: { + // Same cast as `searchDocumentation`: ts-rest types middleware by the route's query. + middleware: [publicReadLimiter as any], + handler: async ({ params, query }) => { + const team = await findTeamByPublicId(params.teamId); + if (!team) return { status: 404, body: { error: "Team not found" } }; - const [items, total] = await Promise.all([ - listPublishedDocuments("changelog", { - teamId: team.id, - offset: query.offset, - itemsPerPage: query.itemsPerPage, - }), - countPublishedDocuments("changelog", { teamId: team.id }), - ]); - return { - status: 200, - body: { items: items.map((item) => toPublicDocumentProjection(item)), total }, - }; + const [items, total] = await Promise.all([ + listPublishedDocuments("changelog", { + teamId: team.id, + offset: query.offset, + itemsPerPage: query.itemsPerPage, + }), + countPublishedDocuments("changelog", { teamId: team.id }), + ]); + return { + status: 200, + body: { items: items.map((item) => toPublicDocumentProjection(item)), total }, + }; + }, }, getProductUpdateBySlug: async ({ params }) => { const team = await findTeamByPublicId(params.teamId); diff --git a/apps/sites/AGENTS.md b/apps/sites/AGENTS.md index 643577df..af3b7c84 100644 --- a/apps/sites/AGENTS.md +++ b/apps/sites/AGENTS.md @@ -1,6 +1,6 @@ -# This is NOT the Next.js you know +## This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. diff --git a/apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.test.ts b/apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.test.ts new file mode 100644 index 00000000..4488804e --- /dev/null +++ b/apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, vi } from "vitest"; + +const entry = (slug: string, overrides: Record = {}) => ({ + documentId: `doc_${slug}`, + slug, + title: `Entry ${slug}`, + excerpt: null, + content: { + type: "doc", + content: [ + { type: "paragraph", content: [{ type: "text", text: `Body of ${slug} ` }] }, + ], + }, + featuredImage: null, + meta: { labels: [] }, + publishedAt: "2026-11-08T23:30:00.000Z", + updatedAt: null, + ...overrides, +}); + +vi.mock("@/lib/api", () => ({ + getSettings: async () => ({ title: "Acme", subtitle: null }), + getCanonicalHost: async () => "acme.com", + listProductUpdates: async () => ({ + items: [ + entry("dark-mode", { meta: { labels: ["added"], releaseDate: "2026-11-07" } }), + entry("faster-exports", { excerpt: "Exports are 2x faster." }), + ], + total: 2, + }), +})); +vi.mock("@/lib/docs-tree", () => ({})); + +const { GET } = await import("./route"); + +describe("changelog RSS feed", () => { + it("gives every entry its own anchor link and guid", async () => { + const xml = await ( + await GET(new Request("https://acme.com/changelog/rss.xml"), { + params: Promise.resolve({ teamId: "team_1" }), + }) + ).text(); + + expect(xml).toContain("https://acme.com/changelog#dark-mode"); + expect(xml).toContain("https://acme.com/changelog#faster-exports"); + // Release date wins over the publish date; date-only values are UTC days. + expect(xml).toContain("Sat, 07 Nov 2026 00:00:00 GMT"); + expect(xml).toContain("Sun, 08 Nov 2026 00:00:00 GMT"); + // Excerpt when set, otherwise an escaped summary of the body. + expect(xml).toContain("Exports are 2x faster."); + expect(xml).toContain("Body of dark-mode <b>"); + }); +}); diff --git a/apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.ts b/apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.ts index e609e82c..c58262ef 100644 --- a/apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.ts +++ b/apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.ts @@ -1,4 +1,6 @@ +import { changelogDate, publicContentPath } from "@frontlit/api-contract"; import { getCanonicalHost, getSettings, listProductUpdates } from "@/lib/api"; +import { plainTextSummary } from "@/lib/markdown"; import { buildRssFeed } from "@/lib/rss"; export async function GET( @@ -20,9 +22,10 @@ export async function GET( siteUrl: `https://${canonicalHost}/changelog`, items: items.map((entry) => ({ title: entry.title ?? entry.slug, - url: `https://${canonicalHost}/changelog`, - description: entry.excerpt, - pubDate: entry.publishedAt, + // Each entry is an anchor on /changelog; the URL doubles as its guid. + url: `https://${canonicalHost}${publicContentPath("changelog", entry.slug)}`, + description: entry.excerpt || plainTextSummary(entry.content) || null, + pubDate: changelogDate(entry), })), }); diff --git a/apps/sites/components/content-feed.tsx b/apps/sites/components/content-feed.tsx index 0ddb5e46..3de47679 100644 --- a/apps/sites/components/content-feed.tsx +++ b/apps/sites/components/content-feed.tsx @@ -1,11 +1,13 @@ import { getDocumentationSnapshot } from "@/lib/api"; import type { DataSlotLoader } from "@frontlit/page-builder/data-slots"; import { + CHANGELOG_PAGE_SIZE, getCanonicalHost, getSettings, listArticles, listProductUpdates, } from "@/lib/api"; +import { publicContentPath } from "@frontlit/api-contract"; import { changelogJsonLd, serializeJsonLd } from "@/lib/json-ld"; import { ThemedArticleFeed, @@ -71,7 +73,8 @@ async function DocumentationFeed({ async function ProductUpdatesFeed({ teamId }: { teamId: string }) { const [settings, { items }, canonicalHost] = await Promise.all([ getSettings(teamId), - listProductUpdates(teamId, { itemsPerPage: 100 }), + // The whole history renders on one page so every entry's anchor keeps working. + listProductUpdates(teamId, { itemsPerPage: CHANGELOG_PAGE_SIZE }), getCanonicalHost(teamId), ]); @@ -86,7 +89,7 @@ async function ProductUpdatesFeed({ teamId }: { teamId: string }) { url: `https://${canonicalHost}/changelog`, entries: items.map((entry) => ({ name: entry.title ?? entry.slug, - url: `https://${canonicalHost}/changelog`, + url: `https://${canonicalHost}${publicContentPath("changelog", entry.slug)}`, })), }), )} diff --git a/apps/sites/components/themed-content-feed.tsx b/apps/sites/components/themed-content-feed.tsx index b523ee6e..ea148a23 100644 --- a/apps/sites/components/themed-content-feed.tsx +++ b/apps/sites/components/themed-content-feed.tsx @@ -11,6 +11,8 @@ import { Text2, } from "@frontlit/page-builder/primitives/server"; import { getTeamThemeStyle } from "@/lib/team-theme"; +import { changelogDate } from "@frontlit/api-contract"; +import { Link as LinkIcon } from "lucide-react"; import type { ThemeStyle } from "@frontlit/page-builder/models"; function FeedHeading({ @@ -122,16 +124,30 @@ export async function ThemedArticleFeed({ ); } -const LABEL_VARIANT: Record< +const LABELS: Record< string, - "default" | "secondary" | "destructive" | "outline" + { text: string; variant: "default" | "secondary" | "destructive" | "outline" } > = { - added: "default", - fixed: "secondary", - improved: "outline", - removed: "destructive", + added: { text: "Added", variant: "default" }, + changed: { text: "Changed", variant: "outline" }, + deprecated: { text: "Deprecated", variant: "outline" }, + removed: { text: "Removed", variant: "destructive" }, + fixed: { text: "Fixed", variant: "secondary" }, + security: { text: "Security", variant: "destructive" }, }; +/** Formats a `YYYY-MM-DD` calendar date without shifting it into the + * server's time zone (a plain `new Date()` would show Nov 6 for 2026-11-07 + * west of UTC). */ +function formatCalendarDate(value: string) { + return new Date(`${value}T00:00:00Z`).toLocaleDateString(undefined, { + year: "numeric", + month: "long", + day: "numeric", + timeZone: "UTC", + }); +} + export async function ThemedProductUpdatesFeed({ teamId, title, @@ -145,10 +161,11 @@ export async function ThemedProductUpdatesFeed({ jsonLd: string; items: Array<{ documentId: string; + slug: string; title: string | null; content: TextEditorContent | null; publishedAt: string | null; - meta: { version?: string | null; labels: string[] }; + meta: { version?: string | null; labels: string[]; releaseDate?: string | null }; }>; }) { const theme = await getTeamThemeStyle(teamId); @@ -160,40 +177,52 @@ export async function ThemedProductUpdatesFeed({ No changelog entries published yet. ) : (
    - {items.map((entry) => ( -
  1. - -
    - {entry.meta.version && ( - {entry.meta.version} - )} - {entry.publishedAt && ( - - - - )} - {entry.meta.labels.map((label) => ( - - {label} - - ))} -
    - - {entry.title} - - {entry.content && ( -
    - + {items.map((entry) => { + const date = changelogDate(entry); + return ( +
  2. + +
    + {entry.meta.version && ( + {entry.meta.version} + )} + {date && ( + + + + )} + {entry.meta.labels.map((label) => ( + + {LABELS[label]?.text ?? label} + + ))}
    - )} -
  3. - ))} + {/* The entry title is its permalink (`/changelog#`). */} + + + {entry.title} + + + + {entry.content && ( +
    + +
    + )} + + ); + })}
)} diff --git a/apps/sites/lib/api.ts b/apps/sites/lib/api.ts index 493f6574..27b95e23 100644 --- a/apps/sites/lib/api.ts +++ b/apps/sites/lib/api.ts @@ -396,6 +396,10 @@ export interface ProductUpdateMeta { seo?: { title?: string; description?: string }; } +/** The public API's max page size; the changelog renders its full history on + * one page, so this is also the most entries a changelog shows. */ +export const CHANGELOG_PAGE_SIZE = 1000; + export async function listProductUpdates( teamId: string, params: { offset?: number; itemsPerPage?: number } = {}, diff --git a/apps/sites/lib/markdown.test.ts b/apps/sites/lib/markdown.test.ts index 26d23012..2a0192fd 100644 --- a/apps/sites/lib/markdown.test.ts +++ b/apps/sites/lib/markdown.test.ts @@ -3,7 +3,8 @@ import { describe, expect, it, vi } from "vitest"; vi.mock("./api", () => ({})); vi.mock("./docs-tree", () => ({})); -const { buildLlmsTxt, notFoundMarkdown, tiptapToMarkdown } = await import("./markdown"); +const { buildLlmsTxt, notFoundMarkdown, plainTextSummary, tiptapToMarkdown } = + await import("./markdown"); describe("buildLlmsTxt", () => { const settings = { @@ -98,3 +99,27 @@ describe("markdown bodies", () => { ).toBe("## Setup\n\nInstall it."); }); }); + +describe("plainTextSummary", () => { + const doc = (text: string) => ({ + type: "doc", + content: [ + { type: "heading", content: [{ type: "text", text: "Added" }] }, + { type: "paragraph", content: [{ type: "text", text }] }, + ], + }); + + it("flattens a TipTap body to one line", () => { + expect(plainTextSummary(doc("Dark mode."))).toBe("Added Dark mode."); + }); + + it("truncates long bodies with an ellipsis", () => { + const summary = plainTextSummary(doc("x".repeat(400)), 20); + expect(summary).toHaveLength(20); + expect(summary.endsWith("…")).toBe(true); + }); + + it("returns an empty string for empty content", () => { + expect(plainTextSummary(null)).toBe(""); + }); +}); diff --git a/apps/sites/lib/markdown.ts b/apps/sites/lib/markdown.ts index 7e7c3945..d0377470 100644 --- a/apps/sites/lib/markdown.ts +++ b/apps/sites/lib/markdown.ts @@ -1,7 +1,8 @@ -import type { ApiRichText } from "@frontlit/api-contract"; +import { changelogDate, type ApiRichText } from "@frontlit/api-contract"; import { layoutToMarkdown } from "@frontlit/page-builder/schema"; import { serializeDocsMarkdown } from "@frontlit/text-editor/docs"; import { + CHANGELOG_PAGE_SIZE, getApiOperation, getApiReference, getArticleBySlug, @@ -34,6 +35,13 @@ export function tiptapToMarkdown(content: unknown): string { } } +/** First `max` characters of a TipTap body as one line of plain text, for + * feed summaries (RSS ``). */ +export function plainTextSummary(content: unknown, max = 300): string { + const text = plainText(content).replace(/\s+/g, " ").trim(); + return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text; +} + function plainText(node: unknown): string { if (!node || typeof node !== "object") return ""; const { text, content, type } = node as { @@ -80,13 +88,12 @@ async function blogIndex(teamId: string) { } async function changelogFeed(teamId: string) { - const { items } = await listProductUpdates(teamId, { itemsPerPage: 1000 }); + const { items } = await listProductUpdates(teamId, { + itemsPerPage: CHANGELOG_PAGE_SIZE, + }); return items .map((update) => { - const label = [ - update.meta.version, - date(update.meta.releaseDate ?? update.publishedAt), - ] + const label = [update.meta.version, changelogDate(update)] .filter(Boolean) .join(", "); return [ diff --git a/apps/sites/lib/rss.ts b/apps/sites/lib/rss.ts index d2d4d6df..29ab45d5 100644 --- a/apps/sites/lib/rss.ts +++ b/apps/sites/lib/rss.ts @@ -5,10 +5,9 @@ function escapeXml(value: string): string { export interface RssItem { title: string; url: string; - /** Plain-text summary (an entry's `excerpt`), emitted as - * ``. Full-body rendering is skipped: a Route Handler - * can't import React's server renderer, and the TipTap JSON body has - * no paragraph structure once flattened to plain text anyway. */ + /** Plain-text summary (an entry's `excerpt`, or the start of its body), + * emitted as ``. Full-body HTML is skipped: a Route Handler + * can't import React's server renderer. */ description: string | null; pubDate: string | null; } diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md index 403a9310..0a7947cc 100644 --- a/apps/web/AGENTS.md +++ b/apps/web/AGENTS.md @@ -1,6 +1,6 @@ -# This is NOT the Next.js you know +## This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. diff --git a/apps/web/components/content/editors/product-update-editor.tsx b/apps/web/components/content/editors/product-update-editor.tsx index dd34483e..7b77b65a 100644 --- a/apps/web/components/content/editors/product-update-editor.tsx +++ b/apps/web/components/content/editors/product-update-editor.tsx @@ -3,9 +3,16 @@ import { use, useEffect, useRef, useState } from "react"; import { Editor } from "@frontlit/text-editor"; import type { TextEditorContent } from "@frontlit/text-editor"; +import { ExternalLink } from "lucide-react"; +import { + productUpdateLabels, + publicContentPath, + type ProductUpdateLabel, +} from "@frontlit/api-contract"; import { discardProductUpdateDraft, getAiConfig, + getDomainSettings, getProductUpdate, publishProductUpdate, updateProductUpdate, @@ -15,14 +22,18 @@ import { FullScreenLoading } from "@/components/workspace/loading"; import { SlugEditor } from "@/components/workspace/slug-editor"; import { WritingEditorDocumentHeader } from "@/components/workspace/writing-editor-document-header"; import { WritingEditorShell } from "@/components/workspace/writing-editor-shell"; -import { Badge } from "@/components/ui/codelit/badge"; +import { IconButton } from "@/components/ui/codelit/icon-button"; import { Input } from "@/components/ui/codelit/input"; import { Label } from "@/components/ui/codelit/label"; import { Toggle } from "@/components/ui/toggle"; import { ContentIndexSettings } from "@/components/workspace/content-index-settings"; import { FeaturedImageSetting } from "@/components/workspace/featured-image-setting"; - -const LABEL_OPTIONS = ["added", "fixed", "improved", "removed"] as const; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/codelit/tooltip"; +import { siteUrl } from "@/lib/site-url"; export default function ProductUpdateEditor({ params, @@ -39,6 +50,7 @@ export default function ProductUpdateEditor({ const [settingsOpen, setSettingsOpen] = useState(false); const [resetKey, setResetKey] = useState(0); const [teamIndexesChangelog, setTeamIndexesChangelog] = useState(false); + const [liveHost, setLiveHost] = useState(null); const titleDebounceRef = useRef | null>(null); const versionDebounceRef = useRef | null>(null); const releaseDateDebounceRef = useRef | null>(null); @@ -56,6 +68,9 @@ export default function ProductUpdateEditor({ setTeamIndexesChangelog(Boolean(config?.indexContent?.changelog)), ) .catch(() => setTeamIndexesChangelog(false)); + getDomainSettings() + .then((settings) => setLiveHost(settings.canonicalHost)) + .catch(() => setLiveHost(null)); }, [documentId]); useEffect( @@ -117,7 +132,7 @@ export default function ProductUpdateEditor({ ); } - async function handleLabelToggle(label: string) { + async function handleLabelToggle(label: ProductUpdateLabel) { const current = entry?.draftMeta.labels ?? []; const labels = current.includes(label) ? current.filter((item) => item !== label) @@ -177,11 +192,15 @@ export default function ProductUpdateEditor({ if (!entry) return ; + const liveUrl = + entry.status !== "draft" && liveHost + ? `${siteUrl(liveHost)}${publicContentPath("changelog", entry.slug)}` + : null; + return ( + + + + + + + + View live entry + + ) : undefined + } settings={
- {entry.draftMeta.github && ( - Imported from GitHub - )} @@ -228,13 +263,21 @@ export default function ProductUpdateEditor({ value={releaseDate} onChange={(event) => handleReleaseDateChange(event.target.value)} className="mt-1" + aria-describedby="changelog-release-date-hint" data-testid="changelog-release-date" /> +

+ Shown on the changelog and used to order entries. If empty, the publish + date is used. +

- +
- {LABEL_OPTIONS.map((label) => ( + {productUpdateLabels.map((label) => ( (null); const [creating, setCreating] = useState(false); - const [github, setGithub] = useState(null); - const [repo, setRepo] = useState(""); - const [token, setToken] = useState(""); - const [connecting, setConnecting] = useState(false); - const [importing, setImporting] = useState(false); - const [importResult, setImportResult] = useState(null); function refresh() { return listProductUpdates().then(({ items }) => setEntries(items)); } - function refreshGithub() { - return getGithubIntegration().then(setGithub); - } - useEffect(() => { - Promise.all([refresh(), refreshGithub()]).finally(() => setLoading(false)); + refresh().finally(() => setLoading(false)); }, []); async function handleCreate(e: React.FormEvent) { @@ -89,47 +72,6 @@ export default function ChangelogPage() { } } - async function handleConnectGithub(e: React.FormEvent) { - e.preventDefault(); - if (!repo.trim() || !token.trim()) return; - setConnecting(true); - try { - setGithub( - await connectGithubIntegration({ - repo: repo.trim(), - token: token.trim(), - }), - ); - setRepo(""); - setToken(""); - } catch (err) { - toast.error(err instanceof ApiError ? err.message : "Failed to connect GitHub."); - } finally { - setConnecting(false); - } - } - - async function handleDisconnectGithub() { - await disconnectGithubIntegration(); - setGithub({ connected: false, repo: null }); - } - - async function handleImport() { - setImporting(true); - setImportResult(null); - try { - const result = await importGithubReleases(); - setImportResult( - `Imported ${result.imported}, skipped ${result.skipped} (already imported).`, - ); - await refresh(); - } catch (err) { - toast.error(err instanceof ApiError ? err.message : "Failed to import releases."); - } finally { - setImporting(false); - } - } - if (loading) { return ( @@ -196,7 +138,7 @@ export default function ChangelogPage() {

- /changelog/{entry.slug} · updated{" "} + /changelog#{entry.slug} · updated{" "} {entry.updatedAt ? new Date(entry.updatedAt).toLocaleString() : "—"} @@ -210,62 +152,6 @@ export default function ChangelogPage() { ))}

- - - - GitHub integration - - - {github?.connected ? ( -
-

- Connected to{" "} - {github.repo} -

-
- - -
- {importResult && ( -

{importResult}

- )} -
- ) : ( -
-

- Connect a repo with a fine-grained personal access token (read-only - Contents access is enough) to import Releases as draft changelog - entries. -

-
- - setRepo(e.target.value)} - placeholder="owner/repo" - /> -
-
- - setToken(e.target.value)} - type="password" - /> -
- -
- )} -
-
diff --git a/apps/web/lib/api.ts b/apps/web/lib/api.ts index 017c4bdd..000ffd9b 100644 --- a/apps/web/lib/api.ts +++ b/apps/web/lib/api.ts @@ -1,5 +1,5 @@ import { initClient } from "@ts-rest/core"; -import { contract } from "@frontlit/api-contract"; +import { contract, type ProductUpdateLabel } from "@frontlit/api-contract"; import type { TextEditorContent } from "@frontlit/text-editor"; import type { WidgetInstance } from "@frontlit/page-builder/models"; import { ApiError } from "./api-client"; @@ -816,9 +816,9 @@ export function searchDocumentation(q: string, k?: number) { export interface ProductUpdateMeta { version?: string; - labels: string[]; + labels: ProductUpdateLabel[]; + /** `YYYY-MM-DD`; the changelog falls back to the publish date. */ releaseDate?: string; - github?: { releaseId: number; releaseUrl: string; tagName: string }; indexable?: boolean; } @@ -896,33 +896,6 @@ export function deleteProductUpdate(documentId: string) { return unwrap(client.content.productUpdates.remove({ params: { documentId } })); } -export interface GithubIntegration { - connected: boolean; - repo: string | null; -} - -export function getGithubIntegration() { - return unwrap( - client.content.productUpdates.getGithubIntegration(), - ); -} - -export function connectGithubIntegration(input: { token: string; repo: string }) { - return unwrap( - client.content.productUpdates.connectGithub({ body: input }), - ); -} - -export function disconnectGithubIntegration() { - return unwrap(client.content.productUpdates.disconnectGithub()); -} - -export function importGithubReleases() { - return unwrap<{ imported: number; skipped: number }>( - client.content.productUpdates.importGithubReleases(), - ); -} - // ---- Email (SendLit, read-through) ------------------------------------ export interface Contact { diff --git a/docs/architecture.md b/docs/architecture.md index 503b3913..dbce5417 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -5,8 +5,7 @@ Status: **Living architecture** · Last verified: 2026-07-29 This document designs the core content features of FrontLit — the "front office of your SaaS": draft/publish pages, blog (`/blog`), documentation (`/docs`) with pgvector-backed search, a help desk (`/help`), an -embeddable RAG chatbot, a changelog (`/changelog`) with GitHub -integration, and email (lead capture on pages + sending via +embeddable RAG chatbot, a changelog (`/changelog`), and email (lead capture on pages + sending via the hosted SendLit REST API). > **Route naming**: `/docs` hosts the documentation (help-center articles, @@ -240,7 +239,6 @@ does not collide with the customer-facing `/docs` route in `apps/sites`. | Documentation sections (§5) | `POST/GET/PATCH/DELETE /content/documentation/sections…` | `list/create/update/delete_documentation_section` | | Documentation search (§6) | `GET /content/documentation/search?q=` (authed; public variant below `/public/…`) | `search_documentation` (semantic search over published documentation) | | Content: product updates (§8) | `POST/GET/PATCH/DELETE /content/product-updates…`, `…/publish`, `…/discard-draft` | `list/create/get/update/delete_product_update`, `publish_product_update`, `discard_product_update_draft` | -| GitHub import (§8) | `POST /content/product-updates/import-github` | `import_github_releases` | | Chatbot settings (§7) | `GET/PATCH /chatbot` | `get_chatbot_settings`, `update_chatbot_settings` | | Team settings (§12.5) | `GET/PATCH /settings` (branding + code injection; the shared shell moves through page publish) | `get_team_settings`, `update_team_settings` | | Ask the docs (§7) | `POST /public/:teamId/chat` (public, persisted thread) | — (visitor widget only for now; no MCP twin yet) | @@ -486,7 +484,7 @@ jsonb so the core engine stays generic): | _all kinds_ | `seo?: { title?, description? }` — optional overrides for public metadata (§12.3) | | `blog` | `tags: string[]` | | `doc` | `sectionId: string`, `order: number` (help-center nav tree) | -| `changelog` | `version?: string`, `labels: ("added"\|"fixed"\|"improved"\|"removed")[]`, `releaseDate: string`, `github?: { releaseId, tag, htmlUrl }` | +| `changelog` | `version?: string` (≤ 64), `labels: ("added"\|"changed"\|"deprecated"\|"removed"\|"fixed"\|"security")[]`, `releaseDate?: "YYYY-MM-DD"` | Help-center sections get a small companion table: @@ -745,26 +743,22 @@ helper (`streamChatAnswer`) exists for a future SSE path: --- -## 8. Changelog & GitHub integration +## 8. Changelog Changelog entries are `documents` with `kind: "changelog"` — the whole -draft/publish/authoring stack comes for free. `/changelog` renders the -published feed (grouped by month, label badges, optional version tags), plus -an RSS feed at `/changelog/rss.xml` served by `apps/sites` (§12.3). - -### 8.1 GitHub integration (standard path) - -Per-team connection stored in a generic `integrations` table -(`provider = "github"`, encrypted `config` jsonb: installation/token, `repo`): - -- **v1 (ship first)**: connect with a fine-grained PAT + repo name. - `POST /content/product-updates/import-github` lists GitHub **Releases** and creates one - _draft_ product update per release (release notes markdown → TipTap JSON - via a small converter in the API). Idempotent by `meta.github.releaseId`. - Nothing is auto-published — a human reviews and publishes. -- **v2**: proper GitHub App (install flow, webhook on `release: published` - creating drafts automatically; optional LLM pass that rewrites raw release - notes into user-facing prose). +draft/publish/authoring stack comes for free. They are written in the editor +or drafted by an agent through MCP (`create_product_update`) and published by +a person. There is no GitHub integration for the changelog; see +[changelog-management.md](changelog-management.md) for the decision and +when to revisit it. + +`/changelog` renders every published entry on one page, newest first by +`releaseDate ?? publishedAt` (`changelogDate()` in the API contract), with +Keep a Changelog type badges and optional version. Each entry is the anchor +`/changelog#` (`publicContentPath`), which the RSS feed at +`/changelog/rss.xml` (§12.3), JSON-LD, and AI citations all link to. +Changelog chunks carry their release ("Title — 1.2.0, 2026-11-07") so +retrieval never presents an old change without its date. --- @@ -898,7 +892,7 @@ calls SendLit's `POST /provisioning/teams` with id — idempotent find-or-create, exactly how FrontLit's own provisioning endpoint serves _its_ consumers. The returned `apiKey` (only present on the creating call) is stored encrypted in the `integrations` table -(`provider: "sendlit"`, alongside `github` from §8.1). Creation-time +(`provider: "sendlit"`, its only provider). Creation-time failures enqueue a `provision_sendlit` outbox job (§10.5). Teams created while `SENDLIT_API_URL` is unset are skipped silently — there is no startup backfill — so the 1:1 mapping holds only when SendLit was @@ -1900,8 +1894,7 @@ the feature, not in a later pass. `search_documentation` tool. 5. **Chatbot** — settings routes/tools, persisted RAG chat endpoint (public widget only; no MCP ask twin yet), widget + embed loader. -6. **Product-update behavior** — update routes/tools, RSS, GitHub - release import (v1 PAT) + `import_github_releases` tool. +6. **Product-update behavior** — update routes/tools, RSS. 7. **Email** — eager per-team SendLit provisioning with `provision_sendlit` retry on the §10.5 outbox, typed REST client (`@sendlit/api-contract`), public capture endpoint wired into the diff --git a/docs/changelog-management.md b/docs/changelog-management.md new file mode 100644 index 00000000..65fdf641 --- /dev/null +++ b/docs/changelog-management.md @@ -0,0 +1,221 @@ +# FrontLit Changelog Management — Implementation Plan + +Status: implemented (2026-11-07). +Scope: authoring, publishing, and delivering product updates (internal kind `changelog`, contract name `productUpdates`, public route `/changelog`). + +## 1. Decisions + +1. **The editor and API/MCP are the only authoring path.** Entries are drafted in the dashboard editor or created as drafts by an agent through MCP, then published by a person. FrontLit does not integrate with GitHub for changelogs. +2. **Automation lives on the customer's side.** A team's coding agent (Claude Code, Cursor, …) already has `git log`, `gh`, and the PRs. With FrontLit's MCP server connected, it drafts entries using the existing `create_product_update` tool. FrontLit needs no webhooks, LLM pipeline, or repository access for this. +3. **Keep the existing model.** An entry is a `documents` row (`kind: "changelog"`) with a title, body, optional `version`, optional `releaseDate`, and optional labels. Draft/publish, slugs, AI indexing, and MCP are unchanged. +4. **Follow [Keep a Changelog](https://keepachangelog.com/) where it is cheap.** Newest first, each entry dated and linkable, and labels limited to its six change types. The body structure is not enforced. +5. **One public page.** Each entry has an anchor at `/changelog#` (as Mintlify does). There are no pages for individual entries. + +### Why GitHub is removed + +- Most FrontLit customers deploy continuously and do not cut GitHub Releases, so syncing Releases would do nothing for them. FrontLit itself has none (`createGithubReleases: false` in `.github/workflows/publish-packages.yaml`). +- There is little evidence of demand for hosted pages generated from `CHANGELOG.md`. Mintlify, Fern, and ReadMe do not parse it. +- The current import stores a GitHub personal access token per team. Deleting it removes stored secrets and an outbound API path. + +### Non-goals + +- GitHub Releases sync, `CHANGELOG.md` or folder sources, or a Mintlify importer +- Pages for individual entries, Open Graph images, or pagination of the public page +- Label or tag filters on the public page +- Email notifications, an in-app "What's new" widget, or scheduled publishing +- Enforcing Keep a Changelog body structure (sections, Unreleased) + +Each can be added later without reworking this design. + +## 2. Current state and problems + +| # | Problem | Where | +|---|---|---| +| P1 | Every RSS item uses `/changelog` as both its link and its guid, so feed readers that use the guid as identity dedupe or misshow items. Items also have no body: `description` is `excerpt`, which changelog entries do not set. | `apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.ts` | +| P2 | JSON-LD `ItemList` entries all point to `/changelog`. | `apps/sites/components/content-feed.tsx` | +| P3 | Entries render with no `id`, so `publicContentPath("changelog", slug)` (`/changelog#slug`) used in AI citations links to an anchor that does not exist. | `apps/sites/components/themed-content-feed.tsx`, `packages/api-contract/src/content-paths.ts` | +| P4 | Changelog chunks set `anchor = slugify(heading)` (e.g. `fixed`). `publicContentPath` prefers that over the slug, so citations point to `#fixed`. | `apps/api/src/documentation/chunking.ts:126` | +| P5 | Chunks have no release context, so the bot can present an old fix as current. | `apps/api/src/documentation/chunking.ts`, index job | +| P6 | The feed displays and sorts by `publishedAt` and ignores `releaseDate`. | `themed-content-feed.tsx`, `listPublishedDocuments` in `apps/api/src/documents/queries.ts` | +| P7 | The feed shows at most 100 entries (RSS 50), so older entries, and their anchors, disappear. | `content-feed.tsx` | +| P8 | `releaseDate` and `labels` accept any string, with no format or length limits. The editor offers `added/fixed/improved/removed`, while the import wrote `prerelease`. | `packages/api-contract/src/schemas/product-updates.ts` | +| P9 | The public product-updates list has no `publicReadLimiter` and no upper bound on `itemsPerPage`. Other public reads in the same router use the limiter. | `apps/api/src/public/routes.ts` (`listProductUpdates`) | +| P10 | The GitHub import (personal access token, `listReleases`, `markdownToTipTap`) is spread across REST, MCP, the dashboard, the policy maps, and the `integrations` provider enum. | see §3.2 | + +## 3. Work packages + +Two PRs, followed by docs and dogfooding. Each PR must pass a full browser smoke test before it is considered done (AGENTS.md). + +### 3.1 PR 1 — Correct public changelog + +**Contract (`packages/api-contract`)** + +- `productUpdateMetaSchema`: + - `releaseDate`: `YYYY-MM-DD` only (regex), optional. + - `labels`: enum array of `added | changed | deprecated | removed | fixed | security`, max 6, deduplicated, default `[]`. + - `version`: `max(64)`. + - Remove `github`. +- Public list query for product updates: `itemsPerPage` `max(1000)`. +- Add a shared helper `changelogDate(entry)` that returns `meta.releaseDate ?? publishedAt` as `YYYY-MM-DD`. Every surface uses it so they all show the same date. +- Change `publicContentPath("changelog", slug, anchor)` to **ignore `anchor`** and always return `/changelog#`. Entries are the only linkable unit. + +**API (`apps/api`)** + +- `listPublishedDocuments("changelog")`: order by `coalesce(meta->>'releaseDate', to_char(published_at, 'YYYY-MM-DD')) desc, published_at desc`. Use the published `meta`, never `draftMeta`. Leave other kinds unchanged. +- `listProductUpdates` public route: add `publicReadLimiter` to match the other public reads. +- Chunking for `kind === "changelog"`: + - `anchor = null`, since citations fall back to `#slug`. + - Prefix each chunk with `" — <version>, <date>"` so it carries release context. The index job passes the document's title and meta, because `extractChunks` sees only the content. +- Migration (pre-production, so rewrite it): change `improved` → `changed` and drop unknown labels and `meta.github` from existing changelog rows, in both `meta` and `draftMeta`. + +**Sites (`apps/sites`)** + +- `ThemedProductUpdatesFeed`: + - `id={slug}` on each `<li>`, with `scroll-margin-top` for the sticky header. + - The entry title (`Header4`) is its permalink: an in-page `<a href="#<slug>">` with a decorative link icon on hover, the GitHub/Mintlify heading-anchor convention. No clipboard button, so the feed stays a server component. + - Show `changelogDate`. Format date-only strings in UTC, otherwise `2026-11-07` shows as Nov 6 in US time zones. + - All visible elements stay page-builder primitives. +- Fetch all entries (`itemsPerPage: 1000`) for the feed, Markdown output, and JSON-LD. RSS keeps the newest 50. +- RSS items: + - `link`/`guid`: `https://<host>/changelog#<slug>` + - `pubDate`: `changelogDate` + - `description`: `excerpt` if set, otherwise the first ~300 characters of plain text from the TipTap body (escaped by the existing `escapeXml`) +- JSON-LD: each entry's `url` becomes its anchor URL. +- Markdown (`changelogFeed` in `apps/sites/lib/markdown.ts`): same ordering, `changelogDate`, and a heading of `## <title> (<version>, <date>)`. + +**Dashboard (`apps/web`)** + +- Product-update editor: + - Labels become the six Keep a Changelog types. + - `releaseDate` uses a date input, with a hint that it defaults to the publish date when empty. + - Show the public anchor URL ("View on site") for published entries. + +### 3.2 PR 2 — Remove the GitHub import + +The changelog import and the docs/pages GitHub feature are **separate systems that share only the word "GitHub"**. PR 2 deletes the changelog import and must leave docs/pages GitHub unchanged. + +#### How the two systems differ (verified 2026-11-07) + +| | Docs/pages GitHub (KEEP) | Changelog GitHub import (DELETE) | +|---|---|---| +| Auth | GitHub App installations (`apps/api/src/documentation/github/app.ts`) | Personal access token per team | +| Storage | `documentation_git_*` tables (`documentationGitBindings`, `…Setups`, `…Grants`, `…Jobs`, …) and `gitAssets` | One `integrations` row with `provider = 'github'` (encrypted token and repo) | +| API code | `apps/api/src/documentation/github/*`, mounted in `app.ts` as `docsGitHubRoutes` and `docsGitHubWebhooks` | `apps/api/src/integrations/github.ts`; GitHub helpers in `apps/api/src/integrations/queries.ts`; four handlers in `apps/api/src/documents/changelogs-routes.ts` | +| HTTP routes | `/integrations/github/callback`, `/integrations/github/webhook` (and `/docs/` variants) | `/content/product-updates/github*`, `/content/product-updates/import-github` | +| Contract | `contract.content.documentationGithub` | `contract.content.productUpdates.{connectGithub, getGithubIntegration, disconnectGithub, importGithubReleases}` | +| MCP tools | `*_documentation_github*` (`apps/api/src/mcp/tools/documentation-github.ts`) | `connect_github_product_updates`, `get_github_product_updates_integration`, `disconnect_github_product_updates`, `import_github_releases` | +| Web | Settings → Integrations (reads `getDocumentationGitSource`), `documentation-source.tsx`, `documentation-proposals.tsx`, the `documentationGithub` client functions in `apps/web/lib/api.ts` | The GitHub card in `product-updates-view.tsx`; `getGithubIntegration`, `connectGithubIntegration`, `disconnectGithubIntegration`, `importGithubReleases`, `GithubIntegration` in `apps/web/lib/api.ts` | + +Checked: no docs/pages code imports `apps/api/src/integrations/github.ts` or the GitHub helpers in `integrations/queries.ts`. The Settings → Integrations "GitHub" row reflects the docs/pages connection, not the changelog token. + +#### Delete + +- **Contract:** the four `productUpdates` GitHub endpoints, `githubIntegrationConfigSchema`, `githubIntegrationSchema`, `importGithubReleasesResponseSchema`, and `github` in `productUpdateMetaSchema`. +- **REST:** the four handlers and their authorization entries in `apps/api/src/documents/changelogs-routes.ts`. Drop the route-order comment, which only existed for the literal GitHub routes. +- **MCP:** the four tools in `apps/api/src/mcp/tools/changelogs.ts`. Remove their scope entries (`apps/api/src/mcp/policy.ts:145-148`) and `import_github_releases` from the confirmation list (`policy.ts:241`), and update `apps/api/src/mcp/server.test.ts`. +- **Operation policy:** the four `content.productUpdates.*Github*` entries in `apps/api/src/auth/operation-policy.ts`. +- **Queries:** + - `findChangelogByGithubReleaseId` in `apps/api/src/documents/queries.ts`. + - The GitHub functions in `apps/api/src/integrations/queries.ts` (`getGithubIntegration`, `upsertGithubIntegration`, and `deleteIntegration` if only these callers use it). Keep the SendLit functions and the generic helpers they use. +- **`apps/api/src/integrations/github.ts`** (`listReleases`, `markdownToTipTap`): delete it. Its only consumers are the two changelog files above. +- **Schema and migration:** remove `"github"` from `integrationProviders`, keeping `"sendlit"`, and fix the outdated comment above it. The migration runs only `DELETE FROM integrations WHERE provider = 'github'`. It must not touch any `documentation_git_*` table, `git_assets`, or `sendlit` rows. +- **Web:** the GitHub card and its state in `product-updates-view.tsx`, and the five changelog-specific exports in `apps/web/lib/api.ts`. Leave every `documentationGithub` function and the page-sync `git*` fields untouched. +- **OpenAPI and docs:** remove the product-update GitHub operations from `apps/api/src/openapi.ts`. Update `docs/architecture.md` §8 and its API/MCP tables, and change the "changelog with GitHub…" wording in the README and architecture doc. + +#### Do not touch + +- `apps/api/src/documentation/github/**`, `apps/api/src/pages/git-import.ts`, `apps/api/src/pages/releases.ts` +- `/integrations/github/*` routes (same prefix as the deleted file's directory name, but they are the docs App's routes) +- `apps/api/src/mcp/tools/documentation-github.ts` and the `*_documentation_github*` policy entries +- Settings → Integrations (`?tab=github`), `documentation-source.tsx`, `documentation-proposals.tsx` +- The `integrations` table and SendLit key storage + +**Naming trap.** `apps/api/src/integrations/github.ts` (delete) and the `/integrations/github/*` routes (keep) look related but are not. Delete files and symbols by exact name, not by searching for "github". + +#### Safety checks for PR 2 + +- **Before deleting:** run `rg -n "integrations/github|getGithubIntegration|upsertGithubIntegration|listReleases|markdownToTipTap|findChangelogByGithubReleaseId"`. The only hits should be the files listed under Delete. If anything else matches, stop and reassess. +- **Test suites:** the full API suite, including `apps/api/src/documentation/github/*.test.ts` (setup, webhooks, source, migrations, transports, OAuth), plus `apps/api/scripts/test-docs-postgres.ts` against a real database. All must pass with no changes to those test files. +- **Typecheck and lint** across `apps/api`, `apps/web`, `apps/sites`, and `packages/api-contract`. This catches any remaining caller of removed contract routes. +- **Migration test:** seed one `github` and one `sendlit` `integrations` row plus a docs Git binding. After migrating, only the `github` row is gone. +- **Browser smoke test (docs/pages GitHub must still work):** + 1. Settings → Integrations shows the GitHub row with the correct connected state for a docs-connected team. + 2. Push a docs change to the connected test repo (`rajat1saxena/docs-site` for the `hathi` team). The webhook delivers, sync runs, and the change appears on `/docs`. + 3. If pages sync is enabled, edit a page file and confirm it publishes. + 4. Product updates shows no GitHub card. Creating, editing, and publishing an entry still works. + +### 3.3 Agent recipe and dogfooding + +- Add a short user-facing guide, "Write your changelog with your coding agent": + - connect the FrontLit MCP server with product-updates read and write scopes; + - a sample prompt (below); + - then review and publish in the editor. +- Publish the guide on FrontLit's own docs. +- Dogfooding: the FrontLit team writes its product changelog in the editor, or with Claude Code through MCP, and publishes it to FrontLit's own `/changelog`. + +Sample prompt: + +> List the PRs merged to `main` since the last published FrontLit changelog entry. Skip chores, refactors, CI, and dependency bumps. Draft one product update with a short title, a one-sentence intro, and bullets grouped under Added / Changed / Fixed, written for customers, not engineers. Set labels to the groups used. Create it as a draft with `create_product_update` and do not publish it. + +## 4. Security + +- **Fewer secrets and less outbound access.** PR 2 deletes the changelog's stored GitHub tokens and the code that calls `api.github.com` with them. The migration purges only `integrations` rows with `provider = 'github'`. Docs/pages GitHub App credentials and bindings live in separate tables and are not affected (§3.2). +- **Public read limits.** Add `publicReadLimiter` to the public product-updates list, and cap `itemsPerPage` at 1000. Edge caching (`changelog:<team>` tag) already absorbs most traffic. +- **No draft leakage.** Public list, RSS, JSON-LD, Markdown, and AI indexing read only published columns (`content`, `meta`, `publishedAt`) and filter on `publishedAt IS NOT NULL`. The new sort must use `meta`, not `draftMeta`. Add a test that a draft-only `releaseDate`/label change does not affect public order or output. +- **Input validation.** `releaseDate` uses a strict format, `labels` an enum, `version` a length limit. Slugs are already normalized (`normalizeEditableSlug`) and URL-encoded in `publicContentPath`. +- **Output encoding.** React escapes anchor ids and labels. RSS keeps `escapeXml` for every field, including the new plain-text description. No raw HTML is added. +- **Agent drafts are untrusted input.** PR titles and descriptions the agent summarizes may contain injected instructions. The recipe creates **drafts only**, and a person publishes. This is a convention, not enforced: `publish` uses the same `productUpdatesWrite` scope as `create` (`apps/api/src/auth/operation-policy.ts`). Splitting out a publish scope is deferred until a team asks for it. +- **Authorization unchanged.** Remaining endpoints keep their `content.productUpdates.*` permissions. Removed operations disappear from both the REST and MCP policy maps in the same PR, so no orphaned permission remains. + +## 5. Maintainability + +- **One definition per concept.** + - `publicContentPath` is the only builder of changelog URLs, used by the feed, RSS, JSON-LD, sitemap, and citations. + - `changelogDate` is the only date rule. + - `listPublishedDocuments` holds the only sort. +- **Remove code rather than disable it.** The GitHub import is deleted end to end, not hidden behind a flag. With fewer endpoints, MCP tools, and policies, there's less to keep consistent. +- **No new tables, workers, or integrations.** Changelog stays a thin kind on the shared documents engine. +- **Docs move with the code.** PR 2 updates `docs/architecture.md` §8, its API/MCP tables, and the README. This file records the decisions so the GitHub question is not reopened without new evidence. + +## 6. Testing + +**Unit and integration (`apps/api`, `packages/api-contract`)** + +- Meta schema: valid and invalid `releaseDate`; label enum, dedupe, and max; `version` length. +- `changelogDate`: `releaseDate` wins, falls back to `publishedAt`, and is formatted in UTC. +- `publicContentPath("changelog", slug, "fixed")` returns `/changelog#<slug>`. +- `listPublishedDocuments("changelog")`: ordered by release date, then `publishedAt`; ignores `draftMeta`; excludes unpublished entries. +- Chunking: changelog chunks have `anchor: null` and a release-context prefix. Doc and blog chunks are unchanged. +- Public route: rejects `itemsPerPage` > 1000; rate limiter attached. +- PR 2: the contract, REST router, MCP tool list, and policy maps contain no GitHub product-update operations. The migration removes `github` integration rows and leaves `sendlit` rows. + +**Sites (`apps/sites`)** + +- RSS: unique `guid` per item equal to its anchor URL; `pubDate` from `changelogDate`; description escaped. +- JSON-LD: anchor URL for each entry. +- Markdown: ordering and date match the HTML feed. + +**Browser smoke test (required)** + +1. Create a draft through MCP (`create_product_update`) with labels and a `releaseDate` in the past. Confirm it does not appear on `/changelog`, in `/changelog/rss.xml`, or in `/changelog.md`. +2. Edit it in the dashboard (labels, date input) and publish. +3. On `/changelog`: + - the entry sits at the correct position by release date; + - its date is not shifted by time zone; + - clicking an entry title sets the URL to `/changelog#<slug>`; + - opening that URL scrolls to the entry below the sticky header. +4. RSS shows the item with its anchor link. A feed validator reports unique GUIDs. +5. With changelog indexing enabled, ask the site chatbot about the change. Confirm the citation links to `/changelog#<slug>` and the answer names the version or date. +6. Confirm the dashboard has no GitHub card on product updates, and that the docs GitHub connection in Settings → Integrations still works. + +## 7. Delivery order + +1. **PR 1:** correct public changelog (§3.1). This fixes P1–P9. +2. **PR 2:** remove the GitHub import (§3.2). This fixes P10. It can be developed in parallel with PR 1; merge it second to keep migrations ordered. +3. **Agent recipe and dogfooding** (§3.3): the guide, plus FrontLit's first entries on its own `/changelog`. + +## 8. Revisit when + +- **A paying customer with a versioned product asks for automatic sync:** add GitHub Releases sync through the existing GitHub App, with GitHub-owned entries read-only. Do not bring back personal access tokens. +- **Changelogs grow past a few hundred entries:** add label filters (`?labels=`) and consider pagination with anchor redirects. +- **Teams ask to share individual launches:** add `/changelog/<slug>` pages with Open Graph images. Anchors remain valid. diff --git a/packages/api-contract/src/content-paths.ts b/packages/api-contract/src/content-paths.ts index a79e5376..779ff578 100644 --- a/packages/api-contract/src/content-paths.ts +++ b/packages/api-contract/src/content-paths.ts @@ -7,9 +7,8 @@ export function publicContentPath( ): string { const hash = anchor ? `#${encodeURIComponent(anchor)}` : ""; if (kind === "blog") return `/blog/${encodeURIComponent(slug)}${hash}`; - if (kind === "changelog") { - return `/changelog${hash || `#${encodeURIComponent(slug)}`}`; - } + // Changelog entries live on one page; the entry itself is the only anchor. + if (kind === "changelog") return `/changelog#${encodeURIComponent(slug)}`; // Docs slugs and API reference routes can be nested (`api-reference/list-contacts`). return `/docs/${slug.split("/").map(encodeURIComponent).join("/")}${hash}`; } diff --git a/packages/api-contract/src/contract.ts b/packages/api-contract/src/contract.ts index 2d347b43..0a743056 100644 --- a/packages/api-contract/src/contract.ts +++ b/packages/api-contract/src/contract.ts @@ -119,9 +119,6 @@ import { productUpdateMetaSchema, productUpdateSchema, createProductUpdateBodySchema, - githubIntegrationConfigSchema, - githubIntegrationSchema, - importGithubReleasesResponseSchema, updateProductUpdateBodySchema, } from "./schemas/product-updates"; import { @@ -761,7 +758,8 @@ const documentationContract = c.router({ }, }, }); -const baseProductUpdatesContract = makeDocumentContract( +/** Product updates are documents internally (`kind: "changelog"`). */ +const productUpdatesContract = makeDocumentContract( "content/product-updates", "Content: Product updates", "product update", @@ -771,48 +769,6 @@ const baseProductUpdatesContract = makeDocumentContract( updateProductUpdateBodySchema, ); -/** Product updates are documents internally plus a GitHub Releases importer. */ -const productUpdatesContract = c.router( - { - // Register literal GitHub routes before the generic - // `/content/product-updates/:documentId` routes below (spread last), - // because Express matches routes in registration order. - connectGithub: { - method: "PUT", - path: "/content/product-updates/github", - body: githubIntegrationConfigSchema, - responses: { 200: githubIntegrationSchema }, - summary: "Connect a GitHub repo for product-update import", - }, - getGithubIntegration: { - method: "GET", - path: "/content/product-updates/github", - responses: { 200: githubIntegrationSchema }, - summary: "Get the team's GitHub integration status", - }, - disconnectGithub: { - method: "DELETE", - path: "/content/product-updates/github", - responses: { 204: c.noBody() }, - summary: "Disconnect the team's GitHub integration", - }, - importGithubReleases: { - method: "POST", - path: "/content/product-updates/import-github", - body: c.noBody(), - responses: { - 200: importGithubReleasesResponseSchema, - 400: errorSchema, - }, - summary: "Import GitHub Releases as draft product updates", - description: - "Lists Releases from the connected repo and creates one draft product update per release, idempotent by meta.github.releaseId (architecture.md §8.1). Nothing is auto-published.", - }, - ...baseProductUpdatesContract, - }, - { metadata: { tag: "Content: Product updates" } }, -); - /** The help-center nav tree a "doc" document's `meta.sectionId` points * into (architecture.md §5.1), plus semantic search over published docs * (§6.4), backed by the pgvector chunks generated on publish. */ @@ -1219,7 +1175,7 @@ const publicContract = c.router( path: "/public/:teamId/content/product-updates", query: z.object({ offset: z.coerce.number().int().min(1).optional(), - itemsPerPage: z.coerce.number().int().min(1).optional(), + itemsPerPage: z.coerce.number().int().min(1).max(1000).optional(), }), responses: { 200: paginated(publicProductUpdateSchema), diff --git a/packages/api-contract/src/schemas/product-updates.ts b/packages/api-contract/src/schemas/product-updates.ts index 2b5a7592..aed2ecd3 100644 --- a/packages/api-contract/src/schemas/product-updates.ts +++ b/packages/api-contract/src/schemas/product-updates.ts @@ -5,16 +5,30 @@ import { makeUpdateDocumentBodySchema, } from "./documents"; +/** Keep a Changelog change types (https://keepachangelog.com/). */ +export const productUpdateLabels = [ + "added", + "changed", + "deprecated", + "removed", + "fixed", + "security", +] as const; +export type ProductUpdateLabel = (typeof productUpdateLabels)[number]; + export const productUpdateMetaSchema = z.object({ - version: z.string().optional(), - labels: z.array(z.string()).default([]), - releaseDate: z.string().optional(), - github: z - .object({ - releaseId: z.number(), - releaseUrl: z.string(), - tagName: z.string(), + version: z.string().max(64).optional(), + labels: z + .array(z.enum(productUpdateLabels)) + .max(productUpdateLabels.length) + .refine((labels) => new Set(labels).size === labels.length, { + message: "Labels must be unique", }) + .default([]), + /** Calendar date of the release (`YYYY-MM-DD`). Falls back to the publish date. */ + releaseDate: z + .string() + .regex(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/, "Use the YYYY-MM-DD format") .optional(), indexable: z.boolean().optional(), }); @@ -35,17 +49,16 @@ export const updateProductUpdateBodySchema = makeUpdateDocumentBodySchema( productUpdateMetaSchema.partial(), ); -export const githubIntegrationConfigSchema = z.object({ - token: z.string().min(1), - repo: z.string().min(1).describe("owner/repo, e.g. courselit/courselit"), -}); - -export const githubIntegrationSchema = z.object({ - connected: z.boolean(), - repo: z.string().nullable(), -}); - -export const importGithubReleasesResponseSchema = z.object({ - imported: z.number(), - skipped: z.number(), -}); +/** + * The date a changelog entry is shown and sorted by: its release date, or + * the UTC calendar date it was published. Always `YYYY-MM-DD`. + */ +export function changelogDate(entry: { + meta?: { releaseDate?: string | null } | null; + publishedAt?: string | Date | null; +}): string | null { + if (entry.meta?.releaseDate) return entry.meta.releaseDate; + if (!entry.publishedAt) return null; + const date = new Date(entry.publishedAt); + return Number.isNaN(date.getTime()) ? null : date.toISOString().slice(0, 10); +} From bebe5481b0167e0b34cbab4e8acb085270421b97 Mon Sep 17 00:00:00 2001 From: Rajat <hi@rajatsaxena.dev> Date: Sat, 7 Nov 2026 16:56:49 +0530 Subject: [PATCH 2/3] refactor(changelog): drop version and label fields A changelog entry is now a title, body and optional release date. - Remove `version`: it duplicated the title and could disagree with it. Versioned products put the version in the title ("v1.2.0: Dark mode"). - Remove change-type `labels`: entry badges repeated the body's `### Added` / `### Fixed` sections and were never filtered on. - Migration 0015 drops `meta.version` and `meta.labels` (pre-production, rewritten in place). - Document the page's scale limits: no pagination, 1,000-entry cutoff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- apps/api/drizzle/0015_changelog_cleanup.sql | 30 ++-------- .../db/changelog-cleanup-migration.test.ts | 15 +---- apps/api/src/documentation/chunking.test.ts | 10 ++-- apps/api/src/documentation/chunking.ts | 13 ++--- .../product-updates-contract.test.ts | 31 +++++----- .../(site)/changelog/rss.xml/route.test.ts | 4 +- apps/sites/components/themed-content-feed.tsx | 39 ++----------- apps/sites/lib/api.ts | 2 - apps/sites/lib/markdown.ts | 6 +- .../content/editors/product-update-editor.tsx | 57 +------------------ .../content/product-updates-view.tsx | 4 -- apps/web/lib/api.ts | 4 +- docs/architecture.md | 7 +-- docs/changelog-management.md | 42 ++++++++------ .../src/schemas/product-updates.ts | 19 ------- 15 files changed, 73 insertions(+), 210 deletions(-) diff --git a/apps/api/drizzle/0015_changelog_cleanup.sql b/apps/api/drizzle/0015_changelog_cleanup.sql index 0b24f7b1..f2690574 100644 --- a/apps/api/drizzle/0015_changelog_cleanup.sql +++ b/apps/api/drizzle/0015_changelog_cleanup.sql @@ -5,33 +5,13 @@ -- SendLit key ("sendlit" rows) is untouched. DELETE FROM "integrations" WHERE "provider" = 'github'; --> statement-breakpoint --- 2. Normalize changelog meta (published and draft) to the new contract: --- labels limited to the Keep a Changelog types ("improved" -> "changed", --- unknown labels such as "prerelease" dropped, duplicates removed), --- releaseDate reduced to YYYY-MM-DD (GitHub imports stored a timestamp), --- and the import-only "github" key removed. +-- 2. Normalize changelog meta (published and draft) to the new contract: an +-- entry is a title, body and optional release date. releaseDate is reduced +-- to YYYY-MM-DD (GitHub imports stored a timestamp) and the removed +-- "version", "labels" and import-only "github" keys are dropped. CREATE OR REPLACE FUNCTION pg_temp.frontlit_changelog_meta(m jsonb) RETURNS jsonb LANGUAGE sql IMMUTABLE AS $$ - SELECT (coalesce(m, '{}'::jsonb) - 'github' - 'labels' - 'releaseDate') - || jsonb_build_object( - 'labels', - coalesce( - ( - SELECT jsonb_agg(label ORDER BY first_seen) - FROM ( - SELECT - CASE WHEN value = 'improved' THEN 'changed' ELSE value END AS label, - min(ordinality) AS first_seen - FROM jsonb_array_elements_text( - CASE WHEN jsonb_typeof(m -> 'labels') = 'array' THEN m -> 'labels' ELSE '[]'::jsonb END - ) WITH ORDINALITY - GROUP BY 1 - ) mapped - WHERE label IN ('added', 'changed', 'deprecated', 'removed', 'fixed', 'security') - ), - '[]'::jsonb - ) - ) + SELECT (coalesce(m, '{}'::jsonb) - 'github' - 'version' - 'labels' - 'releaseDate') || CASE WHEN m ->> 'releaseDate' ~ '^\d{4}-\d{2}-\d{2}' THEN jsonb_build_object('releaseDate', left(m ->> 'releaseDate', 10)) diff --git a/apps/api/src/db/changelog-cleanup-migration.test.ts b/apps/api/src/db/changelog-cleanup-migration.test.ts index b500fdbc..9d01c5ff 100644 --- a/apps/api/src/db/changelog-cleanup-migration.test.ts +++ b/apps/api/src/db/changelog-cleanup-migration.test.ts @@ -44,7 +44,7 @@ describe("0015_changelog_cleanup migration", () => { expect(rows.map((row) => row.provider)).toEqual(["sendlit"]); }); - it("normalizes changelog labels, release dates and drops GitHub meta", async () => { + it("normalizes release dates and drops version, labels and GitHub meta", async () => { const legacy = { version: "v1.0.0", labels: ["improved", "prerelease", "fixed", "changed"], @@ -78,17 +78,8 @@ describe("0015_changelog_cleanup migration", () => { .select() .from(documents) .where(eq(documents.slug, "legacy")); - expect(changelog.meta).toEqual({ - version: "v1.0.0", - labels: ["changed", "fixed"], - releaseDate: "2026-03-04", - indexable: false, - }); - expect(changelog.draftMeta).toEqual({ - version: "v1.0.0", - labels: ["changed", "fixed"], - indexable: false, - }); + expect(changelog.meta).toEqual({ releaseDate: "2026-03-04", indexable: false }); + expect(changelog.draftMeta).toEqual({ indexable: false }); const [blog] = await db.select().from(documents).where(eq(documents.slug, "post")); expect(blog.meta).toEqual({ labels: ["improved"], github: { releaseId: 2 } }); diff --git a/apps/api/src/documentation/chunking.test.ts b/apps/api/src/documentation/chunking.test.ts index 8add3cef..3f8a1e1d 100644 --- a/apps/api/src/documentation/chunking.test.ts +++ b/apps/api/src/documentation/chunking.test.ts @@ -204,15 +204,15 @@ describe("changelog chunks", () => { expect(chunk.anchor).toBeNull(); }); - it("builds release context from version and release date", () => { + it("builds release context from the title and release date", () => { expect( changelogChunkContext({ - title: "Dark mode", - slug: "dark-mode", - meta: { version: "1.2.0", releaseDate: "2026-11-07" }, + title: "v1.2.0: Dark mode", + slug: "v1-2-0-dark-mode", + meta: { releaseDate: "2026-11-07" }, publishedAt: new Date("2026-11-08T10:00:00Z"), }), - ).toBe("Dark mode — 1.2.0, 2026-11-07"); + ).toBe("v1.2.0: Dark mode — 2026-11-07"); }); it("falls back to the publish date and slug", () => { diff --git a/apps/api/src/documentation/chunking.ts b/apps/api/src/documentation/chunking.ts index fb850c90..be5d4cc4 100644 --- a/apps/api/src/documentation/chunking.ts +++ b/apps/api/src/documentation/chunking.ts @@ -160,8 +160,9 @@ export function extractChunks( } /** - * Release context prepended to every changelog chunk ("Dark mode — 1.2.0, - * 2026-11-07"), so retrieval never presents an old fix without its release. + * Release context prepended to every changelog chunk ("Dark mode — 2026-11-07"), + * so retrieval never presents an old fix without its release. Versioned + * products put the version in the title. */ export function changelogChunkContext(doc: { title: string | null; @@ -169,10 +170,8 @@ export function changelogChunkContext(doc: { meta: Record<string, unknown> | null; publishedAt: Date | null; }): string { - const meta = (doc.meta ?? {}) as { version?: string; releaseDate?: string }; - const details = [meta.version, changelogDate({ meta, publishedAt: doc.publishedAt })] - .filter(Boolean) - .join(", "); + const meta = (doc.meta ?? {}) as { releaseDate?: string }; + const date = changelogDate({ meta, publishedAt: doc.publishedAt }); const title = doc.title || doc.slug; - return details ? `${title} — ${details}` : title; + return date ? `${title} — ${date}` : title; } diff --git a/apps/api/src/documents/product-updates-contract.test.ts b/apps/api/src/documents/product-updates-contract.test.ts index ae25468c..1304ae1e 100644 --- a/apps/api/src/documents/product-updates-contract.test.ts +++ b/apps/api/src/documents/product-updates-contract.test.ts @@ -7,30 +7,25 @@ import { } from "@frontlit/api-contract"; describe("product update contract", () => { - it("accepts Keep a Changelog labels and calendar dates", () => { - expect( - productUpdateMetaSchema.parse({ - version: "v1.2.0", - labels: ["added", "security"], - releaseDate: "2026-11-07", - }), - ).toEqual({ - version: "v1.2.0", - labels: ["added", "security"], + it("accepts a calendar release date", () => { + expect(productUpdateMetaSchema.parse({ releaseDate: "2026-11-07" })).toEqual({ releaseDate: "2026-11-07", }); }); - it.each([ - [{ labels: ["improved"] }], - [{ labels: ["fixed", "fixed"] }], - [{ releaseDate: "2026-11-07T10:00:00Z" }], - [{ releaseDate: "07/11/2026" }], - [{ version: "v".repeat(65) }], - ])("rejects invalid meta %j", (meta) => { - expect(productUpdateMetaSchema.safeParse(meta).success).toBe(false); + it("has no version or labels; the title and body carry them", () => { + expect( + productUpdateMetaSchema.parse({ version: "v1.2.0", labels: ["added"] }), + ).toEqual({}); }); + it.each([[{ releaseDate: "2026-11-07T10:00:00Z" }], [{ releaseDate: "07/11/2026" }]])( + "rejects invalid meta %j", + (meta) => { + expect(productUpdateMetaSchema.safeParse(meta).success).toBe(false); + }, + ); + it("caps the public page size", () => { const query = contract.public.listProductUpdates.query; expect(query.safeParse({ itemsPerPage: "1000" }).success).toBe(true); diff --git a/apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.test.ts b/apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.test.ts index 4488804e..4d416cc5 100644 --- a/apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.test.ts +++ b/apps/sites/app/sites/[teamId]/(site)/changelog/rss.xml/route.test.ts @@ -12,7 +12,7 @@ const entry = (slug: string, overrides: Record<string, unknown> = {}) => ({ ], }, featuredImage: null, - meta: { labels: [] }, + meta: {}, publishedAt: "2026-11-08T23:30:00.000Z", updatedAt: null, ...overrides, @@ -23,7 +23,7 @@ vi.mock("@/lib/api", () => ({ getCanonicalHost: async () => "acme.com", listProductUpdates: async () => ({ items: [ - entry("dark-mode", { meta: { labels: ["added"], releaseDate: "2026-11-07" } }), + entry("dark-mode", { meta: { releaseDate: "2026-11-07" } }), entry("faster-exports", { excerpt: "Exports are 2x faster." }), ], total: 2, diff --git a/apps/sites/components/themed-content-feed.tsx b/apps/sites/components/themed-content-feed.tsx index ea148a23..e200dc9d 100644 --- a/apps/sites/components/themed-content-feed.tsx +++ b/apps/sites/components/themed-content-feed.tsx @@ -2,7 +2,6 @@ import { SiteLink as Link } from "@/components/site-link"; import { TextRenderer } from "@frontlit/text-editor/renderer"; import type { TextEditorContent } from "@frontlit/text-editor"; import { - Badge, Caption, Header1, Header4, @@ -124,18 +123,6 @@ export async function ThemedArticleFeed({ ); } -const LABELS: Record< - string, - { text: string; variant: "default" | "secondary" | "destructive" | "outline" } -> = { - added: { text: "Added", variant: "default" }, - changed: { text: "Changed", variant: "outline" }, - deprecated: { text: "Deprecated", variant: "outline" }, - removed: { text: "Removed", variant: "destructive" }, - fixed: { text: "Fixed", variant: "secondary" }, - security: { text: "Security", variant: "destructive" }, -}; - /** Formats a `YYYY-MM-DD` calendar date without shifting it into the * server's time zone (a plain `new Date()` would show Nov 6 for 2026-11-07 * west of UTC). */ @@ -165,7 +152,7 @@ export async function ThemedProductUpdatesFeed({ title: string | null; content: TextEditorContent | null; publishedAt: string | null; - meta: { version?: string | null; labels: string[]; releaseDate?: string | null }; + meta: { releaseDate?: string | null }; }>; }) { const theme = await getTeamThemeStyle(teamId); @@ -186,25 +173,11 @@ export async function ThemedProductUpdatesFeed({ className="relative scroll-mt-24" > <span className="absolute -left-[calc(2rem+5px)] top-1.5 h-2.5 w-2.5 rounded-full bg-muted-foreground" /> - <div className="flex flex-wrap items-center gap-2"> - {entry.meta.version && ( - <Caption theme={theme}>{entry.meta.version}</Caption> - )} - {date && ( - <Caption theme={theme}> - <time dateTime={date}>{formatCalendarDate(date)}</time> - </Caption> - )} - {entry.meta.labels.map((label) => ( - <Badge - key={label} - theme={theme} - variant={LABELS[label]?.variant ?? "secondary"} - > - {LABELS[label]?.text ?? label} - </Badge> - ))} - </div> + {date && ( + <Caption theme={theme}> + <time dateTime={date}>{formatCalendarDate(date)}</time> + </Caption> + )} {/* The entry title is its permalink (`/changelog#<slug>`). */} <a href={`#${entry.slug}`} className="group mt-2 block"> <Header4 theme={theme} className="flex items-center gap-2"> diff --git a/apps/sites/lib/api.ts b/apps/sites/lib/api.ts index 27b95e23..0ec9178d 100644 --- a/apps/sites/lib/api.ts +++ b/apps/sites/lib/api.ts @@ -390,8 +390,6 @@ export async function getApiSpec( } export interface ProductUpdateMeta { - version?: string; - labels: string[]; releaseDate?: string; seo?: { title?: string; description?: string }; } diff --git a/apps/sites/lib/markdown.ts b/apps/sites/lib/markdown.ts index d0377470..f5a608e8 100644 --- a/apps/sites/lib/markdown.ts +++ b/apps/sites/lib/markdown.ts @@ -93,11 +93,9 @@ async function changelogFeed(teamId: string) { }); return items .map((update) => { - const label = [update.meta.version, changelogDate(update)] - .filter(Boolean) - .join(", "); + const date = changelogDate(update); return [ - `## ${update.title || update.slug}${label ? ` (${label})` : ""}`, + `## ${update.title || update.slug}${date ? ` (${date})` : ""}`, tiptapToMarkdown(update.content), ] .filter(Boolean) diff --git a/apps/web/components/content/editors/product-update-editor.tsx b/apps/web/components/content/editors/product-update-editor.tsx index 7b77b65a..edd200b4 100644 --- a/apps/web/components/content/editors/product-update-editor.tsx +++ b/apps/web/components/content/editors/product-update-editor.tsx @@ -4,11 +4,7 @@ import { use, useEffect, useRef, useState } from "react"; import { Editor } from "@frontlit/text-editor"; import type { TextEditorContent } from "@frontlit/text-editor"; import { ExternalLink } from "lucide-react"; -import { - productUpdateLabels, - publicContentPath, - type ProductUpdateLabel, -} from "@frontlit/api-contract"; +import { publicContentPath } from "@frontlit/api-contract"; import { discardProductUpdateDraft, getAiConfig, @@ -25,7 +21,6 @@ import { WritingEditorShell } from "@/components/workspace/writing-editor-shell" import { IconButton } from "@/components/ui/codelit/icon-button"; import { Input } from "@/components/ui/codelit/input"; import { Label } from "@/components/ui/codelit/label"; -import { Toggle } from "@/components/ui/toggle"; import { ContentIndexSettings } from "@/components/workspace/content-index-settings"; import { FeaturedImageSetting } from "@/components/workspace/featured-image-setting"; import { @@ -43,7 +38,6 @@ export default function ProductUpdateEditor({ const { documentId } = use(params); const [entry, setEntry] = useState<ProductUpdate | null>(null); const [title, setTitle] = useState(""); - const [version, setVersion] = useState(""); const [releaseDate, setReleaseDate] = useState(""); const [saving, setSaving] = useState(false); const [publishing, setPublishing] = useState(false); @@ -52,7 +46,6 @@ export default function ProductUpdateEditor({ const [teamIndexesChangelog, setTeamIndexesChangelog] = useState(false); const [liveHost, setLiveHost] = useState<string | null>(null); const titleDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); - const versionDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); const releaseDateDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); useEffect(() => { @@ -60,7 +53,6 @@ export default function ProductUpdateEditor({ getProductUpdate(documentId).then((loaded) => { setEntry(loaded); setTitle(loaded.draftTitle); - setVersion(loaded.draftMeta.version ?? ""); setReleaseDate(loaded.draftMeta.releaseDate ?? ""); }); getAiConfig() @@ -76,7 +68,6 @@ export default function ProductUpdateEditor({ useEffect( () => () => { if (titleDebounceRef.current) clearTimeout(titleDebounceRef.current); - if (versionDebounceRef.current) clearTimeout(versionDebounceRef.current); if (releaseDateDebounceRef.current) clearTimeout(releaseDateDebounceRef.current); }, [], @@ -114,15 +105,6 @@ export default function ProductUpdateEditor({ }, 600); } - function handleVersionChange(value: string) { - setVersion(value); - if (versionDebounceRef.current) clearTimeout(versionDebounceRef.current); - versionDebounceRef.current = setTimeout( - () => saveMeta({ version: value || undefined }), - 600, - ); - } - function handleReleaseDateChange(value: string) { setReleaseDate(value); if (releaseDateDebounceRef.current) clearTimeout(releaseDateDebounceRef.current); @@ -132,14 +114,6 @@ export default function ProductUpdateEditor({ ); } - async function handleLabelToggle(label: ProductUpdateLabel) { - const current = entry?.draftMeta.labels ?? []; - const labels = current.includes(label) - ? current.filter((item) => item !== label) - : [...current, label]; - await saveMeta({ labels }); - } - async function handleContentChange(json: TextEditorContent) { setSaving(true); try { @@ -169,14 +143,12 @@ export default function ProductUpdateEditor({ async function handleDiscardDraft() { if (titleDebounceRef.current) clearTimeout(titleDebounceRef.current); - if (versionDebounceRef.current) clearTimeout(versionDebounceRef.current); if (releaseDateDebounceRef.current) clearTimeout(releaseDateDebounceRef.current); setPublishing(true); try { const reverted = await discardProductUpdateDraft(documentId); setEntry(reverted); setTitle(reverted.draftTitle); - setVersion(reverted.draftMeta.version ?? ""); setReleaseDate(reverted.draftMeta.releaseDate ?? ""); setResetKey((key) => key + 1); } finally { @@ -245,16 +217,6 @@ export default function ProductUpdateEditor({ onChange={handleFeaturedImageChange} disabled={saving || publishing} /> - <div> - <Label htmlFor="changelog-version">Version</Label> - <Input - id="changelog-version" - value={version} - onChange={(event) => handleVersionChange(event.target.value)} - placeholder="v1.2.0" - className="mt-1" - /> - </div> <div> <Label htmlFor="changelog-release-date">Release date</Label> <Input @@ -274,23 +236,6 @@ export default function ProductUpdateEditor({ date is used. </p> </div> - <div> - <Label>Change types</Label> - <div className="mt-2 flex flex-wrap gap-1.5" data-testid="changelog-labels"> - {productUpdateLabels.map((label) => ( - <Toggle - key={label} - size="sm" - pressed={entry.draftMeta.labels.includes(label)} - onPressedChange={() => handleLabelToggle(label)} - variant="outline" - className="capitalize" - > - {label} - </Toggle> - ))} - </div> - </div> {teamIndexesChangelog && ( <ContentIndexSettings checked={entry.draftMeta.indexable !== false} diff --git a/apps/web/components/content/product-updates-view.tsx b/apps/web/components/content/product-updates-view.tsx index 591b12ae..2f8af660 100644 --- a/apps/web/components/content/product-updates-view.tsx +++ b/apps/web/components/content/product-updates-view.tsx @@ -19,7 +19,6 @@ import { DocumentStatusBadge } from "@/components/workspace/document-status-badg import { DocumentIndexBadge } from "@/components/workspace/document-index-badge"; import { Button } from "@/components/ui/codelit/button"; import { Input } from "@/components/ui/codelit/input"; -import { Badge } from "@/components/ui/codelit/badge"; import { useSetBreadcrumb } from "@/components/workspace/breadcrumb-context"; export default function ChangelogPage() { @@ -131,9 +130,6 @@ export default function ChangelogPage() { > {entry.draftTitle} </Link> - {entry.draftMeta.version && ( - <Badge variant="neutral">{entry.draftMeta.version}</Badge> - )} <DocumentStatusBadge status={entry.status} /> <DocumentIndexBadge index={entry.index} /> </div> diff --git a/apps/web/lib/api.ts b/apps/web/lib/api.ts index 000ffd9b..addc80a5 100644 --- a/apps/web/lib/api.ts +++ b/apps/web/lib/api.ts @@ -1,5 +1,5 @@ import { initClient } from "@ts-rest/core"; -import { contract, type ProductUpdateLabel } from "@frontlit/api-contract"; +import { contract } from "@frontlit/api-contract"; import type { TextEditorContent } from "@frontlit/text-editor"; import type { WidgetInstance } from "@frontlit/page-builder/models"; import { ApiError } from "./api-client"; @@ -815,8 +815,6 @@ export function searchDocumentation(q: string, k?: number) { // ---- Content: product updates ------------------------------------------- export interface ProductUpdateMeta { - version?: string; - labels: ProductUpdateLabel[]; /** `YYYY-MM-DD`; the changelog falls back to the publish date. */ releaseDate?: string; indexable?: boolean; diff --git a/docs/architecture.md b/docs/architecture.md index dbce5417..c80947cb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -484,7 +484,7 @@ jsonb so the core engine stays generic): | _all kinds_ | `seo?: { title?, description? }` — optional overrides for public metadata (§12.3) | | `blog` | `tags: string[]` | | `doc` | `sectionId: string`, `order: number` (help-center nav tree) | -| `changelog` | `version?: string` (≤ 64), `labels: ("added"\|"changed"\|"deprecated"\|"removed"\|"fixed"\|"security")[]`, `releaseDate?: "YYYY-MM-DD"` | +| `changelog` | `releaseDate?: "YYYY-MM-DD"` (version goes in the title; change types are `###` headings in the body) | Help-center sections get a small companion table: @@ -753,11 +753,10 @@ a person. There is no GitHub integration for the changelog; see when to revisit it. `/changelog` renders every published entry on one page, newest first by -`releaseDate ?? publishedAt` (`changelogDate()` in the API contract), with -Keep a Changelog type badges and optional version. Each entry is the anchor +`releaseDate ?? publishedAt` (`changelogDate()` in the API contract). Each entry is the anchor `/changelog#<slug>` (`publicContentPath`), which the RSS feed at `/changelog/rss.xml` (§12.3), JSON-LD, and AI citations all link to. -Changelog chunks carry their release ("Title — 1.2.0, 2026-11-07") so +Changelog chunks carry their release ("v1.2.0: Dark mode — 2026-11-07") so retrieval never presents an old change without its date. --- diff --git a/docs/changelog-management.md b/docs/changelog-management.md index 65fdf641..4e035269 100644 --- a/docs/changelog-management.md +++ b/docs/changelog-management.md @@ -7,8 +7,8 @@ Scope: authoring, publishing, and delivering product updates (internal kind `cha 1. **The editor and API/MCP are the only authoring path.** Entries are drafted in the dashboard editor or created as drafts by an agent through MCP, then published by a person. FrontLit does not integrate with GitHub for changelogs. 2. **Automation lives on the customer's side.** A team's coding agent (Claude Code, Cursor, …) already has `git log`, `gh`, and the PRs. With FrontLit's MCP server connected, it drafts entries using the existing `create_product_update` tool. FrontLit needs no webhooks, LLM pipeline, or repository access for this. -3. **Keep the existing model.** An entry is a `documents` row (`kind: "changelog"`) with a title, body, optional `version`, optional `releaseDate`, and optional labels. Draft/publish, slugs, AI indexing, and MCP are unchanged. -4. **Follow [Keep a Changelog](https://keepachangelog.com/) where it is cheap.** Newest first, each entry dated and linkable, and labels limited to its six change types. The body structure is not enforced. +3. **Keep the existing model.** An entry is a `documents` row (`kind: "changelog"`) with a title, body, and optional `releaseDate`. There is no separate version field or change-type labels: they repeated the title and body and could disagree with them. Versioned products put the version in the title (`v1.2.0` or `v1.2.0: Dark mode`), as Claude Code and Keep a Changelog headings do, and group changes in the body under `### Added`, `### Fixed`, and so on. Draft/publish, slugs, AI indexing, and MCP are unchanged. +4. **Follow [Keep a Changelog](https://keepachangelog.com/) where it is cheap.** Newest first, each entry dated and linkable, and change types as `###` headings in the body. The body structure is not enforced. 5. **One public page.** Each entry has an anchor at `/changelog#<slug>` (as Mintlify does). There are no pages for individual entries. ### Why GitHub is removed @@ -52,8 +52,8 @@ Two PRs, followed by docs and dogfooding. Each PR must pass a full browser smoke - `productUpdateMetaSchema`: - `releaseDate`: `YYYY-MM-DD` only (regex), optional. - - `labels`: enum array of `added | changed | deprecated | removed | fixed | security`, max 6, deduplicated, default `[]`. - - `version`: `max(64)`. + - Remove `labels`: entry-level badges repeated the body's `### Added` / `### Fixed` sections and were only displayed, never filtered on. + - Remove `version`: it duplicated the title and could disagree with it. - Remove `github`. - Public list query for product updates: `itemsPerPage` `max(1000)`. - Add a shared helper `changelogDate(entry)` that returns `meta.releaseDate ?? publishedAt` as `YYYY-MM-DD`. Every surface uses it so they all show the same date. @@ -65,8 +65,8 @@ Two PRs, followed by docs and dogfooding. Each PR must pass a full browser smoke - `listProductUpdates` public route: add `publicReadLimiter` to match the other public reads. - Chunking for `kind === "changelog"`: - `anchor = null`, since citations fall back to `#slug`. - - Prefix each chunk with `"<title> — <version>, <date>"` so it carries release context. The index job passes the document's title and meta, because `extractChunks` sees only the content. -- Migration (pre-production, so rewrite it): change `improved` → `changed` and drop unknown labels and `meta.github` from existing changelog rows, in both `meta` and `draftMeta`. + - Prefix each chunk with `"<title> — <date>"` so it carries release context. The index job passes the document's title and meta, because `extractChunks` sees only the content. +- Migration (pre-production, so rewrite it): drop `meta.labels`, `meta.version`, and `meta.github` from existing changelog rows and reduce `releaseDate` to `YYYY-MM-DD`, in both `meta` and `draftMeta`. **Sites (`apps/sites`)** @@ -81,12 +81,12 @@ Two PRs, followed by docs and dogfooding. Each PR must pass a full browser smoke - `pubDate`: `changelogDate` - `description`: `excerpt` if set, otherwise the first ~300 characters of plain text from the TipTap body (escaped by the existing `escapeXml`) - JSON-LD: each entry's `url` becomes its anchor URL. -- Markdown (`changelogFeed` in `apps/sites/lib/markdown.ts`): same ordering, `changelogDate`, and a heading of `## <title> (<version>, <date>)`. +- Markdown (`changelogFeed` in `apps/sites/lib/markdown.ts`): same ordering, `changelogDate`, and a heading of `## <title> (<date>)`. **Dashboard (`apps/web`)** - Product-update editor: - - Labels become the six Keep a Changelog types. + - Remove the version field and the change-type toggles. - `releaseDate` uses a date input, with a hint that it defaults to the publish date when empty. - Show the public anchor URL ("View on site") for published entries. @@ -155,15 +155,15 @@ Checked: no docs/pages code imports `apps/api/src/integrations/github.ts` or the Sample prompt: -> List the PRs merged to `main` since the last published FrontLit changelog entry. Skip chores, refactors, CI, and dependency bumps. Draft one product update with a short title, a one-sentence intro, and bullets grouped under Added / Changed / Fixed, written for customers, not engineers. Set labels to the groups used. Create it as a draft with `create_product_update` and do not publish it. +> List the PRs merged to `main` since the last published FrontLit changelog entry. Skip chores, refactors, CI, and dependency bumps. Draft one product update with a short title, a one-sentence intro, and bullets grouped under `### Added` / `### Changed` / `### Fixed` headings, written for customers, not engineers. Create it as a draft with `create_product_update` and do not publish it. ## 4. Security - **Fewer secrets and less outbound access.** PR 2 deletes the changelog's stored GitHub tokens and the code that calls `api.github.com` with them. The migration purges only `integrations` rows with `provider = 'github'`. Docs/pages GitHub App credentials and bindings live in separate tables and are not affected (§3.2). - **Public read limits.** Add `publicReadLimiter` to the public product-updates list, and cap `itemsPerPage` at 1000. Edge caching (`changelog:<team>` tag) already absorbs most traffic. - **No draft leakage.** Public list, RSS, JSON-LD, Markdown, and AI indexing read only published columns (`content`, `meta`, `publishedAt`) and filter on `publishedAt IS NOT NULL`. The new sort must use `meta`, not `draftMeta`. Add a test that a draft-only `releaseDate`/label change does not affect public order or output. -- **Input validation.** `releaseDate` uses a strict format, `labels` an enum, `version` a length limit. Slugs are already normalized (`normalizeEditableSlug`) and URL-encoded in `publicContentPath`. -- **Output encoding.** React escapes anchor ids and labels. RSS keeps `escapeXml` for every field, including the new plain-text description. No raw HTML is added. +- **Input validation.** `releaseDate` uses a strict format; unknown meta keys are rejected by MCP and stripped by REST. Slugs are already normalized (`normalizeEditableSlug`) and URL-encoded in `publicContentPath`. +- **Output encoding.** React escapes anchor ids and titles. RSS keeps `escapeXml` for every field, including the new plain-text description. No raw HTML is added. - **Agent drafts are untrusted input.** PR titles and descriptions the agent summarizes may contain injected instructions. The recipe creates **drafts only**, and a person publishes. This is a convention, not enforced: `publish` uses the same `productUpdatesWrite` scope as `create` (`apps/api/src/auth/operation-policy.ts`). Splitting out a publish scope is deferred until a team asks for it. - **Authorization unchanged.** Remaining endpoints keep their `content.productUpdates.*` permissions. Removed operations disappear from both the REST and MCP policy maps in the same PR, so no orphaned permission remains. @@ -181,7 +181,7 @@ Sample prompt: **Unit and integration (`apps/api`, `packages/api-contract`)** -- Meta schema: valid and invalid `releaseDate`; label enum, dedupe, and max; `version` length. +- Meta schema: valid and invalid `releaseDate`; no `version` or `labels` fields. - `changelogDate`: `releaseDate` wins, falls back to `publishedAt`, and is formatted in UTC. - `publicContentPath("changelog", slug, "fixed")` returns `/changelog#<slug>`. - `listPublishedDocuments("changelog")`: ordered by release date, then `publishedAt`; ignores `draftMeta`; excludes unpublished entries. @@ -197,15 +197,15 @@ Sample prompt: **Browser smoke test (required)** -1. Create a draft through MCP (`create_product_update`) with labels and a `releaseDate` in the past. Confirm it does not appear on `/changelog`, in `/changelog/rss.xml`, or in `/changelog.md`. -2. Edit it in the dashboard (labels, date input) and publish. +1. Create a draft through MCP (`create_product_update`) with a `releaseDate` in the past. Confirm it does not appear on `/changelog`, in `/changelog/rss.xml`, or in `/changelog.md`. +2. Edit it in the dashboard (date input) and publish. 3. On `/changelog`: - the entry sits at the correct position by release date; - its date is not shifted by time zone; - clicking an entry title sets the URL to `/changelog#<slug>`; - opening that URL scrolls to the entry below the sticky header. 4. RSS shows the item with its anchor link. A feed validator reports unique GUIDs. -5. With changelog indexing enabled, ask the site chatbot about the change. Confirm the citation links to `/changelog#<slug>` and the answer names the version or date. +5. With changelog indexing enabled, ask the site chatbot about the change. Confirm the citation links to `/changelog#<slug>` and the answer names the release title and date. 6. Confirm the dashboard has no GitHub card on product updates, and that the docs GitHub connection in Settings → Integrations still works. ## 7. Delivery order @@ -217,5 +217,15 @@ Sample prompt: ## 8. Revisit when - **A paying customer with a versioned product asks for automatic sync:** add GitHub Releases sync through the existing GitHub App, with GitHub-owned entries read-only. Do not bring back personal access tokens. -- **Changelogs grow past a few hundred entries:** add label filters (`?labels=`) and consider pagination with anchor redirects. +- **Changelogs grow past a few hundred entries:** add change-type filters derived from the body's `###` headings and split the history into yearly archives (see the scale limits below). + +### Known scale limits + +`/changelog` is not paginated: it renders up to `CHANGELOG_PAGE_SIZE` (1,000, the public API's max page size) entries on one page, cached until the next publish. RSS always shows the newest 50. + +- **A few hundred entries:** the page grows heavy, because every entry's full body is in the HTML. This is the first limit a real team would notice. +- **Over 1,000 entries:** older entries **silently drop off** the page and `/changelog.md`, and their `#slug` links stop resolving. There is no warning. +- **Very large histories:** each cache miss sorts all of a team's published entries, because the `releaseDate ?? publishedAt` order has no matching index. Caching keeps this off the visitor path. + +When a team approaches a few hundred entries, move to yearly archives: `/changelog` shows the latest year (or latest N), older entries live at `/changelog/<year>` with the same anchors, and a small script on `/changelog` redirects old `#slug` links to their year page. Add an index on the release-date ordering only if the sort shows up as slow. - **Teams ask to share individual launches:** add `/changelog/<slug>` pages with Open Graph images. Anchors remain valid. diff --git a/packages/api-contract/src/schemas/product-updates.ts b/packages/api-contract/src/schemas/product-updates.ts index aed2ecd3..3231ff76 100644 --- a/packages/api-contract/src/schemas/product-updates.ts +++ b/packages/api-contract/src/schemas/product-updates.ts @@ -5,26 +5,7 @@ import { makeUpdateDocumentBodySchema, } from "./documents"; -/** Keep a Changelog change types (https://keepachangelog.com/). */ -export const productUpdateLabels = [ - "added", - "changed", - "deprecated", - "removed", - "fixed", - "security", -] as const; -export type ProductUpdateLabel = (typeof productUpdateLabels)[number]; - export const productUpdateMetaSchema = z.object({ - version: z.string().max(64).optional(), - labels: z - .array(z.enum(productUpdateLabels)) - .max(productUpdateLabels.length) - .refine((labels) => new Set(labels).size === labels.length, { - message: "Labels must be unique", - }) - .default([]), /** Calendar date of the release (`YYYY-MM-DD`). Falls back to the publish date. */ releaseDate: z .string() From 09aa74c55eebc58129623ab6f2757ee3fd13f663 Mon Sep 17 00:00:00 2001 From: Rajat <hi@rajatsaxena.dev> Date: Sat, 7 Nov 2026 18:14:21 +0530 Subject: [PATCH 3/3] fix(changelog): address review feedback - Link each entry's permalink through the site link wrapper (`SiteLink`) instead of a raw anchor, matching the article feed. - Drop "GitHub release import" from the Changelog OpenAPI tag description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- apps/api/src/openapi.ts | 2 +- apps/sites/components/themed-content-feed.tsx | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/api/src/openapi.ts b/apps/api/src/openapi.ts index de3cc404..807aac82 100644 --- a/apps/api/src/openapi.ts +++ b/apps/api/src/openapi.ts @@ -70,7 +70,7 @@ export const openApiDocument = generateOpenApi( }, { name: "Changelog", - description: "Changelog authoring, publishing, and GitHub release import.", + description: "Changelog authoring and publishing.", }, { name: "Email", diff --git a/apps/sites/components/themed-content-feed.tsx b/apps/sites/components/themed-content-feed.tsx index e200dc9d..f0ab00ee 100644 --- a/apps/sites/components/themed-content-feed.tsx +++ b/apps/sites/components/themed-content-feed.tsx @@ -10,7 +10,7 @@ import { Text2, } from "@frontlit/page-builder/primitives/server"; import { getTeamThemeStyle } from "@/lib/team-theme"; -import { changelogDate } from "@frontlit/api-contract"; +import { changelogDate, publicContentPath } from "@frontlit/api-contract"; import { Link as LinkIcon } from "lucide-react"; import type { ThemeStyle } from "@frontlit/page-builder/models"; @@ -178,8 +178,12 @@ export async function ThemedProductUpdatesFeed({ <time dateTime={date}>{formatCalendarDate(date)}</time> </Caption> )} - {/* The entry title is its permalink (`/changelog#<slug>`). */} - <a href={`#${entry.slug}`} className="group mt-2 block"> + {/* The entry title is its permalink (`/changelog#<slug>`), through + the same site link wrapper as the article feed above. */} + <Link + href={publicContentPath("changelog", entry.slug)} + className="group mt-2 block" + > <Header4 theme={theme} className="flex items-center gap-2"> {entry.title} <LinkIcon @@ -187,7 +191,7 @@ export async function ThemedProductUpdatesFeed({ className="size-4 shrink-0 opacity-0 transition-opacity group-hover:opacity-60 group-focus-visible:opacity-60" /> </Header4> - </a> + </Link> {entry.content && ( <div className="mt-3"> <TextRenderer json={entry.content} theme={theme} />