From b1978bbf87fa2e32f366405eea54cdaef86c638b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 21:54:01 +0000 Subject: [PATCH 1/2] =?UTF-8?q?perf:=20=E2=9A=A1=EF=B8=8F=20speed=20up=20t?= =?UTF-8?q?he=20Hono=20API,=20data=20loading=20and=20React=20rendering=20a?= =?UTF-8?q?cross=20the=20repo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API - SSR hands API calls to the in-process Hono app instead of looping over HTTP - withPagination runs the count and the page query in parallel - in-memory LRU cache (15s TTL cap) when Redis is not configured - fewer and parallel staff-permission queries, per-request device/epoch memos - core middleware object built once at boot; event listeners indexed by name - timeouts on captcha, SSO, cron and storage fetches - hono/compress on the standalone API, pre-compressed static assets - queue drains batches with limited concurrency, set-based status updates - emails processed right after the response, pooled SMTP transport - cron jobs claimed atomically so multi-instance deploys run them once - websocket: stringify once, user/room indexes, heartbeat, client backoff - notifications: skip recipientIds on read paths, sliced fanout, seekable inbox cursor, cached admin overview totals - search: bulk upsert, container deletes in one statement, capped ES totals - AI policies and action settings resolved in batches - indexes for roleId, session expiry, delivery eventId, queue retention, AI history, user files and the moderation queue Content Engine - never-public draft edits no longer expire the public page cache - generation-keyed public list/detail cache, cached container trees - parallel hydration, optional publicApi.listFields, narrow delivery metadata - concurrent write effects, dependent search reindex on the queue Web - session refresh no longer blocks navigation; 75ms intent preload - profile timeline streams from the loader; cached llms/docs text - AutoForm memoizes its schema, WAAPI shake instead of motion - visual editor split contexts and memoized blocks; debounced TipTap onChange - dnd-kit lazy-loaded only for reorderable tables - marketing.css loaded per route; no transition-all; cheaper glows - forum: cached stats, topics with stats in one request, lazy composer Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BpbpzvNwCm19By2HEKWq7d --- .../migration.sql | 9 + .../snapshot.json | 20069 ++++++++++++++++ apps/api/src/index.ts | 38 +- apps/web/.env.example | 10 +- .../content/docs/content-engine/approvals.mdx | 4 +- .../content/docs/content-engine/caching.mdx | 78 +- .../docs/content-engine/public-api.mdx | 64 +- .../content/docs/content-engine/reference.mdx | 71 +- apps/web/content/docs/dev/advanced/queue.mdx | 4 + .../web/content/docs/dev/ai/automatic-alt.mdx | 4 +- apps/web/content/docs/dev/cache/api.mdx | 4 +- .../docs/dev/cache/pages-invalidation.mdx | 2 +- .../docs/dev/cache/pages-reference.mdx | 50 +- apps/web/content/docs/dev/cache/pages.mdx | 10 +- apps/web/content/docs/dev/cache/reference.mdx | 20 +- apps/web/content/docs/dev/cron/node-cron.mdx | 16 +- .../docs/dev/deployments/cloud/vercel.mdx | 8 +- .../docs/dev/deployments/self-hosted.mdx | 41 +- apps/web/content/docs/dev/email/index.mdx | 5 +- apps/web/content/docs/dev/fetcher.mdx | 10 + .../content/docs/dev/moderation/reference.mdx | 2 +- apps/web/content/docs/dev/performance.mdx | 11 + apps/web/content/docs/dev/routing/loading.mdx | 4 +- .../content/docs/dev/routing/navigation.mdx | 4 +- .../content/docs/dev/routing/preloading.mdx | 24 +- .../content/docs/dev/search-elasticsearch.mdx | 20 +- apps/web/content/docs/dev/search.mdx | 34 +- apps/web/content/docs/dev/websocket.mdx | 3 +- apps/web/content/docs/guides/forum.mdx | 10 +- apps/web/content/docs/ui/image-zoom.mdx | 15 + .../src/docs/examples/attachment-layouts.tsx | 9 +- .../src/docs/examples/attachment-states.tsx | 9 +- apps/web/src/docs/examples/attachment.tsx | 9 +- apps/web/src/docs/examples/carousel.tsx | 2 +- .../src/docs/examples/comparison-handle.tsx | 20 +- .../src/docs/examples/comparison-hover.tsx | 20 +- apps/web/src/docs/examples/comparison.tsx | 20 +- apps/web/src/docs/examples/image-zoom.tsx | 6 + apps/web/src/docs/examples/item-media.tsx | 9 +- apps/web/src/docs/source.server.ts | 41 +- apps/web/src/router.tsx | 4 +- apps/web/src/routes/_docs/docs.$.tsx | 6 +- apps/web/src/routes/_main.tsx | 7 + apps/web/src/routes/docs.search.ts | 6 +- apps/web/src/routes/docs.{$}[.]md.ts | 11 +- apps/web/src/routes/llms-full[.]txt.ts | 27 +- apps/web/src/routes/llms[.]txt.ts | 19 +- apps/web/src/server/vitnode-api.server.ts | 6 + .../hero-wall/hero-wall-columns.tsx | 2 +- apps/web/src/site/marketing/marketing.css | 1 + apps/web/src/site/marketing/shared.tsx | 2 +- apps/web/src/site/plugins/plugins-page.tsx | 2 +- apps/web/src/site/solutions/solution-page.tsx | 2 +- apps/web/src/styles.css | 1 - .../copy-of-vitnode-app/root/src/router.tsx | 4 +- packages/elasticsearch/src/index.ts | 62 +- packages/node-cron/src/index.ts | 11 +- packages/nodemailer/src/index.ts | 47 +- packages/s3/src/index.ts | 20 +- packages/supabase-storage/src/index.ts | 50 +- .../vitnode/src/api/adapters/events/local.ts | 34 +- .../src/api/adapters/search/postgres.ts | 57 +- .../vitnode/src/api/adapters/sso/discord.ts | 5 + .../vitnode/src/api/adapters/sso/facebook.ts | 11 +- .../vitnode/src/api/adapters/sso/google.ts | 5 + packages/vitnode/src/api/config.ts | 43 +- packages/vitnode/src/api/lib/ai/alt.ts | 73 +- .../src/api/lib/ai/available-actions.ts | 37 +- packages/vitnode/src/api/lib/ai/ledger.ts | 13 + .../vitnode/src/api/lib/ai/memory-ledger.ts | 31 + .../vitnode/src/api/lib/ai/postgres-ledger.ts | 198 +- packages/vitnode/src/api/lib/ai/runner.ts | 1 + .../vitnode/src/api/lib/ai/stream-response.ts | 2 + .../vitnode/src/api/lib/ai/usage-summary.ts | 54 +- packages/vitnode/src/api/lib/cache-memory.ts | 116 + packages/vitnode/src/api/lib/cache.ts | 100 +- .../src/api/lib/check-staff-permission.ts | 108 +- packages/vitnode/src/api/lib/cron.ts | 10 + .../src/api/lib/staff-permission-cache.ts | 83 +- .../src/api/lib/with-pagination.test.ts | 6 +- .../vitnode/src/api/lib/with-pagination.ts | 23 +- .../src/api/middlewares/captcha.middleware.ts | 22 +- .../src/api/middlewares/global.middleware.ts | 84 +- packages/vitnode/src/api/models/device.ts | 57 +- packages/vitnode/src/api/models/email.ts | 40 +- .../src/api/models/notifications/admin.ts | 69 +- .../src/api/models/notifications/danger.ts | 4 + .../src/api/models/notifications/email.ts | 57 +- .../src/api/models/notifications/fanout.ts | 43 +- .../src/api/models/notifications/inbox.ts | 66 +- .../src/api/models/notifications/render.ts | 29 +- .../src/api/models/notifications/shared.ts | 22 +- .../vitnode/src/api/models/search.test.ts | 31 +- packages/vitnode/src/api/models/search.ts | 155 +- .../admin/debug/routes/search-status.route.ts | 44 +- .../modules/admin/roles/routes/list.route.ts | 106 +- .../admin/staff/lib/resolve-staff-edges.ts | 66 +- .../src/api/modules/ai/cron/alt.cron.ts | 4 + .../src/api/modules/content/content.module.ts | 7 +- .../tasks/content-search-dependents.task.ts | 19 + .../src/api/modules/cron/cron/clean.cron.ts | 6 +- .../src/api/modules/cron/routes/cron.route.ts | 77 +- .../moderation/routes/approvals.routes.ts | 12 +- .../modules/moderation/routes/list.route.ts | 46 +- .../queue/helpers/process-queue-tasks.ts | 243 +- .../vitnode/src/components/date-format.tsx | 2 +- .../vitnode/src/components/form/auto-form.tsx | 99 +- .../image-crop/image-crop-fields.tsx | 49 +- .../vitnode/src/components/reveal-panel.tsx | 44 +- .../switchers/themes/theme-switcher.tsx | 4 +- .../vitnode/src/components/table/content.tsx | 99 +- .../components/table/data-table-content.tsx | 2 +- .../src/components/table/reorder-handle.tsx | 50 + .../src/components/table/reorder-state.ts | 14 +- .../src/components/table/reorder.test.tsx | 16 +- .../vitnode/src/components/table/reorder.tsx | 128 +- .../vitnode/src/components/table/row-order.ts | 93 + .../vitnode/src/components/theme-provider.tsx | 6 +- .../src/components/tiptap/tiptap-editor.tsx | 108 +- .../toolbar/actions/emoji/emoji-picker.tsx | 9 +- .../vitnode/src/components/ui/comparison.tsx | 9 +- .../src/components/ui/editor-content.tsx | 8 +- .../vitnode/src/components/ui/icon-picker.tsx | 18 +- .../vitnode/src/components/ui/image-zoom.tsx | 11 + .../vitnode/src/components/ui/sidebar.tsx | 8 +- .../src/content/approvals/server/queue.ts | 11 +- .../src/content/containers/server/service.ts | 29 +- .../vitnode/src/content/containers/tree.ts | 7 +- .../vitnode/src/content/define-public-api.ts | 58 +- .../src/content/server/advanced-store.ts | 32 +- .../src/content/server/delivery-effects.ts | 9 +- .../src/content/server/delivery-service.ts | 19 +- .../src/content/server/duplicate-effects.ts | 5 +- .../src/content/server/editorial-effects.ts | 175 +- .../src/content/server/editorial-service.ts | 52 +- packages/vitnode/src/content/server/emit.ts | 35 +- .../src/content/server/language-resolver.ts | 53 +- .../src/content/server/live-routes.test.ts | 12 +- .../vitnode/src/content/server/live-routes.ts | 20 +- .../vitnode/src/content/server/live-store.ts | 153 +- .../content/server/localized-admin-routes.ts | 11 +- .../server/localized-public-service.ts | 135 +- .../vitnode/src/content/server/page-cache.ts | 138 +- .../src/content/server/public-cache.ts | 167 + .../src/content/server/public-routes.ts | 62 +- .../content/server/public-row-hydration.ts | 152 +- .../src/content/server/public-service.ts | 117 +- .../src/content/server/revalidate-bridge.ts | 34 +- packages/vitnode/src/content/server/routes.ts | 52 +- .../src/content/server/schedule-effects.ts | 5 +- .../vitnode/src/content/server/search-sync.ts | 167 +- .../vitnode/src/content/server/service.ts | 188 +- .../src/content/server/translation-effects.ts | 36 +- .../src/content/server/write-effects.ts | 15 +- packages/vitnode/src/content/types.ts | 15 +- packages/vitnode/src/database/admins.ts | 1 + packages/vitnode/src/database/ai.ts | 2 + packages/vitnode/src/database/files.ts | 3 +- packages/vitnode/src/database/moderation.ts | 2 + .../vitnode/src/database/notifications.ts | 4 + packages/vitnode/src/database/queue.ts | 5 + packages/vitnode/src/database/sessions.ts | 1 + packages/vitnode/src/database/users.ts | 1 + .../editor/block-shell/block-shell.test.tsx | 9 +- .../src/editor/block-shell/block-shell.tsx | 24 +- .../vitnode/src/editor/block-shell/issue.ts | 42 +- packages/vitnode/src/editor/context.ts | 38 +- .../src/editor/dnd/use-catalog-draggable.ts | 4 +- .../src/editor/dnd/use-sortable-node.ts | 4 +- .../vitnode/src/editor/properties/panel.tsx | 56 +- .../vitnode/src/editor/properties/variant.tsx | 4 +- packages/vitnode/src/editor/provider.tsx | 35 + packages/vitnode/src/editor/root.tsx | 6 +- .../src/editor/sidebar/panel-header.tsx | 4 +- .../src/editor/sidebar/sidebar.test.tsx | 6 +- .../vitnode/src/editor/zones/block-body.tsx | 66 +- .../src/editor/zones/editable-zone.tsx | 80 +- .../src/editor/zones/invalid-entries.tsx | 4 +- packages/vitnode/src/emails/ui/button.tsx | 2 +- .../src/framework/vite/ssr-externals.ts | 9 +- packages/vitnode/src/lib/fetcher/core.ts | 53 +- .../lib/fetcher/forwarded-signature.server.ts | 16 +- .../src/lib/fetcher/in-process-api.server.ts | 29 + packages/vitnode/src/lib/fetcher/raw.ts | 32 +- .../src/lib/i18n/load-messages.test.ts | 2 +- .../vitnode/src/lib/i18n/load-messages.ts | 14 +- .../vitnode/src/page-cache/store.server.ts | 34 +- .../src/tanstack/admin/content/query.test.ts | 4 + .../src/tanstack/auth/session-query.ts | 16 + .../vitnode/src/tanstack/fetcher/server.ts | 118 +- .../vitnode/src/tanstack/layout/header.tsx | 4 +- .../vitnode/src/tanstack/page-cache/server.ts | 39 + .../src/tanstack/profile/route.test.ts | 34 +- .../vitnode/src/tanstack/profile/route.ts | 34 + packages/vitnode/src/tests/cache.ts | 2 + .../views/content/lib/invalidate.test.ts | 21 +- .../admin/views/content/lib/invalidate.ts | 18 +- .../live/collaborative-editor.test.tsx | 43 +- .../views/admin/views/content/live/status.tsx | 1 + .../system/integrations/integration-card.tsx | 2 +- .../integrations/test-storage/content.tsx | 9 +- packages/vitnode/src/ws/manager.ts | 21 +- packages/vitnode/src/ws/registry.ts | 100 +- plugins/blog/src/pages/post-page.tsx | 3 + .../views/admin/article/editor/seo-audit.tsx | 4 +- plugins/example/src/content/discussion.ts | 2 + plugins/example/src/pages/board-page.tsx | 67 +- plugins/example/src/pages/boards-page.tsx | 20 +- .../example/src/pages/discussion-new-page.tsx | 20 +- plugins/example/src/pages/discussion-page.tsx | 39 +- plugins/example/src/pages/example-queries.ts | 97 + plugins/forum/src/api/lib/events.ts | 93 + plugins/forum/src/api/lib/stats-cache.ts | 50 + .../src/api/modules/forum/forum.module.ts | 5 +- .../src/api/modules/forum/stats.routes.ts | 156 +- .../src/api/modules/forum/topics.routes.ts | 115 + plugins/forum/src/content/topic.ts | 10 + plugins/forum/src/locales/en.json | 3 +- plugins/forum/src/pages/comment-composer.tsx | 17 +- plugins/forum/src/pages/forum-data.ts | 33 +- plugins/forum/src/pages/forum-page.tsx | 58 +- plugins/forum/src/pages/forum-queries.ts | 34 + plugins/forum/src/pages/pending-page.tsx | 10 +- plugins/forum/src/pages/thread-data.ts | 25 +- plugins/forum/src/pages/thread.tsx | 227 +- plugins/forum/src/pages/topic-page.tsx | 4 +- turbo.json | 5 +- 227 files changed, 26152 insertions(+), 2142 deletions(-) create mode 100644 apps/api/migrations/20261018101300_hot_path_indexes/migration.sql create mode 100644 apps/api/migrations/20261018101300_hot_path_indexes/snapshot.json create mode 100644 packages/vitnode/src/api/lib/cache-memory.ts create mode 100644 packages/vitnode/src/api/modules/content/tasks/content-search-dependents.task.ts create mode 100644 packages/vitnode/src/components/table/reorder-handle.tsx create mode 100644 packages/vitnode/src/components/table/row-order.ts create mode 100644 packages/vitnode/src/content/server/public-cache.ts create mode 100644 packages/vitnode/src/editor/provider.tsx create mode 100644 packages/vitnode/src/lib/fetcher/in-process-api.server.ts create mode 100644 plugins/example/src/pages/example-queries.ts create mode 100644 plugins/forum/src/api/lib/events.ts create mode 100644 plugins/forum/src/api/lib/stats-cache.ts create mode 100644 plugins/forum/src/api/modules/forum/topics.routes.ts diff --git a/apps/api/migrations/20261018101300_hot_path_indexes/migration.sql b/apps/api/migrations/20261018101300_hot_path_indexes/migration.sql new file mode 100644 index 0000000000..f9ff5c249c --- /dev/null +++ b/apps/api/migrations/20261018101300_hot_path_indexes/migration.sql @@ -0,0 +1,9 @@ +DROP INDEX "core_files_user_id_idx";--> statement-breakpoint +CREATE INDEX "core_admin_sessions_expires_at_idx" ON "core_admin_sessions" ("expiresAt");--> statement-breakpoint +CREATE INDEX "core_ai_runs_user_id_idx" ON "core_ai_runs" ("userId","id");--> statement-breakpoint +CREATE INDEX "core_files_user_created_at_id_idx" ON "core_files" ("userId","createdAt","id");--> statement-breakpoint +CREATE INDEX "core_moderation_reports_created_at_id_idx" ON "core_moderation_reports" ("createdAt","id");--> statement-breakpoint +CREATE INDEX "core_notification_deliveries_event_idx" ON "core_notification_deliveries" ("eventId") WHERE "eventId" IS NOT NULL;--> statement-breakpoint +CREATE INDEX "core_queue_completed_at_idx" ON "core_queue" ("completedAt") WHERE "completedAt" IS NOT NULL;--> statement-breakpoint +CREATE INDEX "core_sessions_expires_at_idx" ON "core_sessions" ("expiresAt");--> statement-breakpoint +CREATE INDEX "core_users_role_id_idx" ON "core_users" ("roleId"); \ No newline at end of file diff --git a/apps/api/migrations/20261018101300_hot_path_indexes/snapshot.json b/apps/api/migrations/20261018101300_hot_path_indexes/snapshot.json new file mode 100644 index 0000000000..3fcf722f34 --- /dev/null +++ b/apps/api/migrations/20261018101300_hot_path_indexes/snapshot.json @@ -0,0 +1,20069 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "e825a09f-4603-48ff-b6ad-f406a8062fe5", + "prevIds": [ + "05ff21f4-ccac-4428-a015-8aec4547b8ea" + ], + "ddl": [ + { + "isRlsEnabled": true, + "name": "core_admin_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_admin_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_action_settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_budget_periods", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_calls", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_cost_adjustments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_reservations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_role_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_role_policies", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_runs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_translation_sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_ai_user_overrides", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_containers", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_documents", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_drafts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_field_locks", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_submissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_file_refs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_revisions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_schedules", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_slug_history", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_cron", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_admin_dashboard", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files_alt", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files_alt_analysis", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files_alt_state", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_languages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_languages_words", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_logs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_report_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderation_reports", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderators_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_deliveries", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_receipts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_settings", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notification_user_state", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_notifications", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_page_layouts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_passkey_challenges", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_passkeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_queue", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_search_index", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_search_popularity", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_secrets", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sessions_known_devices", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_update_lock", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_update_results", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_update_sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_confirm_emails", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_forgot_password", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_secondary_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso_operations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso_profile_sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_categories_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_author_id", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_category_id", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_faq", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_related_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_articles_gallery", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_discussions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_events_gallery", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_events_related_events", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_events_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_events_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_events_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_localized_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_localized_articles_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_pages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_tags_related_tags", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_tags_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "forum_comments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "forum_topics", + "entityType": "tables", + "schema": "public" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unrestricted", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeen", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actionKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "modelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fallbackModelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxInputCharacters", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxOutputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timeoutMs", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxRetries", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxSteps", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dailyLimit", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "instructions", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_action_settings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "unit", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodEnd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "limitAmount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "spentAmount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "reservedAmount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "unknownCount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "attempt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "modelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerModelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerRequestId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorCode", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "outputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheReadTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheWriteTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoningTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "images", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costReason", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pricingVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pricingSnapshot", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "finishedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reconciledAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_calls" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "callId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "previousCostUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "previousSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "newCostUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "newSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deltaUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "budgetPeriodId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "amount", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "settledAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permission", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "granted", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dailyLimit", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unlimited", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actionKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorType", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resourceType", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resourceId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "errorCode", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "promptVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pointsConversionVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "modelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerModelId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "outputTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheReadTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cacheWriteTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reasoningTokens", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "costSource", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chargedUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "chargedPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "reservedUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "reservedPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "settlement", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "leaseExpiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accepted", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "acceptedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "finishedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "settledAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyBudgetUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "systemMonthlyBudgetUsd", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "defaultMonthlyPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "pointsConversionVersion", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "userRequestsPerMinute", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "2", + "generated": null, + "identity": null, + "name": "userConcurrency", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "2", + "generated": null, + "identity": null, + "name": "systemConcurrency", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "365", + "generated": null, + "identity": null, + "name": "historyRetentionDays", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "altEnabled", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "10", + "generated": null, + "identity": null, + "name": "altBatchSize", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "altScanCursor", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "altScanStartedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_settings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "locale", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceLocale", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "origin", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedById", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "numeric(24,12)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "monthlyPoints", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unlimited", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "blocked", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "definitionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "policies", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_containers" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "varchar(35)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseVersion", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedBy", + "entityType": "columns", + "schema": "public", + "table": "core_content_documents" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "values", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "baseVersion", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedBy", + "entityType": "columns", + "schema": "public", + "table": "core_content_drafts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "acquiredAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerDefinitionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "values", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseValues", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseVersion", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "baseUpdatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authorId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reviewerId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "decidedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_submissions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "operation", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "snapshot", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "changedFields", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'system'", + "generated": null, + "identity": null, + "name": "actorType", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorUserId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "restoredFromRevisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scheduledFor", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "generation", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "effectsError", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "retiredAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastRun", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "module", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nextRun", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "schedule", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "widgets", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folder", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'automatic'", + "generated": null, + "identity": null, + "name": "altPolicy", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "text", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "origin", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedById", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileFingerprint", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "runId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'UTC'", + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "default", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "time24", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginCode", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tableName", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "variable", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(45)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'GET'", + "generated": null, + "identity": null, + "name": "method", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'localhost'", + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "500", + "generated": null, + "identity": null, + "name": "statusCode", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reportId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetType", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reporterId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "snapshot", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "targetTitle", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'new'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "assigneeId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolution", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolutionNote", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolvedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resolvedById", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeFamily", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scopeId", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "openKey", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unrestricted", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "bigserial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mode", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5", + "generated": null, + "identity": null, + "name": "maxAttempts", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "availableAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodStart", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "periodEnd", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "itemCount", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerMessageId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(500)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "skipReason", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sentAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "type": "bigserial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "schemaVersion", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "idempotencyKey", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectType", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "groupKey", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "data", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "recipientIds", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowSelf", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "recipientCursor", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "deliveredCount", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_events" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "eventId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "notificationId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seq", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailPending", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "emailDeliveryId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "core_notification_settings" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_notification_settings" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "unreadCount", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "preferences", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "type": "bigserial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "groupKey", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "groupBucket", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectType", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subjectId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "latestEventId", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "eventCount", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "activitySeq", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "readSeq", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "readAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "archivedAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastActivityAt", + "entityType": "columns", + "schema": "public", + "table": "core_notifications" + }, + { + "type": "varchar(120)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageId", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "zones", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ceremony", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "challenge", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthnUserId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(1024)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credentialId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicKey", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "counter", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthnUserId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "transports", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceType", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "backedUp", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(36)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aaguid", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastUsedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'default'", + "generated": null, + "identity": null, + "name": "queue", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "priority", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "3", + "generated": null, + "identity": null, + "name": "maxAttempts", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "availableAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reservedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dedupeKey", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "default", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "root", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "guest", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prefix", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowUploadFiles", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "totalMaxStorage", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxStorageForSubmit", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowUploadAvatar", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "2048", + "generated": null, + "identity": null, + "name": "maxAvatarSize", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowUploadCover", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5120", + "generated": null, + "identity": null, + "name": "maxCoverSize", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowEditPersonalInfo", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemType", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "authorIds", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector(CASE lower(split_part(\"core_search_index\".\"languageCode\", '-', 1)) WHEN 'da' THEN 'danish'::regconfig WHEN 'de' THEN 'german'::regconfig WHEN 'en' THEN 'english'::regconfig WHEN 'es' THEN 'spanish'::regconfig WHEN 'fi' THEN 'finnish'::regconfig WHEN 'fr' THEN 'french'::regconfig WHEN 'hu' THEN 'hungarian'::regconfig WHEN 'it' THEN 'italian'::regconfig WHEN 'nl' THEN 'dutch'::regconfig WHEN 'no' THEN 'norwegian'::regconfig WHEN 'pl' THEN 'polish'::regconfig WHEN 'pt' THEN 'portuguese'::regconfig WHEN 'ro' THEN 'romanian'::regconfig WHEN 'ru' THEN 'russian'::regconfig WHEN 'sv' THEN 'swedish'::regconfig WHEN 'tr' THEN 'turkish'::regconfig ELSE 'simple'::regconfig END, coalesce(\"core_search_index\".\"title\", '')), 'A') || setweight(to_tsvector(CASE lower(split_part(\"core_search_index\".\"languageCode\", '-', 1)) WHEN 'da' THEN 'danish'::regconfig WHEN 'de' THEN 'german'::regconfig WHEN 'en' THEN 'english'::regconfig WHEN 'es' THEN 'spanish'::regconfig WHEN 'fi' THEN 'finnish'::regconfig WHEN 'fr' THEN 'french'::regconfig WHEN 'hu' THEN 'hungarian'::regconfig WHEN 'it' THEN 'italian'::regconfig WHEN 'nl' THEN 'dutch'::regconfig WHEN 'no' THEN 'norwegian'::regconfig WHEN 'pl' THEN 'polish'::regconfig WHEN 'pt' THEN 'portuguese'::regconfig WHEN 'ro' THEN 'romanian'::regconfig WHEN 'ru' THEN 'russian'::regconfig WHEN 'sv' THEN 'swedish'::regconfig WHEN 'tr' THEN 'turkish'::regconfig ELSE 'simple'::regconfig END, coalesce(\"core_search_index\".\"content\", '')), 'B')", + "type": "stored" + }, + "identity": null, + "name": "search_vector", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerType", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isPublic", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isHidden", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "imageId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "indexedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemType", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logScore", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "views", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "clicks", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "comments", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_popularity" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeen", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "owner", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastManualAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_lock" + }, + { + "type": "varchar(214)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "componentId", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sourceUrl", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "channel", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "versions", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hasUnknownVersion", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(256)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coreVersion", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "result", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastAttemptAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSuccessAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_update_results" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "etag", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastModified", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "document", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastAttemptAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastSuccessAt", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_update_sources" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nameCode", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "phone", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "headline", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "showRealName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "newsletter", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(6)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarColor", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "birthday", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timeZone", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerEmail", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerUsername", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "syncOnSignIn", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarSourceUrl", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarSha256", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarFileId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "intent", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "fields", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "preview", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverImage", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "friendlyUrl", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "excerpt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverImageAlt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "syndicationIndexable", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "syndicationNoIndex", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5", + "generated": null, + "identity": null, + "name": "syndicationPriority", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "question", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "answer", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seoTitle", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seoDescription", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "excerpt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "views", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "featured", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "noIndex", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "animation", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "board", + "entityType": "columns", + "schema": "public", + "table": "example_discussions" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "byDefault", + "name": "example_events_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_events" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "example_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "example_events" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "primaryTag", + "entityType": "columns", + "schema": "public", + "table": "example_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "example_events" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_events_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_events_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_events_gallery" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_events_gallery" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_events_related_events" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_events_related_events" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_events_related_events" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_events_related_events" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_events_sessions" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_events_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_events_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_events_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_events_sessions" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_events_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "startsAt", + "entityType": "columns", + "schema": "public", + "table": "example_events_sessions" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_events_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_events_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_events_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_events_tags" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_events_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "example_events_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_events_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_events_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_events_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_events_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_events_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_events_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_events_translations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "example_events_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "featured", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "gen_random_uuid()", + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_tags" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "example_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "example_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "example_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_tags_related_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_tags_related_tags" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_tags_related_tags" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_tags_related_tags" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_tags_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "example_tags_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_tags_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_tags_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_tags_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_tags_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_tags_translations" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "example_tags_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_tags_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "topic", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parent", + "entityType": "columns", + "schema": "public", + "table": "forum_comments" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenAt", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hiddenBy", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "forum", + "entityType": "columns", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deviceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_device_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "scopeKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "periodStart", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_budget_periods_scope_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_budget_periods" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "runId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_calls_run_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_calls" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "costSource", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "reconciledAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_calls_reconcile_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_calls" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "callId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_cost_adjustments_call_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "runId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "budgetPeriodId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_reservations_run_period_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_reservations_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_user_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actionKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_action_created_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "leaseExpiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actorType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "idempotencyKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"idempotencyKey\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_runs_idempotency_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "locale", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "field", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_ai_translation_sources_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "definitionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_containers_definition_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "definitionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_containers_definition_parent_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_containers_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "field", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_documents_doc_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_documents_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_documents_updated_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_drafts_key_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_drafts_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_drafts_updated_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "field", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_field_locks_key_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_field_locks_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_field_locks_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"status\" = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_pending_key", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "containerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_queue_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "authorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_submissions_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "revisionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_file_refs_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_file_refs_file_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_item_version_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_translation_version_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_language_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actorUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_actor_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "action", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "status = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "scheduledFor", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_due_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_shared_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_locale_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastRun", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_cron_last_run_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_cron" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_dashboard_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_user_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_alt_file_language_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "fileFingerprint", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_alt_analysis_file_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status <> 'completed'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_alt_state_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_words_lang_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tableName", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "pluginCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "variable", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_words_lookup_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_logs_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_logs_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "reportId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_report_events_report_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "targetId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_target_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "assigneeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_assignee_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "reporterId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_reporter_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "scopeFamily", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "scopeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderation_reports_scope_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "availableAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "status = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_claim_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "eventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"eventId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_deliveries_event_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "idempotencyKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_events_idempotency_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_events_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_events_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "notificationId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "seq", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_receipts_notification_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_receipts_user_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"emailPending\" = true AND \"emailDeliveryId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_receipts_email_pending_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "unreadCount", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notification_user_state_unread_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "groupKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "groupBucket", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_group_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastActivityAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archivedAt\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_inbox_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lastActivityAt", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"archivedAt\" IS NULL AND \"readSeq\" < \"activitySeq\"", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_unread_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "subjectType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "subjectId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_subject_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastActivityAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_last_activity_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "latestEventId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_notifications_latest_event_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkey_challenges_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkey_challenges_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkeys_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "webauthnUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkeys_webauthn_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "availableAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_status_available_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "completedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": "\"completedAt\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_completed_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "dedupeKey", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"dedupeKey\" IS NOT NULL AND status IN ('pending', 'processing')", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_dedupe_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_roles_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "search_vector", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_search_vector_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "to_tsvector('simple'::regconfig, \"title\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_title_vector_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "authorIds", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_author_ids_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_item_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_language_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isPublic", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_is_public_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "imageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_image_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "containerType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "containerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_container_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "logScore", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_popularity_log_score_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_popularity" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "deviceId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_device_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ipAddress", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_known_devices_ip_address_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "avatarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_avatar_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "coverId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_cover_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_secondary_roles_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_operations_user_provider_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_operations_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_translations_language_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_status_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "coverImage", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_cover_image_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_author_id_position_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_author_id_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_category_id_position_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_category_id_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "friendlyUrl", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_translations_language_id_friendly_url_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "syndicationPriority", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_syndication_priority_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_categories_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_categories_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_faq_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_related_articles_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_related_articles_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_tags_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_tags_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_status_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_code_key", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "animation", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_animation_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_gallery_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_gallery_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_categories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_categories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_discussions_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_discussions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_discussions_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_discussions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "board", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_discussions_board_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_discussions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_discussions_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_discussions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_discussions_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_discussions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_discussions_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_discussions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_discussions_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_discussions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "primaryTag", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_primary_tag_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_gallery_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_events_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_gallery_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_events_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_related_events_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_events_related_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_related_events_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_events_related_events" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_sessions_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_events_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_tags_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_events_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_tags_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_events_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_events_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_events_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_events_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_tags_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_tags_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_tags_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_tags_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_tags_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_tags_related_tags_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_tags_related_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_tags_related_tags_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_tags_related_tags" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_tags_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_tags_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_tags_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_tags_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "topic", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_topic_parent_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "topic", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_topic_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parent", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_comments_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "forum", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_forum_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hiddenBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forum_topics_hidden_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_permissions_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_permissions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_sessions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "deviceId" + ], + "schemaTo": "public", + "tableTo": "core_sessions_known_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_calls_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_calls" + }, + { + "nameExplicit": false, + "columns": [ + "callId" + ], + "schemaTo": "public", + "tableTo": "core_ai_calls", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_cost_adjustments_callId_core_ai_calls_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_cost_adjustments_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_cost_adjustments" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_reservations_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": false, + "columns": [ + "budgetPeriodId" + ], + "schemaTo": "public", + "tableTo": "core_ai_budget_periods", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_reservations_FClYpPXZwvsm_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_reservations" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_role_permissions_roleId_core_roles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_role_policies_roleId_core_roles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_role_policies" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_ai_runs_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_runs" + }, + { + "nameExplicit": false, + "columns": [ + "updatedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_ai_translation_sources_updatedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_translation_sources" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_ai_user_overrides_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_ai_user_overrides" + }, + { + "nameExplicit": false, + "columns": [ + "parentId" + ], + "schemaTo": "public", + "tableTo": "core_content_containers", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "core_content_containers_q6QIucJ4HJ7B_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_containers" + }, + { + "nameExplicit": false, + "columns": [ + "updatedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_documents_updatedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_documents" + }, + { + "nameExplicit": false, + "columns": [ + "updatedBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_drafts_updatedBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_drafts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "core_content_field_locks_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_field_locks" + }, + { + "nameExplicit": false, + "columns": [ + "authorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_content_submissions_authorId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": false, + "columns": [ + "reviewerId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_content_submissions_reviewerId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_submissions" + }, + { + "nameExplicit": false, + "columns": [ + "revisionId" + ], + "schemaTo": "public", + "tableTo": "core_content_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "core_content_file_refs_5get6SfhBPHr_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "core_content_file_refs_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": false, + "columns": [ + "actorUserId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_revisions_actorUserId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": false, + "columns": [ + "createdBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_schedules_createdBy_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_dashboard_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "languageCode" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_languageCode_core_languages_code_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_alt_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "updatedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_alt_updatedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_analysis_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "nameExplicit": false, + "columns": [ + "runId" + ], + "schemaTo": "public", + "tableTo": "core_ai_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_alt_analysis_runId_core_ai_runs_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt_analysis" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_files_alt_state_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_files_alt_state" + }, + { + "nameExplicit": false, + "columns": [ + "languageCode" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_languages_words_languageCode_core_languages_code_fk", + "entityType": "fks", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_logs_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": false, + "columns": [ + "reportId" + ], + "schemaTo": "public", + "tableTo": "core_moderation_reports", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderation_report_events_4CqHOxfheaUm_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "nameExplicit": false, + "columns": [ + "actorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_report_events_actorId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_report_events" + }, + { + "nameExplicit": false, + "columns": [ + "reporterId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_reports_reporterId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": false, + "columns": [ + "assigneeId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_reports_assigneeId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": false, + "columns": [ + "resolvedById" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_moderation_reports_resolvedById_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_moderation_reports" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderators_permissions_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderators_permissions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_deliveries_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": false, + "columns": [ + "eventId" + ], + "schemaTo": "public", + "tableTo": "core_notification_events", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_notification_deliveries_vcf37BytIa0M_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_deliveries" + }, + { + "nameExplicit": false, + "columns": [ + "actorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_notification_events_actorId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_events" + }, + { + "nameExplicit": false, + "columns": [ + "eventId" + ], + "schemaTo": "public", + "tableTo": "core_notification_events", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_receipts_UQ7ZEs0I6ijx_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_receipts_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": false, + "columns": [ + "notificationId" + ], + "schemaTo": "public", + "tableTo": "core_notifications", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_receipts_Vxye2Cs6MgcL_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notification_user_state_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notification_user_state" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_notifications_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": false, + "columns": [ + "latestEventId" + ], + "schemaTo": "public", + "tableTo": "core_notification_events", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "core_notifications_I8QoGbTal5Wt_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_notifications" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_passkey_challenges_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_passkeys_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": false, + "columns": [ + "imageId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_search_index_imageId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_sessions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "deviceId" + ], + "schemaTo": "public", + "tableTo": "core_sessions_known_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "core_users_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "language" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET DEFAULT", + "name": "core_users_language_core_languages_code_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "avatarId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_avatarId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "coverId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_coverId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_confirm_emails_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_forgot_password_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_secondary_roles_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_secondary_roles_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "avatarFileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_sso_avatarFileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_operations_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_profile_sources_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "nameExplicit": true, + "columns": [ + "userId", + "providerId" + ], + "schemaTo": "public", + "tableTo": "core_users_sso", + "columnsTo": [ + "userId", + "providerId" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_profile_sources_connection_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_categories_translations_itemId_blog_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_categories_translations_languageId_core_languages_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "blog_posts_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": false, + "columns": [ + "coverImage" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_coverImage_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_author_id_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_author_id_relatedItemId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_category_id_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "blog_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_category_id_relatedItemId_blog_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_translations_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_translations_languageId_core_languages_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_categories_itemId_example_advanced_ar", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "example_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_advanced_articles_categories_relatedItemId_example_cate", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_faq_itemId_example_advanced_articles_", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_related_articles_itemId_example_advan", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_related_articles_relatedItemId_exampl", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_tags_ATchHLRtvoCM_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_tags" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "example_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_advanced_articles_tags_dEsWdkDmu7nW_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_translations_itemId_example_advanced_", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_advanced_articles_translations_languageId_core_language", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "example_articles_author_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": false, + "columns": [ + "animation" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_articles_animation_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "example_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_articles_category_example_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_articles_gallery_itemId_example_articles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_articles_gallery_relatedItemId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "example_discussions_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_discussions" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "example_discussions_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_discussions" + }, + { + "nameExplicit": false, + "columns": [ + "board" + ], + "schemaTo": "public", + "tableTo": "core_content_containers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_discussions_board_core_content_containers_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_discussions" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "example_events_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events" + }, + { + "nameExplicit": false, + "columns": [ + "primaryTag" + ], + "schemaTo": "public", + "tableTo": "example_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "example_events_primaryTag_example_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "example_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "example_events_category_example_categories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_events_gallery_itemId_example_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_events_gallery_relatedItemId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_events_related_events_itemId_example_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events_related_events" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "example_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_events_related_events_oaRvqnKyAS7L_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events_related_events" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_events_sessions_itemId_example_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_events_tags_itemId_example_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events_tags" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "example_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_events_tags_relatedItemId_example_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_events", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_events_translations_itemId_example_events_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_events_translations_languageId_core_languages_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_events_translations" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_localized_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_localized_articles_translations_itemId_example_localize", + "entityType": "fks", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_localized_articles_translations_languageId_core_languag", + "entityType": "fks", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "example_tags_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_tags" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "example_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "example_tags_category_example_categories_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_tags_related_tags_itemId_example_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_tags_related_tags" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "example_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_tags_related_tags_relatedItemId_example_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_tags_related_tags" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_tags", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_tags_translations_itemId_example_tags_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_tags_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_tags_translations_languageId_core_languages_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_tags_translations" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_replies_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_replies_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "topic" + ], + "schemaTo": "public", + "tableTo": "forum_topics", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "forum_replies_topic_forum_topics_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "parent" + ], + "schemaTo": "public", + "tableTo": "forum_comments", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_replies_parent_forum_replies_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_comments" + }, + { + "nameExplicit": false, + "columns": [ + "hiddenBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_topics_hiddenBy_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "forum_topics_author_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_topics" + }, + { + "nameExplicit": false, + "columns": [ + "forum" + ], + "schemaTo": "public", + "tableTo": "core_content_containers", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "forum_topics_forum_core_content_containers_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "forum_topics" + }, + { + "columns": [ + "roleId", + "permission" + ], + "nameExplicit": false, + "name": "core_ai_role_permissions_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_ai_role_permissions" + }, + { + "columns": [ + "eventId", + "userId" + ], + "nameExplicit": false, + "name": "core_notification_receipts_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_notification_receipts" + }, + { + "columns": [ + "itemType", + "itemId" + ], + "nameExplicit": false, + "name": "core_search_popularity_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_search_popularity" + }, + { + "columns": [ + "userId", + "roleId" + ], + "nameExplicit": false, + "name": "core_users_secondary_roles_userId_roleId_pk", + "entityType": "pks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "columns": [ + "userId", + "field" + ], + "nameExplicit": false, + "name": "core_users_sso_profile_sources_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "blog_categories_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "blog_posts_author_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "blog_posts_category_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "blog_posts_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_advanced_articles_categories_pk", + "entityType": "pks", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_advanced_articles_related_articles_pk", + "entityType": "pks", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_advanced_articles_tags_pk", + "entityType": "pks", + "schema": "public", + "table": "example_advanced_articles_tags" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "example_advanced_articles_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_articles_gallery_pk", + "entityType": "pks", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_events_gallery_pk", + "entityType": "pks", + "schema": "public", + "table": "example_events_gallery" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_events_related_events_pk", + "entityType": "pks", + "schema": "public", + "table": "example_events_related_events" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_events_tags_pk", + "entityType": "pks", + "schema": "public", + "table": "example_events_tags" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "example_events_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "example_events_translations" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "example_localized_articles_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_tags_related_tags_pk", + "entityType": "pks", + "schema": "public", + "table": "example_tags_related_tags" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "example_tags_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "example_tags_translations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_permissions_pkey", + "schema": "public", + "table": "core_admin_permissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_sessions_pkey", + "schema": "public", + "table": "core_admin_sessions", + "entityType": "pks" + }, + { + "columns": [ + "actionKey" + ], + "nameExplicit": false, + "name": "core_ai_action_settings_pkey", + "schema": "public", + "table": "core_ai_action_settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_budget_periods_pkey", + "schema": "public", + "table": "core_ai_budget_periods", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_calls_pkey", + "schema": "public", + "table": "core_ai_calls", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_cost_adjustments_pkey", + "schema": "public", + "table": "core_ai_cost_adjustments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_reservations_pkey", + "schema": "public", + "table": "core_ai_reservations", + "entityType": "pks" + }, + { + "columns": [ + "roleId" + ], + "nameExplicit": false, + "name": "core_ai_role_policies_pkey", + "schema": "public", + "table": "core_ai_role_policies", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_runs_pkey", + "schema": "public", + "table": "core_ai_runs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_settings_pkey", + "schema": "public", + "table": "core_ai_settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_ai_translation_sources_pkey", + "schema": "public", + "table": "core_ai_translation_sources", + "entityType": "pks" + }, + { + "columns": [ + "userId" + ], + "nameExplicit": false, + "name": "core_ai_user_overrides_pkey", + "schema": "public", + "table": "core_ai_user_overrides", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_containers_pkey", + "schema": "public", + "table": "core_content_containers", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_documents_pkey", + "schema": "public", + "table": "core_content_documents", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_drafts_pkey", + "schema": "public", + "table": "core_content_drafts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_field_locks_pkey", + "schema": "public", + "table": "core_content_field_locks", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_submissions_pkey", + "schema": "public", + "table": "core_content_submissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_file_refs_pkey", + "schema": "public", + "table": "core_content_file_refs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_revisions_pkey", + "schema": "public", + "table": "core_content_revisions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_schedules_pkey", + "schema": "public", + "table": "core_content_schedules", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_slug_history_pkey", + "schema": "public", + "table": "core_content_slug_history", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_cron_pkey", + "schema": "public", + "table": "core_cron", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_dashboard_pkey", + "schema": "public", + "table": "core_admin_dashboard", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_files_pkey", + "schema": "public", + "table": "core_files", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_files_alt_pkey", + "schema": "public", + "table": "core_files_alt", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_files_alt_analysis_pkey", + "schema": "public", + "table": "core_files_alt_analysis", + "entityType": "pks" + }, + { + "columns": [ + "fileId" + ], + "nameExplicit": false, + "name": "core_files_alt_state_pkey", + "schema": "public", + "table": "core_files_alt_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_languages_pkey", + "schema": "public", + "table": "core_languages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_languages_words_pkey", + "schema": "public", + "table": "core_languages_words", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_logs_pkey", + "schema": "public", + "table": "core_logs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_report_events_pkey", + "schema": "public", + "table": "core_moderation_report_events", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderation_reports_pkey", + "schema": "public", + "table": "core_moderation_reports", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderators_permissions_pkey", + "schema": "public", + "table": "core_moderators_permissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_notification_deliveries_pkey", + "schema": "public", + "table": "core_notification_deliveries", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_notification_events_pkey", + "schema": "public", + "table": "core_notification_events", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "core_notification_settings_pkey", + "schema": "public", + "table": "core_notification_settings", + "entityType": "pks" + }, + { + "columns": [ + "userId" + ], + "nameExplicit": false, + "name": "core_notification_user_state_pkey", + "schema": "public", + "table": "core_notification_user_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_notifications_pkey", + "schema": "public", + "table": "core_notifications", + "entityType": "pks" + }, + { + "columns": [ + "pageId" + ], + "nameExplicit": false, + "name": "core_page_layouts_pkey", + "schema": "public", + "table": "core_page_layouts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_passkey_challenges_pkey", + "schema": "public", + "table": "core_users_passkey_challenges", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_passkeys_pkey", + "schema": "public", + "table": "core_users_passkeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_queue_pkey", + "schema": "public", + "table": "core_queue", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_roles_pkey", + "schema": "public", + "table": "core_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_search_index_pkey", + "schema": "public", + "table": "core_search_index", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "core_secrets_pkey", + "schema": "public", + "table": "core_secrets", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sessions_pkey", + "schema": "public", + "table": "core_sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sessions_known_devices_pkey", + "schema": "public", + "table": "core_sessions_known_devices", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "core_update_lock_pkey", + "schema": "public", + "table": "core_update_lock", + "entityType": "pks" + }, + { + "columns": [ + "componentId" + ], + "nameExplicit": false, + "name": "core_update_results_pkey", + "schema": "public", + "table": "core_update_results", + "entityType": "pks" + }, + { + "columns": [ + "url" + ], + "nameExplicit": false, + "name": "core_update_sources_pkey", + "schema": "public", + "table": "core_update_sources", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_pkey", + "schema": "public", + "table": "core_users", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_confirm_emails_pkey", + "schema": "public", + "table": "core_users_confirm_emails", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_forgot_password_pkey", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_sso_operations_pkey", + "schema": "public", + "table": "core_users_sso_operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_categories_pkey", + "schema": "public", + "table": "blog_categories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_posts_pkey", + "schema": "public", + "table": "blog_posts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_advanced_articles_pkey", + "schema": "public", + "table": "example_advanced_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_advanced_articles_faq_pkey", + "schema": "public", + "table": "example_advanced_articles_faq", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_articles_pkey", + "schema": "public", + "table": "example_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_categories_pkey", + "schema": "public", + "table": "example_categories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_discussions_pkey", + "schema": "public", + "table": "example_discussions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_events_pkey", + "schema": "public", + "table": "example_events", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_events_sessions_pkey", + "schema": "public", + "table": "example_events_sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_localized_articles_pkey", + "schema": "public", + "table": "example_localized_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_pages_pkey", + "schema": "public", + "table": "example_pages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_tags_pkey", + "schema": "public", + "table": "example_tags", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "forum_replies_pkey", + "schema": "public", + "table": "forum_comments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "forum_topics_pkey", + "schema": "public", + "table": "forum_topics", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + "itemType", + "itemId", + "languageCode" + ], + "nullsNotDistinct": false, + "name": "core_search_index_item_unique", + "entityType": "uniques", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + "providerId", + "providerAccountId" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_provider_account_key", + "entityType": "uniques", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + "userId", + "providerId" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_user_provider_key", + "entityType": "uniques", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_admin_sessions_token_unique", + "schema": "public", + "table": "core_admin_sessions", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "core_admin_dashboard_userId_unique", + "schema": "public", + "table": "core_admin_dashboard", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "key" + ], + "nullsNotDistinct": false, + "name": "core_files_key_unique", + "schema": "public", + "table": "core_files", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "code" + ], + "nullsNotDistinct": false, + "name": "core_languages_code_unique", + "schema": "public", + "table": "core_languages", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "openKey" + ], + "nullsNotDistinct": false, + "name": "core_moderation_reports_openKey_key", + "schema": "public", + "table": "core_moderation_reports", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "idempotencyKey" + ], + "nullsNotDistinct": false, + "name": "core_notification_deliveries_idempotencyKey_key", + "schema": "public", + "table": "core_notification_deliveries", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_users_passkey_challenges_tokenHash_key", + "schema": "public", + "table": "core_users_passkey_challenges", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "credentialId" + ], + "nullsNotDistinct": false, + "name": "core_users_passkeys_credentialId_key", + "schema": "public", + "table": "core_users_passkeys", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_sessions_token_unique", + "schema": "public", + "table": "core_sessions", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "publicId" + ], + "nullsNotDistinct": false, + "name": "core_sessions_known_devices_publicId_unique", + "schema": "public", + "table": "core_sessions_known_devices", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "nameCode" + ], + "nullsNotDistinct": false, + "name": "core_users_nameCode_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "name" + ], + "nullsNotDistinct": false, + "name": "core_users_name_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "core_users_email_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_users_confirm_emails_token_unique", + "schema": "public", + "table": "core_users_confirm_emails", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "core_users_forgot_password_userId_unique", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_users_forgot_password_token_unique", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_operations_tokenHash_key", + "schema": "public", + "table": "core_users_sso_operations", + "entityType": "uniques" + }, + { + "value": "\"intent\" IN ('link', 'import', 'sync', 'preview')", + "name": "core_users_sso_operations_intent_check", + "entityType": "checks", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "value": "\"field\" IN ('avatar', 'firstName', 'lastName')", + "name": "core_users_sso_profile_sources_field_check", + "entityType": "checks", + "schema": "public", + "table": "core_users_sso_profile_sources" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 28df2e870b..4cc705c09d 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -7,14 +7,30 @@ import { VitNodeAPI } from "@vitnode/core/api/config"; import { storageStaticHeadersMiddleware } from "@vitnode/core/api/middlewares/storage-static.middleware"; import { websocketOriginMiddleware } from "@vitnode/core/api/middlewares/websocket-origin.middleware"; import { handleVitNodeWebSocket } from "@vitnode/core/ws/handle"; +import { compress } from "hono/compress"; import { mkdirSync } from "node:fs"; -import { WebSocketServer } from "ws"; +import { type WebSocket, WebSocketServer } from "ws"; import "hono"; import { vitNodeApiConfig } from "./vitnode.api.config.js"; const app = new OpenAPIHono().basePath("/api"); +// gzip JSON and other text responses over 1 KB. Registered first so it wraps +// every route below. Hono already leaves `text/event-stream`, the AI routes' +// NDJSON streams and `Cache-Control: no-transform` responses alone; WebSocket +// handshakes skip it outright. +const compressResponses = compress(); +app.use(async (c, next) => { + if (c.req.header("upgrade")) { + await next(); + + return; + } + + await compressResponses(c, next); +}); + // Serve locally stored uploads (Local storage adapter) before VitNodeAPI so the // request skips cors/csrf/rate-limiter/global middleware. No-op for cloud adapters. // Annotated explicitly: the nested type inferred through the config chain widens @@ -55,6 +71,26 @@ VitNodeAPI({ const wss = new WebSocketServer({ noServer: true }); +// Heartbeat: a socket whose client vanished (sleeping laptop, dropped Wi-Fi) +// never sends a close frame, so it would stay in the registry forever. Ping +// every 30 s and terminate whatever did not answer the previous ping. +const awaitingPong = new WeakSet(); +wss.on("connection", ws => { + ws.on("pong", () => awaitingPong.delete(ws)); +}); +const heartbeat = setInterval(() => { + for (const ws of wss.clients) { + if (awaitingPong.has(ws)) { + ws.terminate(); + continue; + } + awaitingPong.add(ws); + ws.ping(); + } +}, 30_000); +heartbeat.unref(); +wss.on("close", () => clearInterval(heartbeat)); + // The handshake is a cookie-authenticated GET, which Hono's `csrf()` does not // cover and the same-origin policy does not apply to. Without this any site // could open a socket as a visiting user - see the middleware. diff --git a/apps/web/.env.example b/apps/web/.env.example index 07b629748c..6ba1ab4364 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -6,11 +6,11 @@ REDIS_URL=redis://localhost:6379 # either of them somewhere else and the browser talks to whatever is there # instead of the API mounted in this process. # -# Neither side strictly needs them: `resolveApiOrigin()` in -# `src/server/fetcher.server.ts` takes the origin off the request being -# rendered, and in the browser `CONFIG.api` falls back to the origin the page -# was served from - so a preview deployment on a generated hostname needs no -# config at all. Set `VITNODE_API_URL` only to point at a separate API +# Neither side strictly needs them: on the server, loaders and middleware call +# the API mounted in this process directly (no HTTP round trip), and in the +# browser `CONFIG.api` falls back to the origin the page was served from - so +# a preview deployment on a generated hostname needs no config at all. Set +# `VITNODE_API_URL` to a different origin only to point at a separate API # server; `VITNODE_WEB_URL` is still what the API stamps cookies with. VITNODE_WEB_URL=http://localhost:3000 VITNODE_API_URL=http://localhost:3000 diff --git a/apps/web/content/docs/content-engine/approvals.mdx b/apps/web/content/docs/content-engine/approvals.mdx index dd574bf161..bd0b14f020 100644 --- a/apps/web/content/docs/content-engine/approvals.mdx +++ b/apps/web/content/docs/content-engine/approvals.mdx @@ -64,7 +64,7 @@ For a proposal, `values` is still the live record and `approval.values` holds wh `GET /{id}/edit` returns the record's latest submission when it is pending, or rejected and not replaced yet, so the author's edit page can say "waiting for a moderator" or show the reason. The shape is `zodContentApprovalState` from `@vitnode/core/content`. -To list everything an author sent, call the core moderation module. It returns pending, approved and rejected submissions, newest first, for the signed-in member: +To list everything an author sent, call the core moderation module. It returns pending, approved and rejected submissions, newest first, for the signed-in member. Pass `pluginId` to keep only your plugin's submissions - the filter runs before the page is cut, so every page is full and `total` counts only yours: ```ts title="plugins/forum/src/pages/my-submissions-data.ts" import { fetcher } from "@vitnode/core/tanstack/fetcher"; @@ -75,7 +75,7 @@ export const fetchMySubmissions = async (page: number) => { method: "get", module: "moderation", path: "/approvals/mine", - args: { query: { page } }, + args: { query: { page, pluginId: "@vitnode/forum" } }, }); if (!res.ok) throw new Error("Could not load your submissions."); diff --git a/apps/web/content/docs/content-engine/caching.mdx b/apps/web/content/docs/content-engine/caching.mdx index 74dc69b996..6480fe6bf7 100644 --- a/apps/web/content/docs/content-engine/caching.mdx +++ b/apps/web/content/docs/content-engine/caching.mdx @@ -4,7 +4,7 @@ description: Cache VitNode Content Engine public reads with TanStack Query in th icon: Zap --- -The Content Engine does not cache public reads. Every request to a [public API](/docs/content-engine/public-api) route queries Postgres, and the response carries no `Cache-Control` header: +Without Redis, the Content Engine does not cache public reads. Every request to a [public API](/docs/content-engine/public-api) route queries Postgres, and the response carries no `Cache-Control` header: ```bash curl -si "http://localhost:3000/api/@vitnode/example/content/articles?first=1" | head -4 @@ -17,7 +17,11 @@ content-type: application/json cross-origin-opener-policy: same-origin ``` -So a published article shows up on the next request, with nothing to expire. Add caching where it pays off: +So a published article shows up on the next request, with nothing to expire. + +With [Redis](/docs/dev/cache/api) (`REDIS_URL`), the server also keeps the public list and detail responses for up to 60 seconds. Each entry is keyed by the parsed query, the resolved locale and a generation number per content type, and every content write moves that generation - so a write still shows up on the next request, and nothing old is ever read again. A record whose future `publishedAt` simply comes due, with no write, can take up to that minute to appear. Only responses that look the same to every reader are kept: a [hidden](/docs/content-engine/content-visibility) record shown to a moderator, a preview link and the delivery routes always go to Postgres. The language list and the container tree behind `includeDescendants` are cached the same way. There is nothing to configure. + +Add caching where it pays off: | Where the content is read | Cache with | Expire it with | | --------------------------------------- | --------------------------------------- | ----------------------------------------------------- | @@ -31,47 +35,47 @@ So a published article shows up on the next request, with nothing to expire. Add Wrap the fetcher call in `queryOptions` and warm it in the route loader, exactly as in [Cache app data](/docs/dev/cache/app). Use `defineRoute` from `@vitnode/core/tanstack/plugin-routes`, because its loader context has the `queryClient`: ```tsx title="latest-articles.tsx" -import { queryOptions } from "@tanstack/react-query"; -import { fetcher } from "@vitnode/core/tanstack/fetcher"; -import { defineRoute } from "@vitnode/core/tanstack/plugin-routes"; -import { z } from "zod"; +import { queryOptions } from '@tanstack/react-query' +import { fetcher } from '@vitnode/core/tanstack/fetcher' +import { defineRoute } from '@vitnode/core/tanstack/plugin-routes' +import { z } from 'zod' const zodArticleCard = z.object({ excerpt: z.string().nullable(), slug: z.string(), title: z.string(), -}); +}) export const latestArticlesQuery = () => queryOptions({ - queryKey: ["@vitnode/example", "articles", "latest"], + queryKey: ['@vitnode/example', 'articles', 'latest'], queryFn: async () => { const response = await fetcher({ - plugin: "@vitnode/example", - method: "get", - module: "content/articles", - path: "/", + plugin: '@vitnode/example', + method: 'get', + module: 'content/articles', + path: '/', args: { query: { first: 5 } }, - }); + }) if (response.status !== 200) { - throw new Error(`Loading articles answered ${response.status}.`); + throw new Error(`Loading articles answered ${response.status}.`) } - const { edges } = await response.json(); + const { edges } = await response.json() - return z.array(zodArticleCard).parse(edges); + return z.array(zodArticleCard).parse(edges) }, - }); + }) export const route = defineRoute({ load: async ({ context }) => { await context.queryClient.query({ ...latestArticlesQuery(), staleTime: 60_000, - }); + }) }, -}); +}) ``` Each server render starts with an empty query client, so a full page load is always fresh. The `staleTime` only decides how long a visitor's open tab reuses the list during client-side navigation. `staleTime: "static"` keeps it until the tab reloads. @@ -98,16 +102,16 @@ A locale goes after the kind, so a localized type's Polish list is `content:exam `contentInvalidationTags` turns a description of one write into the exact tags to expire. Publishing article `3` returns all six tags above: ```ts title="revalidate.ts" -import { contentInvalidationTags } from "@vitnode/core/content"; +import { contentInvalidationTags } from '@vitnode/core/content' const tags = contentInvalidationTags({ - contentTypeId: "example.article", + contentTypeId: 'example.article', id: 3, - slugs: ["hello-content-engine"], + slugs: ['hello-content-engine'], wasPublic: false, isPublic: true, delivery: { sitemap: { contentChanged: true, indexChanged: true } }, -}); +}) ``` A write to a record that was a draft before and after returns `[]`, because no public reader ever saw it. On a slug change, pass both slugs so the old URL stops resolving and the new one starts. @@ -119,9 +123,9 @@ If another app renders your content and caches it, list its origin in the API co ```ts title="apps/api/src/vitnode.api.config.ts" export const vitNodeApiConfig = buildApiConfig({ content: { - revalidateOrigins: ["https://www.example.com"], // [!code ++] + revalidateOrigins: ['https://www.example.com'], // [!code ++] }, -}); +}) ``` VitNode then sends a `POST` to `{origin}/api/vitnode/content/revalidate` in these cases: @@ -147,27 +151,25 @@ The secret travels with the request, so outside development each origin must use The endpoint lives in your front end, not in VitNode. Before you expire anything, check the bearer in constant time and refuse a timestamp more than 5 minutes off, so a captured request cannot be replayed later: ```ts title="is-trusted-revalidation.ts" -import { createHash, timingSafeEqual } from "node:crypto"; +import { createHash, timingSafeEqual } from 'node:crypto' -const MAX_SKEW_MS = 5 * 60 * 1000; +const MAX_SKEW_MS = 5 * 60 * 1000 -const digest = (value: string) => createHash("sha256").update(value).digest(); +const digest = (value: string) => createHash('sha256').update(value).digest() export const isTrustedRevalidation = (request: Request): boolean => { - const secret = process.env.CRON_SECRET; - if (!secret) return false; + const secret = process.env.CRON_SECRET + if (!secret) return false const bearer = /^Bearer (.+)$/.exec( - request.headers.get("authorization") ?? "", - )?.[1]; - if (!bearer || !timingSafeEqual(digest(bearer), digest(secret))) return false; + request.headers.get('authorization') ?? '', + )?.[1] + if (!bearer || !timingSafeEqual(digest(bearer), digest(secret))) return false - const sentAt = Number(request.headers.get("x-vitnode-timestamp")); + const sentAt = Number(request.headers.get('x-vitnode-timestamp')) - return ( - Number.isFinite(sentAt) && Math.abs(Date.now() - sentAt) <= MAX_SKEW_MS - ); -}; + return Number.isFinite(sentAt) && Math.abs(Date.now() - sentAt) <= MAX_SKEW_MS +} ``` Hashing both values first gives `timingSafeEqual` two buffers of the same length. Answer `403` when the check fails. Otherwise pass the body to `contentInvalidationTags`, expire those tags in your cache and answer with any `2xx`. diff --git a/apps/web/content/docs/content-engine/public-api.mdx b/apps/web/content/docs/content-engine/public-api.mdx index ff50306dbd..558925354c 100644 --- a/apps/web/content/docs/content-engine/public-api.mdx +++ b/apps/web/content/docs/content-engine/public-api.mdx @@ -4,10 +4,10 @@ description: Add drafts and publishing to a VitNode Content Engine content type, icon: Globe --- -import { Tab, Tabs } from "fumadocs-ui/components/tabs"; -import { ImgDocs } from "@/components/fumadocs/img"; -import publishRow from "./public-api-publish-row.webp"; -import publishConfirm from "./public-api-publish-confirm.webp"; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' +import { ImgDocs } from '@/components/fumadocs/img' +import publishRow from './public-api-publish-row.webp' +import publishConfirm from './public-api-publish-confirm.webp' A content type is private until you opt in. This guide follows `example.article` from the example plugin. It gets a draft and a published state, and its published records become readable by anyone at `/api/@vitnode/example/content/articles`. You list exactly which fields leave the server. @@ -23,32 +23,32 @@ Add `publication` and `publicApi` to the definition. This excerpt leaves out the ```ts title="plugins/example/src/content/article.ts" export const articleContentType = defineContentType({ - id: "example.article", - tableName: "example_articles", + id: 'example.article', + tableName: 'example_articles', fields: { title: field.text({ required: true, minLength: 3, maxLength: 200 }), - slug: field.slug({ source: "title" }), + slug: field.slug({ source: 'title' }), excerpt: field.textarea({ maxLength: 500, nullable: true }), featured: field.boolean({ defaultValue: false }), author: field.user(), category: field.relation({ required: true, - onDelete: "restrict", + onDelete: 'restrict', target: () => categoryContentType, }), }, publication: true, // [!code ++] // [!code ++:9] publicApi: { - path: "articles", - fields: ["title", "slug", "excerpt", "featured", "category", "publishedAt"], - searchableFields: ["title", "excerpt"], - orderableFields: ["publishedAt", "title"], - filterableFields: ["category", "featured"], - defaultOrderBy: "publishedAt", - defaultOrder: "desc", + path: 'articles', + fields: ['title', 'slug', 'excerpt', 'featured', 'category', 'publishedAt'], + searchableFields: ['title', 'excerpt'], + orderableFields: ['publishedAt', 'title'], + filterableFields: ['category', 'featured'], + defaultOrderBy: 'publishedAt', + defaultOrder: 'desc', }, -}); +}) ``` - `publication` adds a `status` column (`draft` or `published`, default `draft`), a `publishedAt` timestamp, a `can_publish` staff permission and **Publish** / **Unpublish** actions in the AdminCP. @@ -67,9 +67,9 @@ export const articleContentType = defineContentType({ Add `buildContentPublicModule` to the plugin's API config. The example plugin passes all five content models; one without `publicApi` is skipped: ```ts title="plugins/example/src/config.api.ts" -import { buildContentPublicModule } from "@/api/builders"; // [!code ++] +import { buildContentPublicModule } from '@/api/builders' // [!code ++] -import { articleContent } from "@/database/articles"; // [!code ++] +import { articleContent } from '@/database/articles' // [!code ++] export const exampleApiPlugin = () => buildApiPlugin({ @@ -81,7 +81,7 @@ export const exampleApiPlugin = () => contentTypes: [articleContent], }), ], - }); + }) ``` @@ -206,33 +206,33 @@ Article not found. The [fetcher](/docs/dev/fetcher) types the routes from the plugin's API registry. The module is `content/` followed by `publicApi.path`. Put a helper like this anywhere in your plugin: ```ts title="featured-articles.ts" -import { fetcher } from "@vitnode/core/tanstack/fetcher"; -import { z } from "zod"; +import { fetcher } from '@vitnode/core/tanstack/fetcher' +import { z } from 'zod' const zodArticleCard = z.object({ excerpt: z.string().nullable(), publishedAt: z.string().nullable(), slug: z.string(), title: z.string(), -}); +}) export const fetchFeaturedArticles = async () => { const response = await fetcher({ - plugin: "@vitnode/example", - method: "get", - module: "content/articles", - path: "/", + plugin: '@vitnode/example', + method: 'get', + module: 'content/articles', + path: '/', args: { query: { featured: true, first: 5 } }, - }); + }) if (response.status !== 200) { - throw new Error(`Loading articles answered ${response.status}.`); + throw new Error(`Loading articles answered ${response.status}.`) } - const { edges } = await response.json(); + const { edges } = await response.json() - return z.array(zodArticleCard).parse(edges); -}; + return z.array(zodArticleCard).parse(edges) +} ``` The path, the query and `pageInfo` are typed. Each record is a `Record`, so parse the fields you use with Zod, as the example's article page does. A content type without `publicApi` adds no module: `module: "content/categories"` is a compile error. An `orderBy` outside the allowlist type-checks but answers `400`. @@ -251,7 +251,7 @@ All routes are `GET` and live under `/api/{pluginId}/content/{publicApi.path}`. | `/delivery/sitemap` | Sitemap entries, only with `delivery.sitemap` | | `/thread?root={id}` | One page of a reply tree under a root record, only with [`thread`](/docs/content-engine/threads) | -The list accepts `first` or `last` (at most 50, also the default), `cursor`, `page`, `search` over `searchableFields`, `orderBy`, `order` and one parameter per filterable field. A filterable [container field](/docs/content-engine/fields#container) also accepts `includeDescendants=true`, so `?board=4&includeDescendants=true` lists board 4 and every board below it; an unknown container answers `404`. See [Containers on the frontend](/docs/content-engine/container-frontend#add-the-board-page). A record is public only when its `status` is `published`, its `publishedAt` is set and not later than the database clock, and it is not [hidden](/docs/content-engine/content-visibility). Every `publicApi` option is in the [content type reference](/docs/content-engine/reference#publicapi). +The list accepts `first` or `last` (at most 50, also the default), `cursor`, `page`, `search` over `searchableFields`, `orderBy`, `order` and one parameter per filterable field. A filterable [container field](/docs/content-engine/fields#container) also accepts `includeDescendants=true`, so `?board=4&includeDescendants=true` lists board 4 and every board below it; an unknown container answers `404`. See [Containers on the frontend](/docs/content-engine/container-frontend#add-the-board-page). A record is public only when its `status` is `published`, its `publishedAt` is set and not later than the database clock, and it is not [hidden](/docs/content-engine/content-visibility). List rows carry every field in `fields` unless you set `listFields` to a smaller set - handy when a `body` would make every page heavy. Every `publicApi` option is in the [content type reference](/docs/content-engine/reference#publicapi). Next, give each article a web page with [Public pages](/docs/content-engine/public-pages), or see how these reads are cached in [Caching](/docs/content-engine/caching). diff --git a/apps/web/content/docs/content-engine/reference.mdx b/apps/web/content/docs/content-engine/reference.mdx index a3c179378b..e6286dd521 100644 --- a/apps/web/content/docs/content-engine/reference.mdx +++ b/apps/web/content/docs/content-engine/reference.mdx @@ -11,26 +11,26 @@ icon: BookOpen This is `example.article` from the example plugin, trimmed to the options most content types start with: ```ts title="plugins/example/src/content/article.ts" -import { defineContentType, field } from "@vitnode/core/content"; +import { defineContentType, field } from '@vitnode/core/content' export const articleContentType = defineContentType({ - id: "example.article", - tableName: "example_articles", + id: 'example.article', + tableName: 'example_articles', fields: { title: field.text({ required: true, minLength: 3, maxLength: 200 }), - slug: field.slug({ source: "title" }), + slug: field.slug({ source: 'title' }), excerpt: field.textarea({ maxLength: 500, nullable: true }), }, publication: true, publicApi: { - path: "articles", - fields: ["title", "slug", "excerpt", "publishedAt"], + path: 'articles', + fields: ['title', 'slug', 'excerpt', 'publishedAt'], }, admin: { - path: "example/articles", - titleField: "title", + path: 'example/articles', + titleField: 'title', }, -}); +}) ``` ## When options are checked @@ -153,7 +153,8 @@ Generates read-only routes at `/api/{pluginId}/content/{path}` that return publi | `orderableFields` | field names | No | `[]` | Accepted by `?orderBy=`. `publishedAt` is always added | | `defaultOrderBy` | field name | No | `"publishedAt"` | Must be orderable | | `defaultOrder` | `"asc"` \| `"desc"` | No | `"desc"` | | -| `people` | user field names | No | `[]` | Single, shared `field.user()` fields returned as a public person summary. Not listed in `fields` | +| `people` | user field names | No | `[]` | Single, shared `field.user()` fields returned as a public person summary. Not listed in `fields` | +| `listFields` | field names | No | `fields` | What each list row carries. A subset of `fields`, no duplicates. The detail route still returns every field in `fields` | What `fields` accepts: @@ -164,6 +165,8 @@ What `fields` accepts: `people` is the explicit opt-in for a user. Each named field comes back under its own name as `{ id, name, nameCode, avatarColor, avatarUrl, role: { color, prefix } }`, or `null` when nobody is set or the account was deleted. The summaries for a whole page are loaded in one query. `people` refuses a field that is not a `field.user()`, a `multiple: true` or localized user field, and a name listed twice. +`listFields` keeps a public list light: leave a long `body` or a gallery to the detail route, and the list neither selects nor loads them. The list row's TypeScript type is still the full detail shape, so a field you left out is simply missing at runtime. `people` are always included. + `searchableFields`, `filterableFields` and `orderableFields` must all be in `fields`. Repeatable leaves fit none of them. `orderableFields` also refuses file fields, to-many relations and localized fields. ```ts title="plugins/example/src/content/article.ts" @@ -267,15 +270,15 @@ localization: { Keeps published records in the [site search index](/docs/dev/search). Drafts never show up in public results. Guide: [Search](/docs/content-engine/search). -| Name | Type | Required | Description | -| ------------------ | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `titleField` | field name | Yes | The result heading. A text field that is not nullable, or a to-one, non-nullable relation to another searchable content type that lends its title. [Guide](/docs/content-engine/search#borrow-the-title-from-a-related-record) | -| `contentFields` | field names | Yes | The indexed body, at least one, no duplicates. Text, textarea, richText or slug. Repeatable leaves such as `"faq.answer"` are allowed | -| `pathTemplate` | `string` | Yes | The result URL. Starts with `/`, contains `{slug}` exactly once, at most 512 characters | -| `descriptionField` | field name | No | Shown in result excerpts. Text, textarea or richText | -| `authorField` | field name | No | A top-level `field.user()` credited in the index. It does not have to be public | -| `imageField` | field name | No | A shared single `field.file()` shown as the result image. Must be in `publicApi.fields` | -| `context` | `{ field, pathTemplate? }` | No | What the record belongs to, for feed sentences. A `field.container()` with a `pathTemplate`, or a to-one relation to a searchable content type. Defaults to the `titleField` relation. [Guide](/docs/content-engine/search#say-where-a-record-lives) | +| Name | Type | Required | Description | +| ------------------ | -------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `titleField` | field name | Yes | The result heading. A text field that is not nullable, or a to-one, non-nullable relation to another searchable content type that lends its title. [Guide](/docs/content-engine/search#borrow-the-title-from-a-related-record) | +| `contentFields` | field names | Yes | The indexed body, at least one, no duplicates. Text, textarea, richText or slug. Repeatable leaves such as `"faq.answer"` are allowed | +| `pathTemplate` | `string` | Yes | The result URL. Starts with `/`, contains `{slug}` exactly once, at most 512 characters | +| `descriptionField` | field name | No | Shown in result excerpts. Text, textarea or richText | +| `authorField` | field name | No | A top-level `field.user()` credited in the index. It does not have to be public | +| `imageField` | field name | No | A shared single `field.file()` shown as the result image. Must be in `publicApi.fields` | +| `context` | `{ field, pathTemplate? }` | No | What the record belongs to, for feed sentences. A `field.container()` with a `pathTemplate`, or a to-one relation to a searchable content type. Defaults to the `titleField` relation. [Guide](/docs/content-engine/search#say-where-a-record-lives) | Every field except `authorField` must be in `publicApi.fields`, or a result snippet would leak a private value. Leave the language out of `pathTemplate`, because your [i18n settings](/docs/dev/i18n) add the locale prefix. @@ -315,24 +318,24 @@ Makes records of one content type answer each other under a root record, like fo thread: { parent: "parent", root: "topic", maxDepth: 6 }, ``` -| Name | Type | Required | Default | Description | -| ---------- | ---------- | -------- | ------- | ----------------------------------------------------------------------------- | +| Name | Type | Required | Default | Description | +| ---------- | ---------- | -------- | ------- | ------------------------------------------------------------------------------ | | `parent` | field name | Yes | | A to-one, nullable `field.relation({ self: true })`: the record being answered | -| `root` | field name | Yes | | A required, to-one `field.relation({ target })` to another content type | -| `maxDepth` | `number` | No | `8` | Deepest level a record may sit at, from 1 (a flat list) to 32 | +| `root` | field name | Yes | | A required, to-one `field.relation({ target })` to another content type | +| `maxDepth` | `number` | No | `8` | Deepest level a record may sit at, from 1 (a flat list) to 32 | Both fields must be in `publicApi.fields` and neither may be in `publicWrite.update.fields`. `thread` is not available on a localized content type yet. -| Cause | Message starts with | -| ------------------------------------------- | --------------------------------------------------------------------------- | -| `parent` is not a to-one self relation | `thread.parent "x" must be a to-one` | -| `parent` is not nullable | `thread.parent "x" must be nullable` | -| `root` is not a required to-one relation | `thread.root "x" must be a required, to-one` | -| `maxDepth` out of range | `thread.maxDepth must be a whole number from 1 to 32.` | -| No `publicApi` | ``thread needs `publicApi`: the thread route`` | -| `parent` or `root` not in `publicApi.fields` | `thread uses "x", so list it in` | -| `parent` or `root` in `update.fields` | `publicWrite.update.fields includes "x".` | -| A localized content type | `thread is not available on a localized content type yet` | +| Cause | Message starts with | +| -------------------------------------------- | --------------------------------------------------------- | +| `parent` is not a to-one self relation | `thread.parent "x" must be a to-one` | +| `parent` is not nullable | `thread.parent "x" must be nullable` | +| `root` is not a required to-one relation | `thread.root "x" must be a required, to-one` | +| `maxDepth` out of range | `thread.maxDepth must be a whole number from 1 to 32.` | +| No `publicApi` | ``thread needs `publicApi`: the thread route`` | +| `parent` or `root` not in `publicApi.fields` | `thread uses "x", so list it in` | +| `parent` or `root` in `update.fields` | `publicWrite.update.fields includes "x".` | +| A localized content type | `thread is not available on a localized content type yet` | ## `duplication` @@ -426,7 +429,7 @@ Each row is a rule `defineContentType` enforces, with the start of the message i | `publicApi.defaultOrderBy` | An orderable field | `publicApi.defaultOrderBy is "x", which is not in publicApi.orderableFields.` | | `publicApi.searchableFields`, `filterableFields`, `orderableFields` | The field in `publicApi.fields` | `publicApi.searchableFields includes "x", which is not in publicApi.fields.` | | `publicApi.people` | A single, shared `field.user()` | `publicApi.people includes "x", which is a "text" field.` | -| `thread` | `publicApi`, with `parent` and `root` in its fields | ``thread needs `publicApi`: the thread route`` | +| `thread` | `publicApi`, with `parent` and `root` in its fields | ``thread needs `publicApi`: the thread route`` | ### Unique across content types diff --git a/apps/web/content/docs/dev/advanced/queue.mdx b/apps/web/content/docs/dev/advanced/queue.mdx index 7a1efb7165..f11f66d19f 100644 --- a/apps/web/content/docs/dev/advanced/queue.mdx +++ b/apps/web/content/docs/dev/advanced/queue.mdx @@ -91,6 +91,10 @@ export const riskyTask = buildQueueTask({ | Each next failure | Doubles, capped at 1 hour | | Final failure | Marked as `failed` in `core_queue` | +### How the Worker Drains the Queue + +Each `process-queue` tick claims 25 tasks at a time and runs up to 5 of them at once, then claims the next 25 - until the queue is empty or about 45 seconds have passed. Handlers share one tick, so make them safe to run side by side. Finished tasks stay in `core_queue` for 7 days; the hourly `clean` job removes older ones in small batches. + ### Stale Tasks Are Reclaimed A task still `processing` after 15 minutes (`QUEUE_STALE_PROCESSING_MINUTES`) belonged to a worker that died mid-run. The queue worker puts it back to `pending`, or marks it `failed` if it has used every attempt, so a crash never strands a task forever. diff --git a/apps/web/content/docs/dev/ai/automatic-alt.mdx b/apps/web/content/docs/dev/ai/automatic-alt.mdx index fc3ad39a35..ff10ce5703 100644 --- a/apps/web/content/docs/dev/ai/automatic-alt.mdx +++ b/apps/web/content/docs/dev/ai/automatic-alt.mdx @@ -81,7 +81,7 @@ Nothing happens until the host runs VitNode's cron. See [Running the cron](#runn 2. **Sweep.** The `ai-alt-detect` cron (hourly) looks for images still missing ALT in a configured language and queues them. 3. **Work.** The `ai-queue` cron (every minute) takes up to **2** queued images and describes them. 4. **Describe once.** The image gets one base description in English, per file fingerprint. -5. **Translate.** Each missing language gets a translation of that base description. English uses it as-is. +5. **Translate.** Each missing language gets a translation of that base description, two at a time. English uses it as-is. 6. **Write.** Each language goes to `core_files_alt`, but only if no person wrote it and the file is still the same version. 7. **Notify.** `files.alt.updated` is emitted with the languages that were written. @@ -117,7 +117,7 @@ Queuing is deduplicated: a file that is already pending or processing in the que ### Budget waits are not failures -When the site budget, the background jobs budget or the global switch stops a run, the task is **deferred**, not failed. It goes back to `pending` without spending a retry, with the file's state set to `waiting_budget`. It wakes at the start of the next budget month or in one hour, whichever comes first. Raise the budget mid-month and work resumes within the hour. +When the site budget, the background jobs budget or the global switch stops a run, the task is **deferred**, not failed. The same goes for a full house of background AI slots (`systemConcurrency`): the task simply tries again a minute later. It goes back to `pending` without spending a retry, with the file's state set to `waiting_budget`. It wakes at the start of the next budget month or in one hour, whichever comes first. Raise the budget mid-month and work resumes within the hour. Other failures (provider error, invalid output, timeout) use the normal queue retries: 3 attempts, then the file's state is `failed`. The next sweep wrap queues it again. diff --git a/apps/web/content/docs/dev/cache/api.mdx b/apps/web/content/docs/dev/cache/api.mdx index 1afff86333..f2ff7692ed 100644 --- a/apps/web/content/docs/dev/cache/api.mdx +++ b/apps/web/content/docs/dev/cache/api.mdx @@ -4,11 +4,11 @@ description: Cache slow database reads in a VitNode Hono route with c.get("cache icon: Server --- -`c.get("cache")` stores values in Redis from inside a Hono route handler. Use it for reads that are slow and give the same answer to many visitors, such as counts over a large table or a computed summary. Without Redis, every call runs your loader directly, so the same code works with and without it. +`c.get("cache")` stores values in Redis from inside a Hono route handler. Use it for reads that are slow and give the same answer to many visitors, such as counts over a large table or a computed summary. Without Redis, values live in a small in-memory store for at most 15 seconds, so the same code works with and without it. ## Before you begin -Connect Redis in `vitnode.api.config.ts` as described in [Redis](/docs/dev/advanced/redis). You can write the code below without Redis, but nothing is cached until it is connected. +Connect Redis in `vitnode.api.config.ts` as described in [Redis](/docs/dev/advanced/redis). You can write the code below without Redis, but each instance then keeps its own short-lived copy, and a `delete` on one instance never reaches the others. diff --git a/apps/web/content/docs/dev/cache/pages-invalidation.mdx b/apps/web/content/docs/dev/cache/pages-invalidation.mdx index 1c414fa48c..b78b8cb89a 100644 --- a/apps/web/content/docs/dev/cache/pages-invalidation.mdx +++ b/apps/web/content/docs/dev/cache/pages-invalidation.mdx @@ -47,7 +47,7 @@ Every content write announces itself after its database transaction has committe A hard invalidation also covers a render that started before the change and finishes after it: that render is sent to the visitor who asked, but not stored. After a slug change, the old address answers with its `308` redirect straight away. -Invalidation is per record, not per language. Editing the Polish translation also refreshes the English page, which lists the Polish one in its `hreflang` links anyway. Scheduled publication and creating a draft are announced too, but a draft that nobody can see changes no page. +Invalidation is per record, not per language. Editing the Polish translation also refreshes the English page, which lists the Polish one in its `hreflang` links anyway. Scheduled publication and creating a draft are announced too, but a draft that nobody can see changes no page - so creating, autosaving, editing or deleting a record that was never public (and still isn't) invalidates nothing at all. Your blog's front page stays warm while someone polishes tomorrow's post. Data your page reads from your own API routes is invalidated only if you label it. Call `setPageCacheTags(c, [tag])` in the route and `invalidatePageCacheTags([tag], { mode })` after the write. See the [reference](/docs/dev/cache/pages-reference#label-and-invalidate-your-own-data). Anything unlabelled, such as settings, shows up when the TTL ends. diff --git a/apps/web/content/docs/dev/cache/pages-reference.mdx b/apps/web/content/docs/dev/cache/pages-reference.mdx index 63e7c48a8d..72c026ea3b 100644 --- a/apps/web/content/docs/dev/cache/pages-reference.mdx +++ b/apps/web/content/docs/dev/cache/pages-reference.mdx @@ -10,9 +10,9 @@ Exact options and behavior of the page cache. For a walkthrough, see [Cache serv Accepted by `page()` and `index()` from `@vitnode/core/routing`. -| Name | Type | Required | Default | Description | -| ---------------------- | -------- | -------- | ------- | ------------------------------------------------------------------------ | -| `maxAge` | `number` | Yes | - | Seconds a stored page is fresh. Whole number from 1 to 31536000. | +| Name | Type | Required | Default | Description | +| ---------------------- | -------- | -------- | ------- | ---------------------------------------------------------------------------- | +| `maxAge` | `number` | Yes | - | Seconds a stored page is fresh. Whole number from 1 to 31536000. | | `staleWhileRevalidate` | `number` | No | `0` | Extra seconds a stale page may be served while it re-renders. 0 to 31536000. | `cache: false` declares that the route is not cached, which is also the default. The build fails with `invalid-cache` for a layout, an unknown key or a duration outside these ranges. A policy on an AdminCP route, an `/api` path or a route with `requires` (its own or a parent layout's) fails the build too. @@ -21,11 +21,11 @@ Accepted by `page()` and `index()` from `@vitnode/core/routing`. Set in `buildConfig()` in the host app's `src/vitnode.config.ts`. -| Name | Type | Required | Default | Description | -| --------- | -------------------------------------------- | -------- | ---------- | ----------------------------------------------------------------------------------- | -| `enabled` | `boolean` | No | `true` | `false` turns the cache off for every route. Development never caches either way. | -| `storage` | `"memory" \| "redis"` | No | `"memory"` | `"redis"` reads `REDIS_URL` when the server starts. | -| `routes` | `Record` | No | `{}` | Per-route override, keyed by canonical path (`"/blog/:slug"`). `false` disables. | +| Name | Type | Required | Default | Description | +| --------- | ---------------------------------- | -------- | ---------- | ---------------------------------------------------------------------------------- | +| `enabled` | `boolean` | No | `true` | `false` turns the cache off for every route. Development never caches either way. | +| `storage` | `"memory" \| "redis"` | No | `"memory"` | `"redis"` reads `REDIS_URL` (and `REDIS_PASSWORD`, if set) when the server starts. | +| `routes` | `Record` | No | `{}` | Per-route override, keyed by canonical path (`"/blog/:slug"`). `false` disables. | An override wins over the route's own `cache`, and may opt in one of the app's own route files. A key that matches no page route fails the build. @@ -33,21 +33,21 @@ An override wins over the route's own `cache`, and may opt in one of the app's o From `@vitnode/core/tanstack/page-cache/server`. Wraps a TanStack Start server entry and returns one with the same `fetch`. -| Name | Type | Required | Description | -| --------- | --------------------- | -------- | --------------------------------------------- | +| Name | Type | Required | Description | +| --------- | --------------------- | -------- | ----------------------------------------------- | | `buildId` | `string` | Yes | `pageCacheBuildId` from `src/page-cache.gen.ts` | -| `config` | `VitNodeConfig` | Yes | The app's `vitNodeConfig` | +| `config` | `VitNodeConfig` | Yes | The app's `vitNodeConfig` | | `routes` | `PageCacheRouteTable` | Yes | `pageCacheRoutes` from `src/page-cache.gen.ts` | ## Response headers -| Header | Value | Meaning | -| ------------------ | ---------------------------------------------- | -------------------------------------------------------------------- | -| `x-vitnode-cache` | `HIT`, `STALE`, `MISS`, `REVALIDATED` | Served from the cache, stale while refreshing, rendered and stored, or re-rendered after expiry | -| `x-vitnode-cache` | `BYPASS` | The request skipped the cache, or the render could not be shared | -| `cache-control` | `no-cache` | A shared page: browsers and CDNs must revalidate with the `ETag` | -| `cache-control` | `private, no-cache` | A shared page that also sets this visitor's locale cookie | -| `cache-control` | `private, no-store` | A page rendered for one visitor | +| Header | Value | Meaning | +| ----------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `x-vitnode-cache` | `HIT`, `STALE`, `MISS`, `REVALIDATED` | Served from the cache, stale while refreshing, rendered and stored, or re-rendered after expiry | +| `x-vitnode-cache` | `BYPASS` | The request skipped the cache, or the render could not be shared | +| `cache-control` | `no-cache` | A shared page: browsers and CDNs must revalidate with the `ETag` | +| `cache-control` | `private, no-cache` | A shared page that also sets this visitor's locale cookie | +| `cache-control` | `private, no-store` | A page rendered for one visitor | Routes without a policy keep their usual headers and get no `x-vitnode-cache`. @@ -66,10 +66,10 @@ Entries are keyed by the request origin (scheme, host and port), the path, the m From `@vitnode/core/api/lib/page-cache`, for data a cached page reads from your own Hono routes. -| Function | Returns | Behavior | -| ------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------ | -| `setPageCacheTags(c, tags)` | `void` | Labels the response. A cached page that reads it during its render is stored with these tags. | -| `invalidatePageCacheTags(tags, { mode })` | `Promise` | Invalidates every page stored with any of `tags`. `mode` is `"soft"` or `"hard"`. | +| Function | Returns | Behavior | +| ----------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------- | +| `setPageCacheTags(c, tags)` | `void` | Labels the response. A cached page that reads it during its render is stored with these tags. | +| `invalidatePageCacheTags(tags, { mode })` | `Promise` | Invalidates every page stored with any of `tags`. `mode` is `"soft"` or `"hard"`. | ```ts title="plugins/events/src/api/modules/events/routes/list.route.ts" import { setPageCacheTags } from '@vitnode/core/api/lib/page-cache' @@ -93,7 +93,7 @@ if (result.status === 'failed') { Only for an API that runs in its own process next to a web app using `storage: "redis"`. -| Name | Type | Required | Default | Description | -| ------------- | --------- | -------- | ------- | ---------------------------------------------------------------------- | -| `storage` | `"redis"` | Yes | - | Writes invalidations to the Redis at `REDIS_URL`. | +| Name | Type | Required | Default | Description | +| ------------- | --------- | -------- | ------- | ------------------------------------------------------------------------ | +| `storage` | `"redis"` | Yes | - | Writes invalidations through the API's `redis` client, or `REDIS_URL`. | | `maxLifetime` | `number` | No | `86400` | Longest `maxAge + staleWhileRevalidate` of any cached route, in seconds. | diff --git a/apps/web/content/docs/dev/cache/pages.mdx b/apps/web/content/docs/dev/cache/pages.mdx index 2ceb97faa1..2773da7d98 100644 --- a/apps/web/content/docs/dev/cache/pages.mdx +++ b/apps/web/content/docs/dev/cache/pages.mdx @@ -76,14 +76,14 @@ export const vitNodeConfig = buildConfig({ }) ``` -| Storage | Where entries live | Use it when | -| ---------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| `"memory"` | Inside each server process (default) | You run one server process. A restart empties the cache, and each process would keep its own copy. | -| `"redis"` | The Redis at the `REDIS_URL` variable | You run more than one instance. All of them share entries, and an invalidation reaches every instance. | +| Storage | Where entries live | Use it when | +| ---------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `"memory"` | Inside each server process (default) | You run one server process. A restart empties the cache, and each process would keep its own copy. | +| `"redis"` | The Redis at the `REDIS_URL` variable | You run more than one instance. All of them share entries, and an invalidation reaches every instance. | `routes` keys are canonical route paths, the same spelling the route was declared with. A policy replaces the plugin's, `false` turns caching off for that route, and you can also opt in one of your app's own route files. A key that matches no page route fails the build. `enabled: false` turns the whole cache off. -The Redis URL stays in the runtime environment and never ends up in the build. If `storage` is `"redis"` and `REDIS_URL` is missing, the server logs an error and serves pages without caching them. +The Redis URL - and `REDIS_PASSWORD`, if your Redis wants one - stays in the runtime environment and never ends up in the build. If `storage` is `"redis"` and `REDIS_URL` is missing, the server logs an error and serves pages without caching them. diff --git a/apps/web/content/docs/dev/cache/reference.mdx b/apps/web/content/docs/dev/cache/reference.mdx index 9355fb5a1b..2d6ea39f60 100644 --- a/apps/web/content/docs/dev/cache/reference.mdx +++ b/apps/web/content/docs/dev/cache/reference.mdx @@ -4,15 +4,15 @@ description: Every method on c.get("cache") in VitNode API routes - signatures, icon: List --- -`c.get("cache")` is available in every Hono route handler. It stores JSON values in Redis under a per-plugin prefix and does nothing when Redis is not configured. For a step-by-step example, see [Cache API data](/docs/dev/cache/api). +`c.get("cache")` is available in every Hono route handler. It stores JSON values in Redis under a per-plugin prefix, and falls back to a small in-memory store when Redis is not configured. For a step-by-step example, see [Cache API data](/docs/dev/cache/api). ## Methods | Method | Returns | Behavior | | -------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | -| `get(key)` | `Promise` | Reads and parses a value. Returns `null` on a miss, on a Redis error, or without Redis. | +| `get(key)` | `Promise` | Reads and parses a value. Returns `null` on a miss or on a Redis error. | | `set(key, value, ttlSeconds?)` | `Promise` | Stores `value` as JSON. Without `ttlSeconds`, or with `0`, the key never expires. | -| `has(key)` | `Promise` | Whether the key exists. `false` on a Redis error or without Redis. | +| `has(key)` | `Promise` | Whether the key exists. `false` on a Redis error. | | `remember(key, ttlSeconds, loader)` | `Promise` | Returns the cached value when `get` finds one. Otherwise runs `loader`, stores its result with `set` and returns it. | | `delete(key)` | `Promise` | Deletes one key, or every key in an array. | | `flush()` | `Promise` | Deletes every key of the current plugin. Other plugins' keys and unrelated data in the same Redis instance are left untouched. | @@ -20,7 +20,7 @@ icon: List | `releaseLock(key)` | `Promise` | Releases a lock taken with `acquireLock`. | | `status()` | `Promise<{ configured: boolean; connected: boolean }>` | Pings Redis. Both fields are `false` without Redis. | -`getSystem`, `setSystem` and `deleteSystem` also exist. They write to a namespace shared with VitNode core, so plugins should use the plugin-scoped methods above. +`getSystem`, `getSystemMany`, `setSystem` and `deleteSystem` also exist. They write to a namespace shared with VitNode core, so plugins should use the plugin-scoped methods above. ## Keys @@ -41,7 +41,13 @@ Values go through `JSON.stringify` and `JSON.parse`: ## Without Redis and when Redis fails -When the `redis` option in `vitnode.api.config.ts` is not set, every read misses, every write does nothing, and `remember` always runs its loader. +When the `redis` option in `vitnode.api.config.ts` is not set, the cache lives in the memory of each API process: + +- It holds up to 10,000 keys and drops the least recently used one when full. +- Every TTL is capped at 15 seconds. A key written without a TTL stays until it is deleted or pushed out. +- `delete` and `flush` only reach the process that runs them. With one instance that is all of them; with several, the others keep their copy until it expires - hence the short cap. + +Run more than one instance? Connect Redis so every instance shares one cache. When Redis is configured but fails, the error never reaches your route. A failed read counts as a miss and a failed write is skipped. Commands fail immediately while Redis is unreachable instead of waiting in a queue, so an outage does not stall requests. @@ -50,7 +56,7 @@ Locks behave differently, because a lock exists to stop work from running twice: | Situation | `acquireLock` returns | | -------------------- | --------------------- | | Redis connected | `true` or `false` | -| Redis not configured | `true` | +| Redis not configured | `true` or `false` | | Redis error | `false` | -Without Redis, every caller gets the lock. If two instances must never run the same work at once, guard it in the database as well, for example with `FOR UPDATE SKIP LOCKED`. +Without Redis, the lock only covers one process, and its TTL is not capped. If two instances must never run the same work at once, guard it in the database as well, for example with `FOR UPDATE SKIP LOCKED`. diff --git a/apps/web/content/docs/dev/cron/node-cron.mdx b/apps/web/content/docs/dev/cron/node-cron.mdx index a856cd0398..c2a2bd9be9 100644 --- a/apps/web/content/docs/dev/cron/node-cron.mdx +++ b/apps/web/content/docs/dev/cron/node-cron.mdx @@ -117,9 +117,13 @@ import { schedule } from 'node-cron' export const NodeCronAdapter = (): CronAdapter => { return { schedule() { - schedule('*/1 * * * *', async () => { - await handleCronJobs() - }) + schedule( + '*/1 * * * *', + async () => { + await handleCronJobs() + }, + { noOverlap: true }, + ) }, } } @@ -131,6 +135,12 @@ export const NodeCronAdapter = (): CronAdapter => { never calls a job handler; it only knocks on the door once a minute, and the [cron endpoint](/docs/dev/cron/rest-api) decides what is due. +`noOverlap` skips a tick while the previous request is still waiting, and the +request gives up after 55 seconds - the API keeps running the jobs, it just +stops making the ticker wait. Running several API instances? Each one ticks, +but every due job is claimed in `core_cron` first, so it runs on exactly one of +them. + Three consequences worth writing down: | Because the tick is an HTTP call… | …this is true | diff --git a/apps/web/content/docs/dev/deployments/cloud/vercel.mdx b/apps/web/content/docs/dev/deployments/cloud/vercel.mdx index 83cb988feb..33ab516227 100644 --- a/apps/web/content/docs/dev/deployments/cloud/vercel.mdx +++ b/apps/web/content/docs/dev/deployments/cloud/vercel.mdx @@ -75,10 +75,10 @@ the browser bundle and cannot be changed afterwards without building again. A preview gets a generated hostname, so no configured value can name it. Left - unset, the server takes the API origin off the request being rendered and the - browser falls back to the origin the page came from - which is right on every - deployment, including ones that did not exist when you configured the project. - Set it only when the API genuinely lives somewhere else. + unset, the server calls the API in its own process and the browser falls back + to the origin the page came from - which is right on every deployment, + including ones that did not exist when you configured the project. Set it only + when the API genuinely lives somewhere else. diff --git a/apps/web/content/docs/dev/deployments/self-hosted.mdx b/apps/web/content/docs/dev/deployments/self-hosted.mdx index b0141fdc57..f50348da7e 100644 --- a/apps/web/content/docs/dev/deployments/self-hosted.mdx +++ b/apps/web/content/docs/dev/deployments/self-hosted.mdx @@ -72,7 +72,8 @@ REDIS_PASSWORD=change_me_redis_password ### Where the API lives A **Single App** answers `/api/*` itself, so leave `VITNODE_API_URL` unset. -The server takes the API origin off the request being rendered and the browser +Server-side calls (the session read, every loader) go straight to the API in the +same process - no HTTP round trip through your proxy or CDN - and the browser falls back to the origin the page was served from, so the same build is correct on every hostname you point at it. @@ -94,20 +95,46 @@ production at the visitor's own machine. ## Reverse Proxy (Nginx Configuration) -Proxy traffic to VitNode while forwarding client headers and supporting WebSocket upgrades: +Proxy traffic to VitNode while forwarding client headers, keeping connections to the app open between requests, and upgrading only the requests that ask for a WebSocket: ```nginx title="/etc/nginx/sites-available/vitnode.conf" +# "upgrade" for WebSocket handshakes, and an empty Connection header for +# everything else so the upstream connection stays in the keepalive pool. +map $http_upgrade $connection_upgrade { + default upgrade; + '' ''; +} + +upstream vitnode { + server 127.0.0.1:3000; + keepalive 32; +} + server { listen 80; server_name example.com; + # Compress HTML and API responses. Static assets already ship as .br/.gz + # from the build, and nginx never re-compresses a compressed response. + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 5; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml application/json application/javascript application/xml application/manifest+json image/svg+xml; + + # Brotli needs the ngx_brotli module. Uncomment if your nginx has it. + # brotli on; + # brotli_comp_level 5; + # brotli_types text/plain text/css text/xml application/json application/javascript application/xml application/manifest+json image/svg+xml; + location / { - proxy_pass http://localhost:3000; + proxy_pass http://vitnode; proxy_http_version 1.1; # WebSocket support proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; + proxy_set_header Connection $connection_upgrade; # Forwarded identity proxy_set_header Host $host; @@ -118,6 +145,12 @@ server { } ``` +`keepalive 32` plus an empty `Connection` header reuses connections to the app instead of opening a fresh one for every request. Sending `Connection "upgrade"` on every request, as many copy-paste configs do, quietly turns that off. + +In a split deployment, `apps/api` gzips its own JSON responses, so the same config works in front of it too. + +Hashed files under `/assets/` already leave the app with `Cache-Control: public, max-age=31536000, immutable`, so there is nothing to add for them here. + Keep `proxy_set_header Host $host;`. VitNode picks a [language domain](/docs/dev/i18n/localized-urls#behind-a-proxy) from the `Host` header and deliberately ignores `X-Forwarded-Host`, which any visitor can send. --- diff --git a/apps/web/content/docs/dev/email/index.mdx b/apps/web/content/docs/dev/email/index.mdx index 29397455c8..0f7f11aa78 100644 --- a/apps/web/content/docs/dev/email/index.mdx +++ b/apps/web/content/docs/dev/email/index.mdx @@ -38,7 +38,10 @@ export const sendWelcomeRoute = buildRoute({ ``` `send()` renders the template and queues delivery. It keeps a successful API -response quick even when the mail provider is having a moody afternoon. +response quick even when the mail provider is having a moody afternoon. The +queued email is handed to the provider right after the response goes out, so +a sign-up or password-reset link doesn't wait for the next cron tick; if that +attempt fails, the queue worker retries it as usual. diff --git a/apps/web/content/docs/dev/fetcher.mdx b/apps/web/content/docs/dev/fetcher.mdx index cb5dea8bb4..794d153e93 100644 --- a/apps/web/content/docs/dev/fetcher.mdx +++ b/apps/web/content/docs/dev/fetcher.mdx @@ -276,6 +276,16 @@ VitNode provides standard stale times in `@vitnode/core/lib/query-freshness`: server fetchers should never reach client bundles. +### SSR skips the network when the API lives next door + +When your app mounts the API in its own process (the default single app does, +via `configureInProcessApi` from +`@vitnode/core/lib/fetcher/in-process-api.server`), server-side calls hand the +request straight to it - no HTTP round trip, no trip through your proxy or +CDN, same cookies, headers and signed visitor IP. Set `VITNODE_API_URL` to a +different origin, or pass `origin` on a call, and the request goes over the +network as before. + ## Browser-only requests with fetcherClient For client code that **never** executes on the server—such as browser event listeners, interactive dialogs, or framework-neutral utilities—call `fetcherClient` from `@vitnode/core/lib/fetcher-client`: diff --git a/apps/web/content/docs/dev/moderation/reference.mdx b/apps/web/content/docs/dev/moderation/reference.mdx index f87cf14b60..5d47ccd2bc 100644 --- a/apps/web/content/docs/dev/moderation/reference.mdx +++ b/apps/web/content/docs/dev/moderation/reference.mdx @@ -120,7 +120,7 @@ Also under `/api/@vitnode/core/moderation`, for [approvals](/docs/content-engine | -------------------------------- | ------------- | ------------------------------------------------------------------------- | ---------------------------------- | | `GET /approvals` | `can_approve` | Query `status` (default `pending`), `contentTypeId`, `containerId`, `page`, `pageSize` | `{ items, page, pageSize, total }` | | `GET /approvals/filters` | `can_approve` | | `{ contentTypes, containers }` the moderator may review | -| `GET /approvals/mine` | Signed in | Query `page`, `pageSize` | The member's own pending, approved and rejected submissions, newest first | +| `GET /approvals/mine` | Signed in | Query `page`, `pageSize`, `pluginId` (optional, one plugin's submissions only) | The member's own pending, approved and rejected submissions, newest first | | `GET /approvals/{id}` | `can_approve` | | The submission with `values`, `baseValues`, `current`, `fields`, `record` and `history` | | `POST /approvals/{id}/approve` | `can_approve` | | `{ id, status }` | | `POST /approvals/{id}/reject` | `can_approve` | `{ reason? }`, up to 2000 characters | `{ id, status }` | diff --git a/apps/web/content/docs/dev/performance.mdx b/apps/web/content/docs/dev/performance.mdx index 7f8efe4bf2..36751ff8a7 100644 --- a/apps/web/content/docs/dev/performance.mdx +++ b/apps/web/content/docs/dev/performance.mdx @@ -163,6 +163,17 @@ Both sheets are full Tailwind builds emitting into the same `theme, base, compon Two things keep the sets aligned: the route stylesheet starts by importing the app one, and `vitNodeTailwindSources` gives both the same `@source` directives for `@vitnode/core` and your configured plugins. Anything you add to the app stylesheet's sources, add for the route stylesheet too. +Plain CSS with no Tailwind directives is the easy case. The marketing keyframes in `marketing.css` use only `mk-` class names, so the `_main` layout links the file as-is and admin and docs never download it: + +```tsx title="apps/web/src/routes/_main.tsx" +import marketingCss from '@/site/marketing/marketing.css?url' + +export const Route = createFileRoute('/_main')({ + head: () => ({ links: [{ href: marketingCss, rel: 'stylesheet' }] }), + // ... +}) +``` + ### 8. Group Shared Vendor Modules Rolldown splits every module shared by two routes into its own file, which for an icon library means dozens of 300-byte requests per page. Group the libraries that are shared by design into one chunk each: diff --git a/apps/web/content/docs/dev/routing/loading.mdx b/apps/web/content/docs/dev/routing/loading.mdx index a582318f4e..3b951661f8 100644 --- a/apps/web/content/docs/dev/routing/loading.mdx +++ b/apps/web/content/docs/dev/routing/loading.mdx @@ -28,13 +28,13 @@ export function getRouter() { // [!code ++:3] defaultPendingComponent: RoutePendingSpinner, defaultPendingMs: 150, - defaultPendingMinMs: 300, + defaultPendingMinMs: 200, }) } ``` - `defaultPendingMs: 150` — Prevents visual flickering on fast network responses by waiting 150ms before showing the spinner. -- `defaultPendingMinMs: 300` — Ensures that once shown, the spinner remains visible for at least 300ms to avoid jarring flashes. +- `defaultPendingMinMs: 200` — Ensures that once shown, the spinner remains visible for at least 200ms to avoid jarring flashes, without holding a page that is already loaded for long. {/* Image prompt: Minimal centered loading spinner in a clean dark UI shell with accessible aria-live indicator, 1440x900. */} diff --git a/apps/web/content/docs/dev/routing/navigation.mdx b/apps/web/content/docs/dev/routing/navigation.mdx index 1b4325e071..78c1b96c4f 100644 --- a/apps/web/content/docs/dev/routing/navigation.mdx +++ b/apps/web/content/docs/dev/routing/navigation.mdx @@ -6,7 +6,7 @@ icon: Signpost Inside the app, use TanStack Router's `Link`. It builds the href, applies the locale prefix, and preloads the destination once the pointer rests on it for -250 ms. You write the logical path; the router writes the language. +75 ms. You write the logical path; the router writes the language. [Preloading](/docs/dev/routing/preloading) covers what a preload costs and how to turn it off. @@ -120,7 +120,7 @@ export const CatalogItemLink = ({ slug }: { slug: string }) => ( ``` The link navigates without a full page load, [preloads](/docs/dev/routing/preloading) -after a 250 ms hover, and gets the visitor's locale prefix from the router. +after a 75 ms hover, and gets the visitor's locale prefix from the router. Write the href without a locale prefix. An absolute URL such as `https://example.com` renders a plain ``. `aria-current="page"` is set only when the href matches the current URL exactly, query and hash included. diff --git a/apps/web/content/docs/dev/routing/preloading.mdx b/apps/web/content/docs/dev/routing/preloading.mdx index 794f9fe5f7..723bee6af7 100644 --- a/apps/web/content/docs/dev/routing/preloading.mdx +++ b/apps/web/content/docs/dev/routing/preloading.mdx @@ -23,18 +23,18 @@ The app's router sets the defaults in `src/router.tsx`: ```tsx title="apps/web/src/router.tsx" const router = createTanStackRouter({ defaultPreload: 'intent', - defaultPreloadDelay: 250, + defaultPreloadDelay: 75, defaultPreloadStaleTime: 0, // ... }) ``` -| Visitor action | Result | -| ----------------------------------- | ----------------------------------------- | -| Hover or focus a link for 250 ms | The destination preloads once | -| Pass over a link in less than 250 ms | Nothing is loaded | -| Touch a link on a phone | The destination preloads at once | -| Click before 250 ms | The page loads normally, with no preload | +| Visitor action | Result | +| ----------------------------------- | ---------------------------------------- | +| Hover or focus a link for 75 ms | The destination preloads once | +| Pass over a link in less than 75 ms | Nothing is loaded | +| Touch a link on a phone | The destination preloads at once | +| Click before 75 ms | The page loads normally, with no preload | `defaultPreloadStaleTime: 0` hands freshness to TanStack Query instead of the router. The router reruns the loader for every preload and again on the click, and the loader answers from the query cache. That is why the next section matters. @@ -84,11 +84,11 @@ export const routes = definePluginRoutes([ A hover then skips the route's loader: its `load` and the loading of its `messages`. The click still runs everything. -| Value | Effect | -| --------- | ----------------------------------------------------------------------------- | -| left out | Uses the nearest parent `layout()` that sets it, otherwise the app's defaults | -| `false` | A preload skips this route's loader | -| `true` | Turns preloading back on under a `layout()` that set `false` | +| Value | Effect | +| -------- | ----------------------------------------------------------------------------- | +| left out | Uses the nearest parent `layout()` that sets it, otherwise the app's defaults | +| `false` | A preload skips this route's loader | +| `true` | Turns preloading back on under a `layout()` that set `false` | `preload: false` skips only the loader. A preload still downloads the route's diff --git a/apps/web/content/docs/dev/search-elasticsearch.mdx b/apps/web/content/docs/dev/search-elasticsearch.mdx index 33a25ee3e0..c2f500ca6e 100644 --- a/apps/web/content/docs/dev/search-elasticsearch.mdx +++ b/apps/web/content/docs/dev/search-elasticsearch.mdx @@ -80,18 +80,24 @@ collection) needs the **Rebuild and clear the search index** permission - Documents now carry `authorIds` (a list) instead of `authorId`, plus drafts - as private documents. If your index was created before that, delete it in + Documents now carry `authorIds` (a list) instead of `authorId`, plus drafts as + private documents. If your index was created before that, delete it in Elasticsearch and run **Rebuild index**. VitNode recreates it with the new mapping. - Documents now carry an `itemKey` keyword with the record id as text, so - [uuid and bigint](/docs/content-engine/id-strategy) records can be found. - VitNode adds the field to an existing index the first time it uses it. Older - documents get a value on the next **Rebuild index**; until then, results fall - back to the numeric `itemId`, which works for serial ids only. + Documents now carry an `itemKey` keyword with the record id as text, so [uuid + and bigint](/docs/content-engine/id-strategy) records can be found. VitNode + adds the field to an existing index the first time it uses it. Older documents + get a value on the next **Rebuild index**; until then, results fall back to + the numeric `itemId`, which works for serial ids only. + + + + Elasticsearch counts matches up to 10,000, the same depth you can page to. + Past that, `totalCount` reads 10,000 and means "at least this many" - nobody + scrolls that far anyway. diff --git a/apps/web/content/docs/dev/search.mdx b/apps/web/content/docs/dev/search.mdx index a145d4fe68..d1732a04e2 100644 --- a/apps/web/content/docs/dev/search.mdx +++ b/apps/web/content/docs/dev/search.mdx @@ -125,12 +125,12 @@ Results sit on a timeline grouped by month and day, so you can tell at a glance The same filters work in the public search API: -| Parameter | Example | What it does | -| ----------- | --------------------------- | ------------------------------------------------ | -| `types` | `forum.topic,forum.comment` | Only these content types | -| `authorIds` | `1,7` | Written by any of these members (up to 20) | -| `from` | `2026-10-01T00:00:00.000Z` | Created at or after this moment | -| `to` | `2026-10-14T23:59:59.999Z` | Created at or before this moment | +| Parameter | Example | What it does | +| ----------- | --------------------------- | ------------------------------------------ | +| `types` | `forum.topic,forum.comment` | Only these content types | +| `authorIds` | `1,7` | Written by any of these members (up to 20) | +| `from` | `2026-10-01T00:00:00.000Z` | Created at or after this moment | +| `to` | `2026-10-14T23:59:59.999Z` | Created at or before this moment | ```bash curl "http://localhost:3000/api/@vitnode/core/search?authorIds=1,7&from=2026-10-01T00:00:00.000Z" @@ -143,6 +143,10 @@ Two small routes feed the sidebar: A search engine plugin that can count implements `countByType` and sets `capabilities.facets` to `true`. One that can't returns `types: null`, and the sidebar quietly drops the numbers. Engines should also honour `authorIds`, or the author filter will be more of a suggestion. +Engines that can remove many documents at once can implement `deleteMany(c, itemType, itemIds)`. Deleting a container (say, a forum with all its topics) then costs one call instead of one per child. Skip it and VitNode politely calls `delete` for each item. + +Browsing without a search term (a timeline, `/discover`, or the API without `search`) returns the first 1,000 characters of each hit's `content` - plenty for the snippet under the title. Search with a term and you get the whole text, so the snippet can jump to the match. + --- ## Member timelines @@ -151,12 +155,12 @@ Every member gets a timeline: everything credited to them, newest first. It live Who sees what depends on who's looking: -| Viewer | Published | Hidden | Drafts | -| --- | --- | --- | --- | -| Anyone, on `/search` and `/discover` | Yes | Only its authors and editors, with an eye-off icon | No | -| Anyone, on a member's profile | Yes | Only its authors and editors, with an eye-off icon | No | -| The member, on their own profile | Yes | Yes, with an eye-off icon | Yes, marked **Draft** | -| Staff, on the AdminCP user page | Yes | Yes, with an eye-off icon | Yes, marked **Draft** | +| Viewer | Published | Hidden | Drafts | +| ------------------------------------ | --------- | -------------------------------------------------- | --------------------- | +| Anyone, on `/search` and `/discover` | Yes | Only its authors and editors, with an eye-off icon | No | +| Anyone, on a member's profile | Yes | Only its authors and editors, with an eye-off icon | No | +| The member, on their own profile | Yes | Yes, with an eye-off icon | Yes, marked **Draft** | +| Staff, on the AdminCP user page | Yes | Yes, with an eye-off icon | Yes, marked **Draft** | The **Hidden** column follows [Who can still see it](/docs/content-engine/content-visibility#who-can-still-see-it). An editor is staff with `can_edit` on the content type. @@ -192,9 +196,9 @@ The timelines come from two routes: `search.pathTemplate` is the English route of a result's page, with one `{slug}` placeholder. To turn search on for a content type, see [Content Engine search](/docs/content-engine/search). Leave the language out: VitNode adds the prefix, the domain and any translated segments from your app's [localized URL](/docs/dev/i18n/localized-urls) settings. -| `pathTemplate` | English result | Polish result (`as-needed`) | Polish result (with `domains`) | -| :----------------------- | :---------------------- | :-------------------------- | :----------------------------------- | -| `/blog/{slug}` | `/blog/hello-world` | `/pl/blog/witaj-swiecie` | `https://vitnode.pl/blog/witaj-swiecie` | +| `pathTemplate` | English result | Polish result (`as-needed`) | Polish result (with `domains`) | +| :------------- | :------------------ | :-------------------------- | :-------------------------------------- | +| `/blog/{slug}` | `/blog/hello-world` | `/pl/blog/witaj-swiecie` | `https://vitnode.pl/blog/witaj-swiecie` | With `'/blog/:slug': '/wpisy/:slug'` in `routePaths.pl`, the Polish result becomes `/pl/wpisy/witaj-swiecie`. diff --git a/apps/web/content/docs/dev/websocket.mdx b/apps/web/content/docs/dev/websocket.mdx index ed4b039cb9..cb6de11f95 100644 --- a/apps/web/content/docs/dev/websocket.mdx +++ b/apps/web/content/docs/dev/websocket.mdx @@ -192,7 +192,8 @@ Without Redis, all of this works on a single instance with no extra setup: `toRo - **Single Connection**: Only one `/api/ws` socket is opened per client. All features share it via message multiplexing. - **Tab Leader Election**: When a user opens multiple tabs, a Web Lock elects one leader tab to hold the socket, distributing messages across tabs via `BroadcastChannel`. -- **Automatic Reconnect**: Backoff retry automatically re-establishes dropped connections within seconds. +- **Automatic Reconnect**: A dropped connection retries after about a second, then backs off (with a little jitter) up to 30 seconds, so a restarted server is not stampeded by every open tab. +- **Heartbeat**: The bundled API server pings every socket every 30 seconds and closes the ones that stopped answering, so vanished clients do not linger. - **Multiple Instances**: With `REDIS_URL` set, `sendToUser`, `broadcast` and `toRoom` are relayed to every API instance over Redis pub/sub. Without Redis they only reach clients connected to the same instance. ## Learn More diff --git a/apps/web/content/docs/guides/forum.mdx b/apps/web/content/docs/guides/forum.mdx index 319c553199..f952dc6f27 100644 --- a/apps/web/content/docs/guides/forum.mdx +++ b/apps/web/content/docs/guides/forum.mdx @@ -112,7 +112,15 @@ shape. The forum index, forum and topic pages are cached for a minute and refreshed in the background. Edit buttons and the comment box load in the browser, so every -visitor gets the same cached page. Topic pages set a canonical URL and +visitor gets the same cached page. The comment editor itself waits for a click on +**Write a comment…**, so readers who only read never download it. + +Topic and comment counts are cached too when Redis is configured (`REDIS_URL`). +Any new, published, hidden, moved or deleted topic or comment expires them on +the spot, and a five-minute limit catches anything that changed behind the +forum's back, such as a row edited by hand. Topic lists leave out the topic body, +so `GET /api/@vitnode/forum/content/topics` returns titles, slugs and dates only - +read one topic for its body. Topic pages set a canonical URL and `DiscussionForumPosting` structured data with the comment count. Topics also have [delivery](/docs/content-engine/seo): a renamed topic diff --git a/apps/web/content/docs/ui/image-zoom.mdx b/apps/web/content/docs/ui/image-zoom.mdx index 80020c041c..48f3a54cbe 100644 --- a/apps/web/content/docs/ui/image-zoom.mdx +++ b/apps/web/content/docs/ui/image-zoom.mdx @@ -17,8 +17,10 @@ import { ImageZoom } from '@vitnode/core/components/ui/image-zoom' ```tsx ``` @@ -88,5 +90,18 @@ import { TypeTable } from 'fumadocs-ui/components/type-table' description: 'Classes for the image on the page.', type: 'string', }, + width: { + description: + 'Intrinsic width of `src`. With `height`, it reserves space so the page does not jump while the image loads.', + type: 'number', + }, + height: { + description: 'Intrinsic height of `src`.', + type: 'number', + }, + loading: { + description: 'Pass `"lazy"` for images below the fold.', + type: '"eager" | "lazy"', + }, }} /> diff --git a/apps/web/src/docs/examples/attachment-layouts.tsx b/apps/web/src/docs/examples/attachment-layouts.tsx index 58196e87ba..57560825bb 100644 --- a/apps/web/src/docs/examples/attachment-layouts.tsx +++ b/apps/web/src/docs/examples/attachment-layouts.tsx @@ -46,7 +46,14 @@ export default function AttachmentLayoutsExample() { render={} /> - + {file.name} diff --git a/apps/web/src/docs/examples/attachment-states.tsx b/apps/web/src/docs/examples/attachment-states.tsx index 6a673572ab..bbbd7becb3 100644 --- a/apps/web/src/docs/examples/attachment-states.tsx +++ b/apps/web/src/docs/examples/attachment-states.tsx @@ -33,7 +33,14 @@ export default function AttachmentStatesExample() { - + community-banner.webp diff --git a/apps/web/src/docs/examples/attachment.tsx b/apps/web/src/docs/examples/attachment.tsx index 47f1b15684..123a91d24a 100644 --- a/apps/web/src/docs/examples/attachment.tsx +++ b/apps/web/src/docs/examples/attachment.tsx @@ -16,7 +16,14 @@ export default function AttachmentExample() {
- + dashboard-screenshot.webp diff --git a/apps/web/src/docs/examples/carousel.tsx b/apps/web/src/docs/examples/carousel.tsx index 1670f7c984..081c6aac50 100644 --- a/apps/web/src/docs/examples/carousel.tsx +++ b/apps/web/src/docs/examples/carousel.tsx @@ -109,7 +109,7 @@ export default function CarouselExample() { > - + - +
diff --git a/apps/web/src/docs/examples/comparison-hover.tsx b/apps/web/src/docs/examples/comparison-hover.tsx index 7bd278e083..0d32cb407f 100644 --- a/apps/web/src/docs/examples/comparison-hover.tsx +++ b/apps/web/src/docs/examples/comparison-hover.tsx @@ -21,10 +21,26 @@ export default function ComparisonHover() { onPositionChange={setPosition} > - + - + diff --git a/apps/web/src/docs/examples/comparison.tsx b/apps/web/src/docs/examples/comparison.tsx index 8134785ce4..5585b98d58 100644 --- a/apps/web/src/docs/examples/comparison.tsx +++ b/apps/web/src/docs/examples/comparison.tsx @@ -15,10 +15,26 @@ export default function ComparisonExample() { className="aspect-video rounded-lg border" > - + - + diff --git a/apps/web/src/docs/examples/image-zoom.tsx b/apps/web/src/docs/examples/image-zoom.tsx index e59e711603..5cac5e274d 100644 --- a/apps/web/src/docs/examples/image-zoom.tsx +++ b/apps/web/src/docs/examples/image-zoom.tsx @@ -11,15 +11,21 @@ export default function ImageZoomExample() {
diff --git a/apps/web/src/docs/examples/item-media.tsx b/apps/web/src/docs/examples/item-media.tsx index 3752c60284..9fcd840fbb 100644 --- a/apps/web/src/docs/examples/item-media.tsx +++ b/apps/web/src/docs/examples/item-media.tsx @@ -42,7 +42,14 @@ export default function ItemMediaExample() { - + Release notes 2.0 diff --git a/apps/web/src/docs/source.server.ts b/apps/web/src/docs/source.server.ts index d56f998146..c9ac2749c1 100644 --- a/apps/web/src/docs/source.server.ts +++ b/apps/web/src/docs/source.server.ts @@ -14,8 +14,47 @@ export const source = loader({ export type DocsPage = InferPageType -export const getLLMText = async (page: DocsPage): Promise => { +// The docs only change with a build, so a production server renders each +// text once and lets CDNs keep it. Dev stays fresh so edits show up at once. +const IS_MEMOIZED = !import.meta.env.DEV + +export const DOCS_TEXT_CACHE_HEADERS: Record = IS_MEMOIZED + ? { 'cache-control': 'public, max-age=300, s-maxage=3600' } + : {} + +export const memoizeDocsText = ( + build: () => Promise, +): (() => Promise) => { + let pending: Promise | undefined + + return async () => { + if (!IS_MEMOIZED) return await build() + + pending ??= build().catch((error: unknown) => { + pending = undefined + throw error + }) + + return await pending + } +} + +const renderLLMText = async (page: DocsPage): Promise => { const processed = await page.data.getText('processed') return `# ${page.data.title} (${page.url})\n\n${processed}` } + +// Bounded by the number of pages in the build. +const llmTexts = new Map Promise>() + +export const getLLMText = async (page: DocsPage): Promise => { + let read = llmTexts.get(page.url) + + if (!read) { + read = memoizeDocsText(async () => await renderLLMText(page)) + llmTexts.set(page.url, read) + } + + return await read() +} diff --git a/apps/web/src/router.tsx b/apps/web/src/router.tsx index b36e5f777f..120062dcca 100644 --- a/apps/web/src/router.tsx +++ b/apps/web/src/router.tsx @@ -56,9 +56,9 @@ export function getRouter() { context: { queryClient }, defaultPendingComponent: RoutePendingSpinner, defaultPendingMs: 150, - defaultPendingMinMs: 300, + defaultPendingMinMs: 200, defaultPreload: 'intent', - defaultPreloadDelay: 250, + defaultPreloadDelay: 75, defaultPreloadStaleTime: 0, defaultViewTransition: navigationViewTransition( vitNodeConfig.viewTransitions, diff --git a/apps/web/src/routes/_docs/docs.$.tsx b/apps/web/src/routes/_docs/docs.$.tsx index 193a587df1..4adadc513d 100644 --- a/apps/web/src/routes/_docs/docs.$.tsx +++ b/apps/web/src/routes/_docs/docs.$.tsx @@ -23,8 +23,10 @@ export const Route = createFileRoute('/_docs/docs/$')({ } }, loader: async ({ params }) => { - const page = await getDocsPage({ data: params._splat ?? '' }) - const { docs } = await import('@/docs/source') + const [page, { docs }] = await Promise.all([ + getDocsPage({ data: params._splat ?? '' }), + import('@/docs/source'), + ]) await docs.getPage(page.path)?.preload() diff --git a/apps/web/src/routes/_main.tsx b/apps/web/src/routes/_main.tsx index 1ac04b9c7d..201ccb9f33 100644 --- a/apps/web/src/routes/_main.tsx +++ b/apps/web/src/routes/_main.tsx @@ -6,7 +6,9 @@ import { ThemeLayoutContent, } from '@vitnode/core/tanstack/layout' +import { withoutHmrTimestamp } from '@/lib/without-hmr-timestamp' import { SiteFooter } from '@/site/marketing/footer' +import marketingCss from '@/site/marketing/marketing.css?url' const MainLayout = () => ( <> @@ -19,5 +21,10 @@ const MainLayout = () => ( export const Route = createFileRoute('/_main')({ loader: async ({ context }) => await loadMainShell(context), + // Marketing-only keyframes and layouts stay out of the global stylesheet, + // so admin and docs never download them. + head: () => ({ + links: [{ href: withoutHmrTimestamp(marketingCss), rel: 'stylesheet' }], + }), component: MainLayout, }) diff --git a/apps/web/src/routes/docs.search.ts b/apps/web/src/routes/docs.search.ts index ad2b925e00..07b3bd8c1d 100644 --- a/apps/web/src/routes/docs.search.ts +++ b/apps/web/src/routes/docs.search.ts @@ -2,8 +2,11 @@ import { createFileRoute } from '@tanstack/react-router' import { componentNameToIconName } from '@vitnode/core/lib/emoji-icon' import { createFromSource } from 'fumadocs-core/search/server' -import { source } from '@/docs/source.server' +import { DOCS_TEXT_CACHE_HEADERS, source } from '@/docs/source.server' +// Built once per server: the index is the expensive part and only changes +// with a build. Each query's results are computed per request, and the URL +// carries the query, so a CDN keys its copy by it. const docsSearch = createFromSource(source, { language: 'english' }) const pageIconOf = (url: string): string | undefined => { @@ -25,6 +28,7 @@ export const Route = createFileRoute('/docs/search')({ ? { ...result, icon: pageIconOf(result.url) } : result, ), + { headers: DOCS_TEXT_CACHE_HEADERS }, ) }, }, diff --git a/apps/web/src/routes/docs.{$}[.]md.ts b/apps/web/src/routes/docs.{$}[.]md.ts index b4a051e291..70051165dd 100644 --- a/apps/web/src/routes/docs.{$}[.]md.ts +++ b/apps/web/src/routes/docs.{$}[.]md.ts @@ -2,7 +2,11 @@ import { createFileRoute } from '@tanstack/react-router' import { movedDocsPath } from '@/docs/redirects' import { decodeMarkdownUrl, encodeMarkdownUrl } from '@/docs/shared' -import { getLLMText, source } from '@/docs/source.server' +import { + DOCS_TEXT_CACHE_HEADERS, + getLLMText, + source, +} from '@/docs/source.server' export const Route = createFileRoute('/docs/{$}.md')({ server: { @@ -23,7 +27,10 @@ export const Route = createFileRoute('/docs/{$}.md')({ if (!page) return new Response('Not Found', { status: 404 }) return new Response(await getLLMText(page), { - headers: { 'content-type': 'text/markdown; charset=utf-8' }, + headers: { + ...DOCS_TEXT_CACHE_HEADERS, + 'content-type': 'text/markdown; charset=utf-8', + }, }) }, }, diff --git a/apps/web/src/routes/llms-full[.]txt.ts b/apps/web/src/routes/llms-full[.]txt.ts index e7fdf4b3cd..33ba6d68f6 100644 --- a/apps/web/src/routes/llms-full[.]txt.ts +++ b/apps/web/src/routes/llms-full[.]txt.ts @@ -1,17 +1,28 @@ import { createFileRoute } from '@tanstack/react-router' -import { getLLMText, source } from '@/docs/source.server' +import { + DOCS_TEXT_CACHE_HEADERS, + getLLMText, + memoizeDocsText, + source, +} from '@/docs/source.server' + +const readFullText = memoizeDocsText(async () => { + const pages = await Promise.all(source.getPages().map(getLLMText)) + + return pages.join('\n\n') +}) export const Route = createFileRoute('/llms-full.txt')({ server: { handlers: { - GET: async () => { - const pages = await Promise.all(source.getPages().map(getLLMText)) - - return new Response(pages.join('\n\n'), { - headers: { 'content-type': 'text/plain; charset=utf-8' }, - }) - }, + GET: async () => + new Response(await readFullText(), { + headers: { + ...DOCS_TEXT_CACHE_HEADERS, + 'content-type': 'text/plain; charset=utf-8', + }, + }), }, }, }) diff --git a/apps/web/src/routes/llms[.]txt.ts b/apps/web/src/routes/llms[.]txt.ts index b8e2c610d2..96f516cfd0 100644 --- a/apps/web/src/routes/llms[.]txt.ts +++ b/apps/web/src/routes/llms[.]txt.ts @@ -1,14 +1,25 @@ import { createFileRoute } from '@tanstack/react-router' import { llms } from 'fumadocs-core/source' -import { source } from '@/docs/source.server' +import { + DOCS_TEXT_CACHE_HEADERS, + memoizeDocsText, + source, +} from '@/docs/source.server' + +const readIndex = memoizeDocsText(async () => + Promise.resolve(llms(source).index()), +) export const Route = createFileRoute('/llms.txt')({ server: { handlers: { - GET: () => - new Response(llms(source).index(), { - headers: { 'content-type': 'text/plain; charset=utf-8' }, + GET: async () => + new Response(await readIndex(), { + headers: { + ...DOCS_TEXT_CACHE_HEADERS, + 'content-type': 'text/plain; charset=utf-8', + }, }), }, }, diff --git a/apps/web/src/server/vitnode-api.server.ts b/apps/web/src/server/vitnode-api.server.ts index 5f87aa987c..3d36b7b78d 100644 --- a/apps/web/src/server/vitnode-api.server.ts +++ b/apps/web/src/server/vitnode-api.server.ts @@ -1,6 +1,7 @@ import '@tanstack/react-start/server-only' import { OpenAPIHono } from '@hono/zod-openapi' import { VitNodeAPI } from '@vitnode/core/api/config' +import { configureInProcessApi } from '@vitnode/core/lib/fetcher/in-process-api.server' import { createApiBridge } from '@/server/api-bridge' import { vitNodeApiConfig } from '@/vitnode.api.config' @@ -39,3 +40,8 @@ if (cache.__vitnodeApi?.plugins !== configuredPlugins) { export const vitNodeApi = cache.__vitnodeApi.app export const apiBridge = createApiBridge(vitNodeApi) + +// Server-side fetches from loaders, middleware and server functions reach this +// API directly instead of over HTTP to this same server - unless +// `VITNODE_API_URL` points at a separate API. +configureInProcessApi(apiBridge) diff --git a/apps/web/src/site/home/illustrations/hero-wall/hero-wall-columns.tsx b/apps/web/src/site/home/illustrations/hero-wall/hero-wall-columns.tsx index 1c23c4ea56..996fb91275 100644 --- a/apps/web/src/site/home/illustrations/hero-wall/hero-wall-columns.tsx +++ b/apps/web/src/site/home/illustrations/hero-wall/hero-wall-columns.tsx @@ -101,7 +101,7 @@ export const HeroWallColumns = () => { > {COLUMNS.map(({ key, screens }, columnIndex) => (
diff --git a/apps/web/src/site/marketing/marketing.css b/apps/web/src/site/marketing/marketing.css index 90534e21f7..5f466d7fdd 100644 --- a/apps/web/src/site/marketing/marketing.css +++ b/apps/web/src/site/marketing/marketing.css @@ -174,6 +174,7 @@ animation: mk-wall-rise linear both; animation-timeline: scroll(root block); animation-range: 0 max(90vh, 360px); + will-change: transform; } .mk-wall-column:nth-child(even) { diff --git a/apps/web/src/site/marketing/shared.tsx b/apps/web/src/site/marketing/shared.tsx index 64936aeb9a..05fb3e015f 100644 --- a/apps/web/src/site/marketing/shared.tsx +++ b/apps/web/src/site/marketing/shared.tsx @@ -192,7 +192,7 @@ export const CanaryPill = ({ className }: { className?: string }) => ( )} > - + VitNode 2.0 · Canary · very early build diff --git a/apps/web/src/site/plugins/plugins-page.tsx b/apps/web/src/site/plugins/plugins-page.tsx index c4045f7e0b..9609df16e3 100644 --- a/apps/web/src/site/plugins/plugins-page.tsx +++ b/apps/web/src/site/plugins/plugins-page.tsx @@ -131,7 +131,7 @@ export const PluginsPage = () => (
Plugins diff --git a/apps/web/src/site/solutions/solution-page.tsx b/apps/web/src/site/solutions/solution-page.tsx index 555f4af691..b24f7df69e 100644 --- a/apps/web/src/site/solutions/solution-page.tsx +++ b/apps/web/src/site/solutions/solution-page.tsx @@ -47,7 +47,7 @@ const HeroShell = ({ children }: { children: React.ReactNode }) => (
{children} diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 3f1e59797e..5842cb6835 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -4,7 +4,6 @@ @import './theme.css'; @import '@vitnode/core/styles/tiptap.css'; @import '@vitnode/core/styles/view-transitions.css'; -@import './site/marketing/marketing.css'; @custom-variant dark (&:where(.dark, .dark *)); @custom-variant reduced-transparency (@media (prefers-reduced-transparency: reduce)); diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/router.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/router.tsx index 5f0e4c098b..35e8494d74 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/router.tsx +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/router.tsx @@ -52,9 +52,9 @@ export function getRouter() { context: { queryClient }, defaultPendingComponent: RoutePendingSpinner, defaultPendingMs: 150, - defaultPendingMinMs: 300, + defaultPendingMinMs: 200, defaultPreload: "intent", - defaultPreloadDelay: 250, + defaultPreloadDelay: 75, defaultPreloadStaleTime: 0, defaultViewTransition: navigationViewTransition( vitNodeConfig.viewTransitions, diff --git a/packages/elasticsearch/src/index.ts b/packages/elasticsearch/src/index.ts index 9d37a831bd..4c47e94944 100644 --- a/packages/elasticsearch/src/index.ts +++ b/packages/elasticsearch/src/index.ts @@ -14,6 +14,8 @@ const DEFAULT_INDEX = "vitnode"; const DEFAULT_SIZE = 20; const MAX_SIZE = 100; const MAX_SEARCH_OFFSET = 10_000; +// Terms per delete-by-query, well under the 65,536 `terms` default limit. +const DELETE_MANY_CHUNK = 1000; const parseSearchOffset = ( cursor: string | undefined, @@ -361,8 +363,8 @@ export const ElasticsearchSearchAdapter = ( return { name: "elasticsearch", - // `languageScopedDelete`: `delete` below filters the delete-by-query on - // `languageCode` when it is given one, so taking the Polish translation down + // `languageScopedDelete`: `delete` below removes only that language's + // document when it is given one, so taking the Polish translation down // leaves the English document in place. capabilities: { facets: true, @@ -430,23 +432,26 @@ export const ElasticsearchSearchAdapter = ( await getClient().bulk({ operations, refresh: false }); }, - // One document per language shares an (itemType, itemId), so remove every - // language variant with a query rather than a single id - unless the caller - // named one, which is how a single translation is taken down without - // touching the others. + // One document per language shares an (itemType, itemId), so removing + // every language variant takes a query. A named language is one known + // document id - a plain delete, no index scan; a missing one is fine. delete: async (_c, itemType, itemId, languageCode) => { + if (languageCode !== undefined) { + await getClient().delete( + { index, id: docId(itemType, itemId, languageCode) }, + { ignore: [404] }, + ); + + return; + } + await ensureIndex(); await getClient().deleteByQuery( { index, query: { bool: { - filter: [ - { term: { itemType } }, - ...(languageCode === undefined - ? [] - : [{ term: { languageCode } }]), - ], + filter: [{ term: { itemType } }], minimum_should_match: 1, should: [ { term: { itemKey: itemKeyOf(itemId) } }, @@ -459,6 +464,35 @@ export const ElasticsearchSearchAdapter = ( ); }, + deleteMany: async (_c, itemType, itemIds) => { + if (itemIds.length === 0) return; + await ensureIndex(); + + for (let start = 0; start < itemIds.length; start += DELETE_MANY_CHUNK) { + const chunk = itemIds.slice(start, start + DELETE_MANY_CHUNK); + const numericIds = chunk.filter(id => typeof id === "number"); + + await getClient().deleteByQuery( + { + index, + query: { + bool: { + filter: [{ term: { itemType } }], + minimum_should_match: 1, + should: [ + { terms: { itemKey: chunk.map(itemKeyOf) } }, + ...(numericIds.length > 0 + ? [{ terms: { itemId: numericIds } }] + : []), + ], + }, + }, + }, + { ignore: [404] }, + ); + } + }, + clear: async (_c, itemType) => { await getClient().deleteByQuery( { @@ -488,7 +522,9 @@ export const ElasticsearchSearchAdapter = ( index, from, size, - track_total_hits: true, + // Pages stop at `MAX_SEARCH_OFFSET` anyway, so counting past it is work + // nobody can use; beyond it the total reads as "at least" this many. + track_total_hits: MAX_SEARCH_OFFSET, query: buildQuery(params, options.ranking), sort: buildSort(params), }); diff --git a/packages/node-cron/src/index.ts b/packages/node-cron/src/index.ts index d51d24d850..c761ac51d8 100644 --- a/packages/node-cron/src/index.ts +++ b/packages/node-cron/src/index.ts @@ -4,9 +4,14 @@ import { schedule } from "node-cron"; export const NodeCronAdapter = (): CronAdapter => { return { schedule() { - schedule("*/1 * * * *", async () => { - await handleCronJobs(); - }); + schedule( + "*/1 * * * *", + async () => { + await handleCronJobs(); + }, + // Skip a tick while the previous one is still waiting on the API. + { noOverlap: true }, + ); }, }; }; diff --git a/packages/nodemailer/src/index.ts b/packages/nodemailer/src/index.ts index 77148a606d..5cbfe73f99 100644 --- a/packages/nodemailer/src/index.ts +++ b/packages/nodemailer/src/index.ts @@ -1,4 +1,5 @@ import type { EmailApiPlugin } from "@vitnode/core/api/models/email"; +import type { Transporter } from "nodemailer"; import { createTransport } from "nodemailer"; @@ -17,35 +18,41 @@ export const NodemailerEmailAdapter = ({ secure?: boolean; user: string | undefined; }): EmailApiPlugin => { + // One pooled transport per adapter: SMTP connections (and their TLS + // handshakes) are reused across sends instead of opened for every email. + let transporter: null | Transporter = null; + const getTransporter = (): Transporter => { + transporter ??= createTransport({ + pool: true, + maxConnections: 5, + host, + port, + secure, + auth: { + user, + pass: password, + }, + }); + + return transporter; + }; + return { - sendEmail: async ({ metadata, to, subject, html, replyTo }) => { + sendEmail: async ({ metadata, to, subject, html, text, replyTo }) => { if (!(host && user && password && from)) { throw new Error("Missing nodemailer configuration"); } - const transporter = createTransport( - { - host, - port, - secure, - auth: { - user, - pass: password, - }, + const info = await getTransporter().sendMail({ + from: { + name: metadata.shortTitle ?? metadata.title, + address: from, }, - { - from: { - name: metadata.shortTitle ?? metadata.title, - address: from, - }, - replyTo, - }, - ); - - const info = await transporter.sendMail({ + replyTo, to, subject, html, + text, }); return { id: info.messageId }; diff --git a/packages/s3/src/index.ts b/packages/s3/src/index.ts index c7eddc151e..563342f37f 100644 --- a/packages/s3/src/index.ts +++ b/packages/s3/src/index.ts @@ -11,6 +11,15 @@ import { S3Client, } from "@aws-sdk/client-s3"; +const discardBody = (body: unknown): void => { + if (typeof body !== "object" || body === null) return; + if ("destroy" in body && typeof body.destroy === "function") { + (body as { destroy: () => void }).destroy(); + } else if ("cancel" in body && typeof body.cancel === "function") { + void (body as ReadableStream).cancel().catch(() => undefined); + } +}; + export const S3StorageAdapter = ({ accessKeyId = "", bucket = "", @@ -43,6 +52,10 @@ export const S3StorageAdapter = ({ endpoint, forcePathStyle: forcePathStyle ?? !!endpoint, credentials: { accessKeyId, secretAccessKey }, + // Without these a stalled bucket holds a request (and its socket) open + // indefinitely. `requestTimeout` is socket inactivity, not total time, + // so large uploads that keep moving are unaffected. + requestHandler: { connectionTimeout: 5000, requestTimeout: 30_000 }, }); return client; @@ -67,7 +80,12 @@ export const S3StorageAdapter = ({ const object = await getClient().send( new GetObjectCommand({ Bucket: bucket, Key: key }), ); - if ((object.ContentLength ?? 0) > maxBytes || !object.Body) { + if (!object.Body) return null; + if ((object.ContentLength ?? 0) > maxBytes) { + // Not reading the body would leave the socket busy until the whole + // object drains (or never), starving the client's connection pool. + discardBody(object.Body); + return null; } const bytes = await object.Body.transformToByteArray(); diff --git a/packages/supabase-storage/src/index.ts b/packages/supabase-storage/src/index.ts index b9c6e2821d..6bde75dac2 100644 --- a/packages/supabase-storage/src/index.ts +++ b/packages/supabase-storage/src/index.ts @@ -6,6 +6,47 @@ import type { import { StorageClient } from "@supabase/storage-js"; +// Streams the object and gives up as soon as it passes `maxBytes`, so an +// oversized file is never buffered whole just to be thrown away. +const readCapped = async ( + stream: ReadableStream, + maxBytes: number, +): Promise => { + const reader = stream.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + + try { + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + if ( + !(value instanceof Uint8Array) || + total + value.byteLength > maxBytes + ) { + await reader.cancel(); + + return null; + } + total += value.byteLength; + chunks.push(value); + } + } catch { + return null; + } finally { + reader.releaseLock(); + } + + const bytes = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + + return bytes; +}; + // Uses a Supabase secret key (`sb_secret_…`), the modern replacement for the // legacy `service_role` key. A still-valid legacy key works too - the value is // sent verbatim as the `apikey`/`Authorization` header. @@ -38,10 +79,13 @@ export const SupabaseStorageAdapter = ({ return { read: async (key: string, { maxBytes }: { maxBytes: number }) => { - const { data, error } = await getClient().from(bucket).download(key); - if (error || data.size > maxBytes) return null; + const { data, error } = await getClient() + .from(bucket) + .download(key) + .asStream(); + if (error) return null; - return new Uint8Array(await data.arrayBuffer()); + return await readCapped(data, maxBytes); }, delete: async (key: string): Promise => { const { error } = await getClient().from(bucket).remove([key]); diff --git a/packages/vitnode/src/api/adapters/events/local.ts b/packages/vitnode/src/api/adapters/events/local.ts index 6fbcee0072..d2ae7bc52a 100644 --- a/packages/vitnode/src/api/adapters/events/local.ts +++ b/packages/vitnode/src/api/adapters/events/local.ts @@ -1,5 +1,6 @@ import type { Context } from "hono"; +import type { EventListenerConfig } from "@/api/lib/events"; import type { EnvVitNode } from "@/api/middlewares/global.middleware"; import type { EventEmitResult, @@ -7,6 +8,31 @@ import type { EventsApiPlugin, } from "@/api/models/events"; +// Indexed once per listener list - which is built at boot - so an emit is a map +// lookup. +const listenerIndexes = new WeakMap< + readonly EventListenerConfig[], + Map +>(); + +const listenersOf = ( + all: readonly EventListenerConfig[], + event: string, +): readonly EventListenerConfig[] => { + let index = listenerIndexes.get(all); + if (!index) { + index = new Map(); + for (const listener of all) { + const bucket = index.get(listener.event); + if (bucket) bucket.push(listener); + else index.set(listener.event, [listener]); + } + listenerIndexes.set(all, index); + } + + return index.get(event) ?? []; +}; + export const LocalEventsAdapter = (): EventsApiPlugin => ({ name: "local", @@ -14,9 +40,11 @@ export const LocalEventsAdapter = (): EventsApiPlugin => ({ c: Context, envelope: EventEnvelope, ): Promise => { - const listeners = c - .get("core") - .events.listeners.filter(listener => listener.event === envelope.name); + // Run one after another, in registration order. + const listeners = listenersOf( + c.get("core").events.listeners, + envelope.name, + ); const failures: EventEmitResult["failures"] = []; let delivered = 0; diff --git a/packages/vitnode/src/api/adapters/search/postgres.ts b/packages/vitnode/src/api/adapters/search/postgres.ts index 333e50829d..fd13cb3b18 100644 --- a/packages/vitnode/src/api/adapters/search/postgres.ts +++ b/packages/vitnode/src/api/adapters/search/postgres.ts @@ -25,6 +25,43 @@ const leadAuthorId = sql`${core_search_index.authorIds}[1]`; const DEFAULT_PAGE_SIZE = 20; const MAX_PAGE_SIZE = 100; +// Without a term every consumer shows the head of `content` as a short snippet, +// so a page of whole articles is not worth shipping. With one, the snippet is +// cut around the first match, which can sit anywhere - so content stays whole. +const CONTENT_EXCERPT_LENGTH = 1000; + +/** + * The rows after the cursor in `(createdAt, id)` order - the order the page is + * sorted by. The cursor is a row id, so the boundary's `createdAt` is read + * back from that row, as `::text` to keep its microseconds. + * A cursor whose row has since gone falls back to the id alone. + */ +const keysetAfter = async ( + c: Context, + cursor: number, + oldest: boolean, +): Promise => { + const [boundary] = await c + .get("db") + .select({ createdAt: sql`${core_search_index.createdAt}::text` }) + .from(core_search_index) + .where(eq(core_search_index.id, cursor)) + .limit(1); + if (!boundary) { + return oldest + ? gt(core_search_index.id, cursor) + : lt(core_search_index.id, cursor); + } + + const at = sql`${boundary.createdAt}::timestamp`; + const row = sql`(${core_search_index.createdAt}, ${core_search_index.id})`; + + // The plain range is redundant with the row comparison; it is there so the + // planner can start from the `createdAt` index. + return oldest + ? sql`(${core_search_index.createdAt} >= ${at} AND ${row} > (${at}, ${cursor}::integer))` + : sql`(${core_search_index.createdAt} <= ${at} AND ${row} < (${at}, ${cursor}::integer))`; +}; export const PostgresSearchAdapter = (): SearchProviderApiPlugin => ({ name: "postgres", @@ -47,6 +84,7 @@ export const PostgresSearchAdapter = (): SearchProviderApiPlugin => ({ index: async () => {}, bulkIndex: async () => {}, delete: async () => {}, + deleteMany: async () => {}, clear: async () => {}, ping: async c => { @@ -103,19 +141,16 @@ export const PostgresSearchAdapter = (): SearchProviderApiPlugin => ({ if (useRelevance) { orderBy.push(sql`${rankExpr} DESC`, desc(core_search_index.id)); offset = cursorValue ?? 0; - } else if (params.sort === "oldest") { - orderBy.push(asc(core_search_index.createdAt), asc(core_search_index.id)); - if (cursorValue) { - const cond = gt(core_search_index.id, cursorValue); - where = where ? and(where, cond) : cond; - } } else { + const oldest = params.sort === "oldest"; orderBy.push( - desc(core_search_index.createdAt), - desc(core_search_index.id), + oldest + ? asc(core_search_index.createdAt) + : desc(core_search_index.createdAt), + oldest ? asc(core_search_index.id) : desc(core_search_index.id), ); if (cursorValue) { - const cond = lt(core_search_index.id, cursorValue); + const cond = await keysetAfter(c, cursorValue, oldest); where = where ? and(where, cond) : cond; } } @@ -130,7 +165,9 @@ export const PostgresSearchAdapter = (): SearchProviderApiPlugin => ({ authorId: leadAuthorId, authorIds: core_search_index.authorIds, title: core_search_index.title, - content: core_search_index.content, + content: term + ? core_search_index.content + : sql`left(${core_search_index.content}, ${CONTENT_EXCERPT_LENGTH}::integer)`, containerType: core_search_index.containerType, containerId: core_search_index.containerId, url: core_search_index.url, diff --git a/packages/vitnode/src/api/adapters/sso/discord.ts b/packages/vitnode/src/api/adapters/sso/discord.ts index 043d85b34f..a6b0d470a0 100644 --- a/packages/vitnode/src/api/adapters/sso/discord.ts +++ b/packages/vitnode/src/api/adapters/sso/discord.ts @@ -9,6 +9,9 @@ import { getRedirectUri } from "@/api/models/sso"; import { DISCORD_ICON } from "./icons"; +// A provider that stops answering would otherwise hang the sign-in request. +const PROVIDER_TIMEOUT_MS = 10_000; + const DISCORD_AVATAR_HASH = /^(?:a_)?[\da-f]{16,64}$/i; const discordAvatarUrl = ({ @@ -65,6 +68,7 @@ export const DiscordSSOApiPlugin = ({ client_id: clientId, client_secret: clientSecret, }), + signal: AbortSignal.timeout(PROVIDER_TIMEOUT_MS), }); if (!res.ok) { @@ -90,6 +94,7 @@ export const DiscordSSOApiPlugin = ({ headers: { Authorization: `${token_type} ${access_token}`, }, + signal: AbortSignal.timeout(PROVIDER_TIMEOUT_MS), }); const { data, error } = userSchema.safeParse(await res.json()); if (error || !data) { diff --git a/packages/vitnode/src/api/adapters/sso/facebook.ts b/packages/vitnode/src/api/adapters/sso/facebook.ts index 0597552b49..c1d12b3e6b 100644 --- a/packages/vitnode/src/api/adapters/sso/facebook.ts +++ b/packages/vitnode/src/api/adapters/sso/facebook.ts @@ -9,6 +9,9 @@ import { getRedirectUri } from "@/api/models/sso"; import { FACEBOOK_ICON } from "./icons"; +// A provider that stops answering would otherwise hang the sign-in request. +const PROVIDER_TIMEOUT_MS = 10_000; + export const FACEBOOK_GRAPH_VERSION = "v26.0"; const FACEBOOK_USER_FIELDS = @@ -61,7 +64,9 @@ export const FacebookSSOApiPlugin = ({ url.searchParams.set("redirect_uri", redirectUri); url.searchParams.set("client_id", clientId); url.searchParams.set("client_secret", clientSecret); - const res = await fetch(url.toString()); + const res = await fetch(url.toString(), { + signal: AbortSignal.timeout(PROVIDER_TIMEOUT_MS), + }); if (!res.ok) { throw new HTTPException( +res.status.toString() as ContentfulStatusCode, @@ -87,7 +92,9 @@ export const FacebookSSOApiPlugin = ({ ); url.searchParams.set("fields", FACEBOOK_USER_FIELDS); url.searchParams.set("access_token", access_token); - const res = await fetch(url.toString()); + const res = await fetch(url.toString(), { + signal: AbortSignal.timeout(PROVIDER_TIMEOUT_MS), + }); if (!res.ok) { throw new HTTPException( +res.status.toString() as ContentfulStatusCode, diff --git a/packages/vitnode/src/api/adapters/sso/google.ts b/packages/vitnode/src/api/adapters/sso/google.ts index 9fe004a711..e2ab02df39 100644 --- a/packages/vitnode/src/api/adapters/sso/google.ts +++ b/packages/vitnode/src/api/adapters/sso/google.ts @@ -9,6 +9,9 @@ import { getRedirectUri } from "@/api/models/sso"; import { GOOGLE_ICON } from "./icons"; +// A provider that stops answering would otherwise hang the sign-in request. +const PROVIDER_TIMEOUT_MS = 10_000; + export const GoogleSSOApiPlugin = ({ clientId, clientSecret, @@ -55,6 +58,7 @@ export const GoogleSSOApiPlugin = ({ client_id: clientId, client_secret: clientSecret, }), + signal: AbortSignal.timeout(PROVIDER_TIMEOUT_MS), }); if (!res.ok) { @@ -80,6 +84,7 @@ export const GoogleSSOApiPlugin = ({ headers: { Authorization: `${token_type} ${access_token}`, }, + signal: AbortSignal.timeout(PROVIDER_TIMEOUT_MS), }); const dataFromRes = await res.json(); const { data, error } = userSchema.safeParse(dataFromRes); diff --git a/packages/vitnode/src/api/config.ts b/packages/vitnode/src/api/config.ts index b6f6907cf1..d7c41dd5f8 100644 --- a/packages/vitnode/src/api/config.ts +++ b/packages/vitnode/src/api/config.ts @@ -77,8 +77,8 @@ export function VitNodeAPI({ }) { // Shared Redis client, created once at boot. Reused by the rate limiter // (which runs before the request context exists), the realtime pub/sub bridge, - // and the per-request cache exposed as `c.get("cache")`. Safe no-op when - // `redis` is unset. + // and the per-request cache exposed as `c.get("cache")`. Without `redis` the + // cache falls back to a small in-process store; see `MemoryCacheStore`. const redisClient = createCacheClient(vitNodeApiConfig.redis); // Bridge realtime (WebSocket) messages across instances via Redis pub/sub, so @@ -88,7 +88,9 @@ export function VitNodeAPI({ // A separate API process writes page invalidations to the web app's Redis. // Inside the web app the server entry has registered the store already. if (vitNodeApiConfig.pageCache && pageCacheRuntime() === undefined) { - const store = pageCacheStoreFor(vitNodeApiConfig.pageCache.storage); + const store = pageCacheStoreFor(vitNodeApiConfig.pageCache.storage, { + client: redisClient, + }); if (store === null) { // oxlint-disable-next-line no-console @@ -122,15 +124,32 @@ export function VitNodeAPI({ const docsEnabled = vitNodeApiConfig.docs?.enabled ?? CONFIG.node_development; if (docsEnabled) { - app.doc("/swagger/doc", { - openapi: "3.0.0", - info: { - version: manifest.version, - title: "VitNode API", - }, - tags: plugins.flatMap( - plugin => plugin.openApiTags?.map(name => ({ name })) ?? [], - ), + // `app.doc` regenerates the whole document - every route's schema - on each + // request. The routes are fixed once the plugins are mounted, so it is built + // on the first request (the plugins mount below, after this line) and the + // serialized JSON is served from then on. + let openApiJson: string | undefined; + app.get("/swagger/doc", c => { + try { + openApiJson ??= JSON.stringify( + app.getOpenAPIDocument({ + openapi: "3.0.0", + info: { + version: manifest.version, + title: "VitNode API", + }, + tags: plugins.flatMap( + plugin => plugin.openApiTags?.map(name => ({ name })) ?? [], + ), + }), + ); + + return c.body(openApiJson, 200, { + "Content-Type": "application/json", + }); + } catch (error) { + return c.json({ error: describeError(error) }, 500); + } }); } diff --git a/packages/vitnode/src/api/lib/ai/alt.ts b/packages/vitnode/src/api/lib/ai/alt.ts index f17427e674..5d95b99a66 100644 --- a/packages/vitnode/src/api/lib/ai/alt.ts +++ b/packages/vitnode/src/api/lib/ai/alt.ts @@ -316,6 +316,7 @@ const CONFIG_CODES = new Set([ ]); export const ALT_RETRY_AFTER_MS = 24 * 3_600_000; +const ALT_TRANSLATE_CONCURRENCY = 2; export const processAltForFile = async ( c: Context, @@ -437,8 +438,7 @@ export const processAltForFile = async ( .returning(); } - const written: string[] = []; - for (const languageCode of missing) { + const describeIn = async (languageCode: string): Promise => { let text = analysis.description; let runId = analysis.runId; if (languageCode !== ALT_BASE_LANGUAGE) { @@ -452,19 +452,59 @@ export const processAltForFile = async ( runId = translated.runId; } if ( - await writeAiAlt(db, { fileId, fingerprint, languageCode, runId, text }) + !(await writeAiAlt(db, { + fileId, + fingerprint, + languageCode, + runId, + text, + })) ) { - written.push(languageCode); - await c - .get("events") - ?.emit("files.alt.updated", { - fileId, - languageCodes: [languageCode], - origin: "ai", - }) - .catch(() => undefined); + return false; } - } + await c + .get("events") + ?.emit("files.alt.updated", { + fileId, + languageCodes: [languageCode], + origin: "ai", + }) + .catch(() => undefined); + + return true; + }; + + // A few translations at once. Each run still reserves its budget under + // row locks, so concurrency can't overspend; staying within + // `systemConcurrency` keeps the runs from tripping the throttle. The first + // failure stops new languages from starting. + const writtenCodes = new Set(); + const failures: unknown[] = []; + let next = 0; + const worker = async () => { + while (failures.length === 0 && next < missing.length) { + const languageCode = missing[next++]; + try { + if (await describeIn(languageCode)) writtenCodes.add(languageCode); + } catch (error) { + failures.push(error); + } + } + }; + await Promise.all( + Array.from( + { + length: Math.min( + ALT_TRANSLATE_CONCURRENCY, + Math.max(1, settings.systemConcurrency), + missing.length, + ), + }, + worker, + ), + ); + if (failures.length > 0) throw failures[0]; + const written = missing.filter(code => writtenCodes.has(code)); await setAltState(db, fileId, "completed"); @@ -474,6 +514,13 @@ export const processAltForFile = async ( return await deferForBudget(db, fileId, settings); } if (error instanceof QueueDeferError) throw error; + if (isAiError(error) && error.code === "AI_CONCURRENCY_LIMITED") { + // Every system slot is busy: wait for one instead of failing the file. + throw new QueueDeferError( + new Date(Date.now() + 60_000), + "Waiting for a free AI slot", + ); + } if (isAiError(error) && CONFIG_CODES.has(error.code)) { await setAltState(db, fileId, "failed", error.code); diff --git a/packages/vitnode/src/api/lib/ai/available-actions.ts b/packages/vitnode/src/api/lib/ai/available-actions.ts index d78a173292..7a18400267 100644 --- a/packages/vitnode/src/api/lib/ai/available-actions.ts +++ b/packages/vitnode/src/api/lib/ai/available-actions.ts @@ -39,34 +39,25 @@ export const availableAiActions = async ( ); } } - const [grantedByPermission, actionSettings] = await Promise.all([ - Promise.all( - [...permissionDefaults].map( - async ([permissionKey, defaultGranted]) => - [ - permissionKey, - ( - await ledger.resolveUserPolicy({ - defaultGranted, - permissionKey, - userId, - }) - ).granted, - ] as const, + const [policies, actionSettings] = await Promise.all([ + ledger.resolveUserPolicies({ + permissions: [...permissionDefaults].map( + ([permissionKey, defaultGranted]) => ({ + defaultGranted, + permissionKey, + }), ), - ).then(entries => new Map(entries)), - Promise.all( - runnableActions.map( - async action => await ledger.loadActionSettings(action.key), - ), - ), + settings, + userId, + }), + ledger.loadActionSettingsMany(runnableActions.map(action => action.key)), ]); return runnableActions .filter( - (action, index) => - actionSettings[index]?.enabled !== false && - grantedByPermission.get(action.permissionKey) === true, + action => + actionSettings.get(action.key)?.enabled !== false && + policies.get(action.permissionKey)?.granted === true, ) .map(action => action.key); }; diff --git a/packages/vitnode/src/api/lib/ai/ledger.ts b/packages/vitnode/src/api/lib/ai/ledger.ts index 9420070ab8..365089b698 100644 --- a/packages/vitnode/src/api/lib/ai/ledger.ts +++ b/packages/vitnode/src/api/lib/ai/ledger.ts @@ -121,12 +121,25 @@ export interface AiLedger { beginCall: (call: AiCallStart) => Promise; finishCall: (callId: number, call: AiCallFinish) => Promise; loadActionSettings: (actionKey: string) => Promise; + /** Settings for many actions in one read; actions without a row are absent. */ + loadActionSettingsMany: ( + actionKeys: string[], + ) => Promise>; loadSettings: () => Promise; markRunning: (runId: number, leaseExpiresAt: Date) => Promise; reserve: (request: AiReservationRequest) => Promise; + /** One user's policy for many permissions, sharing the role lookups. */ + resolveUserPolicies: (args: { + permissions: { defaultGranted: boolean; permissionKey: string }[]; + /** Already-loaded settings, to skip reading them again. */ + settings?: AiSettingsSnapshot; + userId: number; + }) => Promise>; resolveUserPolicy: (args: { defaultGranted: boolean; permissionKey: string; + /** Already-loaded settings, to skip reading them again. */ + settings?: AiSettingsSnapshot; userId: number; }) => Promise; settle: ( diff --git a/packages/vitnode/src/api/lib/ai/memory-ledger.ts b/packages/vitnode/src/api/lib/ai/memory-ledger.ts index b12e2450eb..0c53bfb7a6 100644 --- a/packages/vitnode/src/api/lib/ai/memory-ledger.ts +++ b/packages/vitnode/src/api/lib/ai/memory-ledger.ts @@ -112,6 +112,20 @@ export class MemoryAiLedger implements AiLedger { }); } + async loadActionSettingsMany( + actionKeys: string[], + ): Promise> { + const entries = await Promise.all( + actionKeys.map( + async key => [key, await this.loadActionSettings(key)] as const, + ), + ); + + return new Map( + entries.flatMap(([key, settings]) => (settings ? [[key, settings]] : [])), + ); + } + async loadSettings(): Promise { return Promise.resolve(this.settings); } @@ -236,6 +250,23 @@ export class MemoryAiLedger implements AiLedger { return Promise.resolve({ ok: true, runId: id }); } + async resolveUserPolicies({ + permissions, + userId, + }: { + permissions: { defaultGranted: boolean; permissionKey: string }[]; + userId: number; + }): Promise> { + return Promise.resolve( + new Map( + permissions.map(({ permissionKey }) => [ + permissionKey, + this.policy({ permissionKey, userId }), + ]), + ), + ); + } + async resolveUserPolicy({ permissionKey, userId, diff --git a/packages/vitnode/src/api/lib/ai/postgres-ledger.ts b/packages/vitnode/src/api/lib/ai/postgres-ledger.ts index 27a69de0a6..f6080e5229 100644 --- a/packages/vitnode/src/api/lib/ai/postgres-ledger.ts +++ b/packages/vitnode/src/api/lib/ai/postgres-ledger.ts @@ -308,25 +308,39 @@ export class PostgresAiLedger implements AiLedger { async loadActionSettings( actionKey: string, ): Promise { - const [row] = await this.db + return ( + (await this.loadActionSettingsMany([actionKey])).get(actionKey) ?? null + ); + } + + async loadActionSettingsMany( + actionKeys: string[], + ): Promise> { + const keys = [...new Set(actionKeys)]; + if (keys.length === 0) return new Map(); + + const rows = await this.db .select() .from(core_ai_action_settings) - .where(eq(core_ai_action_settings.actionKey, actionKey)) - .limit(1); - if (!row) return null; - - return { - dailyLimit: row.dailyLimit, - enabled: row.enabled, - fallbackModelId: row.fallbackModelId, - instructions: row.instructions, - maxInputCharacters: row.maxInputCharacters, - maxOutputTokens: row.maxOutputTokens, - maxRetries: row.maxRetries, - maxSteps: row.maxSteps, - modelId: row.modelId, - timeoutMs: row.timeoutMs, - }; + .where(inArray(core_ai_action_settings.actionKey, keys)); + + return new Map( + rows.map(row => [ + row.actionKey, + { + dailyLimit: row.dailyLimit, + enabled: row.enabled, + fallbackModelId: row.fallbackModelId, + instructions: row.instructions, + maxInputCharacters: row.maxInputCharacters, + maxOutputTokens: row.maxOutputTokens, + maxRetries: row.maxRetries, + maxSteps: row.maxSteps, + modelId: row.modelId, + timeoutMs: row.timeoutMs, + }, + ]), + ); } async loadSettings(): Promise { @@ -382,45 +396,66 @@ export class PostgresAiLedger implements AiLedger { } } - async resolveUserPolicy({ - defaultGranted, - permissionKey, + async resolveUserPolicies({ + permissions: requested, + settings: loadedSettings, userId, }: { - defaultGranted: boolean; - permissionKey: string; + permissions: { defaultGranted: boolean; permissionKey: string }[]; + settings?: AiSettingsSnapshot; userId: number; - }): Promise { - const [user] = await this.db - .select({ roleId: core_users.roleId }) + }): Promise> { + // The primary role and every secondary role in one round trip. + const userRoles = await this.db + .select({ + roleId: core_users.roleId, + secondaryRoleId: core_users_secondary_roles.roleId, + }) .from(core_users) - .where(eq(core_users.id, userId)) - .limit(1); - if (!user) return { dailyLimit: null, granted: false, monthlyPoints: 0n }; + .leftJoin( + core_users_secondary_roles, + eq(core_users_secondary_roles.userId, core_users.id), + ) + .where(eq(core_users.id, userId)); + if (userRoles.length === 0) { + return new Map( + requested.map(({ permissionKey }) => [ + permissionKey, + { dailyLimit: null, granted: false, monthlyPoints: 0n }, + ]), + ); + } - const secondary = await this.db - .select({ roleId: core_users_secondary_roles.roleId }) - .from(core_users_secondary_roles) - .where(eq(core_users_secondary_roles.userId, userId)); const roleIds = [ - ...new Set([user.roleId, ...secondary.map(r => r.roleId)]), + ...new Set( + userRoles.flatMap(row => + row.secondaryRoleId === null + ? [row.roleId] + : [row.roleId, row.secondaryRoleId], + ), + ), + ]; + const permissionKeys = [ + ...new Set(requested.map(item => item.permissionKey)), ]; - const [roles, permissions, policies, [override], settings] = + const [roles, permissionRows, policies, [override], settings] = await Promise.all([ this.db .select({ id: core_roles.id, root: core_roles.root }) .from(core_roles) .where(inArray(core_roles.id, roleIds)), - this.db - .select() - .from(core_ai_role_permissions) - .where( - and( - inArray(core_ai_role_permissions.roleId, roleIds), - eq(core_ai_role_permissions.permission, permissionKey), - ), - ), + permissionKeys.length === 0 + ? [] + : this.db + .select() + .from(core_ai_role_permissions) + .where( + and( + inArray(core_ai_role_permissions.roleId, roleIds), + inArray(core_ai_role_permissions.permission, permissionKeys), + ), + ), this.db .select() .from(core_ai_role_policies) @@ -430,29 +465,66 @@ export class PostgresAiLedger implements AiLedger { .from(core_ai_user_overrides) .where(eq(core_ai_user_overrides.userId, userId)) .limit(1), - this.loadSettings(), + loadedSettings ?? this.loadSettings(), ]); - return resolvePolicy({ - defaultGranted, - defaultMonthlyPoints: settings.defaultMonthlyPoints, - override: override - ? { - blocked: override.blocked, - monthlyPoints: parseDecimalOrNull(override.monthlyPoints), - unlimited: override.unlimited, - } - : null, - permissions: permissions.map(row => ({ - dailyLimit: row.dailyLimit, - granted: row.granted, - })), - policies: policies.map(row => ({ - monthlyPoints: parseDecimalOrNull(row.monthlyPoints), - unlimited: row.unlimited, - })), - root: roles.some(role => role.root), + const root = roles.some(role => role.root); + const parsedOverride = override + ? { + blocked: override.blocked, + monthlyPoints: parseDecimalOrNull(override.monthlyPoints), + unlimited: override.unlimited, + } + : null; + const parsedPolicies = policies.map(row => ({ + monthlyPoints: parseDecimalOrNull(row.monthlyPoints), + unlimited: row.unlimited, + })); + + return new Map( + requested.map(({ defaultGranted, permissionKey }) => [ + permissionKey, + resolvePolicy({ + defaultGranted, + defaultMonthlyPoints: settings.defaultMonthlyPoints, + override: parsedOverride, + permissions: permissionRows + .filter(row => row.permission === permissionKey) + .map(row => ({ + dailyLimit: row.dailyLimit, + granted: row.granted, + })), + policies: parsedPolicies, + root, + }), + ]), + ); + } + + async resolveUserPolicy({ + defaultGranted, + permissionKey, + settings, + userId, + }: { + defaultGranted: boolean; + permissionKey: string; + settings?: AiSettingsSnapshot; + userId: number; + }): Promise { + const policies = await this.resolveUserPolicies({ + permissions: [{ defaultGranted, permissionKey }], + settings, + userId, }); + + return ( + policies.get(permissionKey) ?? { + dailyLimit: null, + granted: false, + monthlyPoints: 0n, + } + ); } async settle( diff --git a/packages/vitnode/src/api/lib/ai/runner.ts b/packages/vitnode/src/api/lib/ai/runner.ts index ff32e5ac8e..3611ad0220 100644 --- a/packages/vitnode/src/api/lib/ai/runner.ts +++ b/packages/vitnode/src/api/lib/ai/runner.ts @@ -527,6 +527,7 @@ export class AiRunner { policy = await this.ledger.resolveUserPolicy({ defaultGranted: definition.permission.defaultGranted, permissionKey: action.permissionKey, + settings, userId: actor.userId, }); if (!policy.granted) { diff --git a/packages/vitnode/src/api/lib/ai/stream-response.ts b/packages/vitnode/src/api/lib/ai/stream-response.ts index 49cb099bd1..ca9d949966 100644 --- a/packages/vitnode/src/api/lib/ai/stream-response.ts +++ b/packages/vitnode/src/api/lib/ai/stream-response.ts @@ -6,6 +6,8 @@ import { isAiError } from "./errors"; export const AI_NDJSON_HEADERS = { "Content-Type": "application/x-ndjson; charset=utf-8", + // nginx buffers proxied responses by default, which would hold the stream back. + "X-Accel-Buffering": "no", } as const; export const aiNdjsonStream = async ( diff --git a/packages/vitnode/src/api/lib/ai/usage-summary.ts b/packages/vitnode/src/api/lib/ai/usage-summary.ts index e806c5d54d..e24333f2a9 100644 --- a/packages/vitnode/src/api/lib/ai/usage-summary.ts +++ b/packages/vitnode/src/api/lib/ai/usage-summary.ts @@ -2,7 +2,7 @@ import type { Context } from "hono"; import { and, eq, gte, sql } from "drizzle-orm"; -import type { AiLedger, AiUserPolicy } from "./ledger"; +import type { AiLedger } from "./ledger"; import { core_ai_budget_periods, core_ai_runs } from "../../../database/ai"; import { AI_USAGE_WARNING_SHARE } from "../../../lib/ai/usage-tone"; @@ -85,26 +85,19 @@ export const loadUserAiUsage = async ( ); } } - const policies = new Map( - await Promise.all( - [...permissionDefaults].map( - async ([permissionKey, defaultGranted]) => - [ - permissionKey, - await ledger.resolveUserPolicy({ - defaultGranted, - permissionKey, - userId, - }), - ] as const, - ), + const policies = await ledger.resolveUserPolicies({ + permissions: [...permissionDefaults].map( + ([permissionKey, defaultGranted]) => ({ defaultGranted, permissionKey }), ), - ); + settings, + userId, + }); const allowance = [...policies.values()][0] ?? (await ledger.resolveUserPolicy({ defaultGranted: false, permissionKey: "", + settings, userId, })); @@ -150,16 +143,27 @@ export const loadUserAiUsage = async ( return policy?.granted ? [{ action, policy }] : []; }); - const actionUsages = await Promise.all( - grantedActions.map(async ({ action, policy }) => { - const [daily, actionSettings] = await Promise.all([ - readRow(db, userDailyScope(userId, action.permissionKey), day.start), - ledger.loadActionSettings(action.key), - ]); - - return { action, actionSettings, daily, policy }; - }), - ); + const [settingsByAction, dailyRows] = await Promise.all([ + ledger.loadActionSettingsMany( + grantedActions.map(({ action }) => action.key), + ), + Promise.all( + grantedActions.map( + async ({ action }) => + await readRow( + db, + userDailyScope(userId, action.permissionKey), + day.start, + ), + ), + ), + ]); + const actionUsages = grantedActions.map(({ action, policy }, index) => ({ + action, + actionSettings: settingsByAction.get(action.key) ?? null, + daily: dailyRows[index], + policy, + })); const actions: AiUserActionUsage[] = []; for (const { action, actionSettings, daily, policy } of actionUsages) { if (actionSettings?.enabled === false) continue; diff --git a/packages/vitnode/src/api/lib/cache-memory.ts b/packages/vitnode/src/api/lib/cache-memory.ts new file mode 100644 index 0000000000..3b5fcbeedc --- /dev/null +++ b/packages/vitnode/src/api/lib/cache-memory.ts @@ -0,0 +1,116 @@ +/** + * Upper bound on how long the in-process store keeps a cached value, whatever + * TTL the caller asked for. + * + * The trade-off: this store lives in one process. On a single instance an + * invalidation (a revoked session, a demoted admin) deletes the entry directly, + * so it takes effect at once. With several instances and no Redis, an + * invalidation only reaches the instance that handled it - the others keep + * serving their copy until it expires. Capping every TTL bounds that window to + * a few seconds while still absorbing the burst of lookups an authenticated + * page load makes. Configure Redis for a cache shared across instances. + */ +export const MEMORY_CACHE_MAX_TTL_SECONDS = 15; + +/** Enough for every active session on a busy single instance, and bounded. */ +export const MEMORY_CACHE_MAX_ENTRIES = 10_000; + +interface MemoryEntry { + /** Epoch ms, or `null` for a key written without a TTL. */ + expiresAt: null | number; + raw: string; +} + +/** + * The cache backend used when no Redis client is configured: a bounded LRU + * over a `Map` (insertion order doubles as recency order). Holds serialized + * JSON so callers get a fresh copy on every read, as they would from Redis. + * + * Keys written without a TTL (generation stamps such as the staff-permission + * epoch) never expire on time, only by eviction - and since every entry filed + * under an old generation stops being read once the stamp moves, LRU evicts + * those entries before it ever evicts the stamp that still gets read. + */ +export class MemoryCacheStore { + constructor(maxEntries = MEMORY_CACHE_MAX_ENTRIES) { + this.maxEntries = maxEntries; + } + + private readonly entries = new Map(); + private readonly maxEntries: number; + + private insert(key: string, entry: MemoryEntry): void { + this.entries.delete(key); + this.entries.set(key, entry); + + while (this.entries.size > this.maxEntries) { + const oldest = this.entries.keys().next(); + if (oldest.done) break; + this.entries.delete(oldest.value); + } + } + + private live(key: string, now = Date.now()): MemoryEntry | null { + const entry = this.entries.get(key); + if (!entry) return null; + + if (entry.expiresAt !== null && entry.expiresAt <= now) { + this.entries.delete(key); + + return null; + } + + return entry; + } + + delete(keys: string[]): void { + for (const key of keys) this.entries.delete(key); + } + + deleteByPrefix(prefix: string): void { + for (const key of [...this.entries.keys()]) { + if (key.startsWith(prefix)) this.entries.delete(key); + } + } + + get(key: string): null | string { + const entry = this.live(key); + if (!entry) return null; + + // Touch: move to the most-recently-used end. + this.entries.delete(key); + this.entries.set(key, entry); + + return entry.raw; + } + + has(key: string): boolean { + return this.live(key) !== null; + } + + /** Stores a cached value, its TTL capped at {@link MEMORY_CACHE_MAX_TTL_SECONDS}. */ + set(key: string, raw: string, ttlSeconds?: number): void { + const ttl = + ttlSeconds && ttlSeconds > 0 + ? Math.min(ttlSeconds, MEMORY_CACHE_MAX_TTL_SECONDS) + : null; + + this.insert(key, { + expiresAt: ttl === null ? null : Date.now() + ttl * 1000, + raw, + }); + } + + /** + * `SET NX EX` for locks. The TTL is honoured as given rather than capped: a + * lock is a coordination window, not a cached copy that can go stale. + */ + setIfAbsent(key: string, raw: string, ttlSeconds: number): boolean { + const now = Date.now(); + if (this.live(key, now)) return false; + + this.insert(key, { expiresAt: now + ttlSeconds * 1000, raw }); + + return true; + } +} diff --git a/packages/vitnode/src/api/lib/cache.ts b/packages/vitnode/src/api/lib/cache.ts index 6d48d1ac2c..fe09a6757d 100644 --- a/packages/vitnode/src/api/lib/cache.ts +++ b/packages/vitnode/src/api/lib/cache.ts @@ -1,6 +1,8 @@ import type { Context } from "hono"; import type { RedisClientOptions, RedisClientType } from "redis"; +import type { MemoryCacheStore } from "./cache-memory"; + /** Connection options accepted by `redis` - `url` plus any client option. */ export type CacheConfig = RedisClientOptions; @@ -10,14 +12,26 @@ export type CacheClient = RedisClientType; const CACHE_PREFIX = "vitnode:cache:"; const SYSTEM_NAMESPACE = "__system__"; +/** + * Per-request cache facade. Backed by Redis when a client is configured; without + * one, by the process-wide {@link MemoryCacheStore} the global middleware passes + * in (short TTLs, single-instance only - see `MEMORY_CACHE_MAX_TTL_SECONDS`). + * With neither, every method is a no-op. + */ export class CacheModel { - constructor(client: CacheClient | null, c: Context) { + constructor( + client: CacheClient | null, + c: Context, + memory: MemoryCacheStore | null = null, + ) { this.c = c; this.client = client; + this.memory = client ? null : memory; } protected readonly c: Context; protected readonly client: CacheClient | null; + protected readonly memory: MemoryCacheStore | null; private key(key: string): string { return `${this.prefix()}${key}`; @@ -36,10 +50,10 @@ export class CacheModel { } private async readKey(fullKey: string): Promise { - if (!this.client) return null; - try { - const raw = await this.client.get(fullKey); + const raw = this.client + ? await this.client.get(fullKey) + : (this.memory?.get(fullKey) ?? null); return raw === null ? null : (JSON.parse(raw) as T); } catch { @@ -47,8 +61,28 @@ export class CacheModel { } } + private async readKeys(fullKeys: string[]): Promise<(null | T)[]> { + if (fullKeys.length === 0) return []; + + try { + const raws = this.client + ? await this.client.mGet(fullKeys) + : fullKeys.map(key => this.memory?.get(key) ?? null); + + return raws.map(raw => + typeof raw === "string" ? (JSON.parse(raw) as T) : null, + ); + } catch { + return fullKeys.map(() => null); + } + } + private async removeKeys(fullKeys: string[]): Promise { - if (!this.client) return; + if (!this.client) { + this.memory?.delete(fullKeys); + + return; + } try { if (fullKeys.length > 0) await this.client.del(fullKeys); @@ -66,10 +100,13 @@ export class CacheModel { value: T, ttlSeconds?: number, ): Promise { - if (!this.client) return; - try { const raw = JSON.stringify(value); + if (!this.client) { + this.memory?.set(fullKey, raw, ttlSeconds); + + return; + } if (ttlSeconds && ttlSeconds > 0) { await this.client.set(fullKey, raw, { expiration: { type: "EX", value: ttlSeconds }, @@ -85,13 +122,22 @@ export class CacheModel { /** * Acquire a short-lived distributed lock (`SET key val NX EX ttl`) in the * system namespace. Returns `true` when the lock is held by this caller. - * **Without Redis it returns `true`** so cache-less / single-instance - * deployments still make progress - callers must guard correctness some other - * way (e.g. Postgres `FOR UPDATE SKIP LOCKED`). Returns `false` on a Redis - * error so a flaky connection skips rather than double-runs work. + * **Without Redis the lock is only process-wide** (and with no store at all + * it always returns `true`), so multi-instance deployments still make + * progress - callers must guard correctness some other way (e.g. Postgres + * `FOR UPDATE SKIP LOCKED`). Returns `false` on a Redis error so a flaky + * connection skips rather than double-runs work. */ async acquireLock(key: string, ttlSeconds: number): Promise { - if (!this.client) return true; + if (!this.client) { + return ( + this.memory?.setIfAbsent( + this.systemKey(`lock:${key}`), + "1", + ttlSeconds, + ) ?? true + ); + } try { const result = await this.client.set(this.systemKey(`lock:${key}`), "1", { @@ -105,15 +151,15 @@ export class CacheModel { } } - /** Remove one or more keys. No-op without Redis. */ + /** Remove one or more keys. */ async delete(key: string | string[]): Promise { const keys = (Array.isArray(key) ? key : [key]).map(k => this.key(k)); await this.removeKeys(keys); } /** - * Remove one or more keys from the framework system namespace. No-op without - * Redis. See {@link SYSTEM_NAMESPACE}. + * Remove one or more keys from the framework system namespace. See + * {@link SYSTEM_NAMESPACE}. */ async deleteSystem(key: string | string[]): Promise { const keys = (Array.isArray(key) ? key : [key]).map(k => this.systemKey(k)); @@ -123,10 +169,14 @@ export class CacheModel { /** * Delete every cache key for the current plugin * (`vitnode:cache:{plugin_code}:*`). Other plugins' keys and unrelated Redis - * data are left untouched. No-op without Redis. + * data are left untouched. */ async flush(): Promise { - if (!this.client) return; + if (!this.client) { + this.memory?.deleteByPrefix(this.prefix()); + + return; + } try { // `scanIterator` yields a batch of keys per SCAN round-trip. @@ -143,7 +193,7 @@ export class CacheModel { } } - /** Read a JSON value. Returns `null` on a miss, without Redis, or on error. */ + /** Read a JSON value. Returns `null` on a miss, without a store, or on error. */ async get(key: string): Promise { return this.readKey(this.key(key)); } @@ -152,9 +202,17 @@ export class CacheModel { return this.readKey(this.systemKey(key)); } - /** Whether a key currently exists. `false` without Redis or on error. */ + /** + * Reads several system-namespace keys in one round trip (`MGET`), in order. + * Each slot is `null` on a miss; every slot is `null` on error. + */ + async getSystemMany(keys: string[]): Promise<(null | T)[]> { + return this.readKeys(keys.map(key => this.systemKey(key))); + } + + /** Whether a key currently exists. `false` without a store or on error. */ async has(key: string): Promise { - if (!this.client) return false; + if (!this.client) return this.memory?.has(this.key(key)) ?? false; try { return (await this.client.exists(this.key(key))) === 1; @@ -163,7 +221,7 @@ export class CacheModel { } } - /** Release a lock taken with {@link acquireLock}. No-op without Redis. */ + /** Release a lock taken with {@link acquireLock}. */ async releaseLock(key: string): Promise { await this.removeKeys([this.systemKey(`lock:${key}`)]); } diff --git a/packages/vitnode/src/api/lib/check-staff-permission.ts b/packages/vitnode/src/api/lib/check-staff-permission.ts index feec43f155..f86b0e20a1 100644 --- a/packages/vitnode/src/api/lib/check-staff-permission.ts +++ b/packages/vitnode/src/api/lib/check-staff-permission.ts @@ -21,8 +21,10 @@ import { mergeStaffPermissionGrants, } from "./staff-permission"; import { + readRequestStaffPermissions, readStaffPermissions, readStaffPermissionsOfUser, + rememberRequestStaffPermissions, STAFF_TYPES, writeStaffPermissions, } from "./staff-permission-cache"; @@ -106,20 +108,30 @@ const toStaffPermissionSet = ( return { root: false, permissions, staff: entries.length > 0 }; }; -const loadStaffPermissions = async ( +const loadStaffPermissionsForRoles = async ( c: Context, - { type, user }: { type: PermissionStaffType; user: StaffUser }, + args: { roleIds: number[]; type: PermissionStaffType; userId: number }, ): Promise => { - const roleIds = await getUserRoleIds(c, user); - - if (await hasRootRole(c, roleIds)) return rootStaffPermissions(); + // Both at once: the entries query is wasted for a root user, but a root user + // is the rare case and the two round trips don't queue behind each other. + const [root, entries] = await Promise.all([ + hasRootRole(c, args.roleIds), + findStaffEntries(c, args), + ]); - return toStaffPermissionSet( - c, - await findStaffEntries(c, { roleIds, type, userId: user.id }), - ); + return root ? rootStaffPermissions() : toStaffPermissionSet(c, entries); }; +const loadStaffPermissions = async ( + c: Context, + { type, user }: { type: PermissionStaffType; user: StaffUser }, +): Promise => + await loadStaffPermissionsForRoles(c, { + roleIds: await getUserRoleIds(c, user), + type, + userId: user.id, + }); + const loadAndCacheStaffPermissions = async ( c: Context, args: { type: PermissionStaffType; user: StaffUser }, @@ -137,24 +149,54 @@ const loadAndCacheStaffPermissions = async ( export const resolveStaffPermissions = async ( c: Context, { type, user }: { type: PermissionStaffType; user: StaffUser }, -): Promise => - (await readStaffPermissions(c, { type, userId: user.id })) ?? - (await loadAndCacheStaffPermissions(c, { type, user })); +): Promise => { + const args = { type, userId: user.id }; + const remembered = readRequestStaffPermissions(c, args); + if (remembered) return remembered; -const findStaffUser = async ( + const resolved = + (await readStaffPermissions(c, args)) ?? + (await loadAndCacheStaffPermissions(c, { type, user })); + rememberRequestStaffPermissions(c, args, resolved, { live: false }); + + return resolved; +}; + +/** The user's role ids - primary and secondary - in one query, or `null`. */ +const findStaffUserRoleIds = async ( c: Context, userId: number, -): Promise => { - const [user] = await c +): Promise => { + const rows = await c .get("db") - .select({ id: core_users.id, roleId: core_users.roleId }) + .select({ + roleId: core_users.roleId, + secondaryRoleId: core_users_secondary_roles.roleId, + }) .from(core_users) - .where(eq(core_users.id, userId)) - .limit(1); + .leftJoin( + core_users_secondary_roles, + eq(core_users_secondary_roles.userId, core_users.id), + ) + .where(eq(core_users.id, userId)); + + const [first] = rows; + if (!first) return null; + + const roleIds = new Set([first.roleId]); + for (const { secondaryRoleId } of rows) { + if (typeof secondaryRoleId === "number") roleIds.add(secondaryRoleId); + } - return user ?? null; + return [...roleIds]; }; +/** + * Whether the user is staff of `type`. With `live`, reads the database and + * ignores the cross-request cache - the session checks use it so a revoked + * admin loses access at once. Either way the answer is remembered for the rest + * of the request, so a second check from the same source does not repeat it. + */ export const isStaff = async ( c: Context, { @@ -163,15 +205,29 @@ export const isStaff = async ( userId, }: { live?: boolean; type: PermissionStaffType; userId: number }, ): Promise => { - const cached = live ? null : await readStaffPermissions(c, { type, userId }); - if (cached) return cached.staff; + const args = { type, userId }; + const remembered = readRequestStaffPermissions(c, args, { live }); + if (remembered) return remembered.staff; - const user = await findStaffUser(c, userId); - if (!user) return false; + if (!live) { + const cached = await readStaffPermissions(c, args); + if (cached) { + rememberRequestStaffPermissions(c, args, cached, { live: false }); + + return cached.staff; + } + } + + const roleIds = await findStaffUserRoleIds(c, userId); + if (!roleIds) return false; - const resolved = live - ? await loadStaffPermissions(c, { type, user }) - : await loadAndCacheStaffPermissions(c, { type, user }); + const resolved = await loadStaffPermissionsForRoles(c, { + roleIds, + type, + userId, + }); + if (!live) await writeStaffPermissions(c, args, resolved); + rememberRequestStaffPermissions(c, args, resolved, { live: true }); return resolved.staff; }; diff --git a/packages/vitnode/src/api/lib/cron.ts b/packages/vitnode/src/api/lib/cron.ts index 1a4597a6f4..f908f83973 100644 --- a/packages/vitnode/src/api/lib/cron.ts +++ b/packages/vitnode/src/api/lib/cron.ts @@ -52,6 +52,12 @@ const reportFailedCronTick = (reason: string) => { ); }; +// Just under the 1-minute tick. The API keeps running jobs after we stop +// waiting (each run is claimed in the database), so giving up here only frees +// the ticker - with `noOverlap` a hung request would otherwise stall every +// later tick. +const CRON_TICK_TIMEOUT_MS = 55_000; + export const handleCronJobs = async () => { const url = new URL("/api/@vitnode/core/cron", CONFIG.api.origin); const headers: HeadersInit = { @@ -66,12 +72,16 @@ export const handleCronJobs = async () => { const response = await fetch(url.toString(), { method: "POST", headers, + signal: AbortSignal.timeout(CRON_TICK_TIMEOUT_MS), }); if (!response.ok) { reportFailedCronTick(`${response.status} ${await response.text()}`); } } catch (error) { + // Slow jobs, not a failure: the API is still working through them. + if (error instanceof Error && error.name === "TimeoutError") return; + reportFailedCronTick( error instanceof Error ? error.message : String(error), ); diff --git a/packages/vitnode/src/api/lib/staff-permission-cache.ts b/packages/vitnode/src/api/lib/staff-permission-cache.ts index eb350fae16..77cc4cd3a7 100644 --- a/packages/vitnode/src/api/lib/staff-permission-cache.ts +++ b/packages/vitnode/src/api/lib/staff-permission-cache.ts @@ -13,9 +13,61 @@ const EPOCH_KEY = "staff-permissions:epoch"; export const STAFF_TYPES = ["admin", "moderator"] as const; -const readEpoch = async (c: Context): Promise => +interface RequestStaffMemo { + epoch?: Promise; + sets: Map; +} + +// Per request, keyed by the Hono context so it dies with the request. Live +// (database) and cached answers are kept apart: a live check only reuses a live +// answer and a cached check only a cached one, so the memo never changes which +// source a check trusts. Cleared by every invalidation below, so a request that +// changes permissions and then checks them again sees the change. +const requestMemos = new WeakMap(); + +const memoOf = (c: Context): RequestStaffMemo => { + let memo = requestMemos.get(c); + if (!memo) { + memo = { sets: new Map() }; + requestMemos.set(c, memo); + } + + return memo; +}; + +const memoKey = ( + { type, userId }: { type: PermissionStaffType; userId: number }, + live: boolean, +): string => `${live ? "live" : "cached"}:${type}:${userId}`; + +export const readRequestStaffPermissions = ( + c: Context, + args: { type: PermissionStaffType; userId: number }, + { live = false }: { live?: boolean } = {}, +): null | ResolvedStaffPermissionSet => + requestMemos.get(c)?.sets.get(memoKey(args, live)) ?? null; + +export const rememberRequestStaffPermissions = ( + c: Context, + args: { type: PermissionStaffType; userId: number }, + set: ResolvedStaffPermissionSet, + { live }: { live: boolean }, +): void => { + memoOf(c).sets.set(memoKey(args, live), set); +}; + +const fetchEpoch = async (c: Context): Promise => (await c.get("cache").getSystem(EPOCH_KEY)) ?? "0"; +// The memo holds the promise, set before awaiting, so an invalidation that +// lands while the read is in flight clears it and the next read starts over. +const readEpoch = async (c: Context): Promise => { + const memo = memoOf(c); + memo.epoch ??= fetchEpoch(c); + + return await memo.epoch; +}; + const permissionsKey = ( epoch: string, { type, userId }: { type: PermissionStaffType; userId: number }, @@ -69,16 +121,12 @@ export const readStaffPermissionsOfUser = async ( userId: number, ): Promise => { const epoch = await readEpoch(c); - const readType = async (type: PermissionStaffType) => - await c - .get("cache") - .getSystem( - permissionsKey(epoch, { type, userId }), - ); - const [admin, moderator] = await Promise.all([ - readType("admin"), - readType("moderator"), - ]); + const [admin = null, moderator = null] = await c + .get("cache") + .getSystemMany([ + permissionsKey(epoch, { type: "admin", userId }), + permissionsKey(epoch, { type: "moderator", userId }), + ]); return { sets: { admin, moderator }, @@ -106,6 +154,9 @@ export const readStaffPermissionsOfUser = async ( export const invalidateAllStaffPermissions = async ( c: Context, ): Promise => { + const memo = memoOf(c); + memo.epoch = undefined; + memo.sets.clear(); await c.get("cache").setSystem(EPOCH_KEY, randomUUID()); }; @@ -120,7 +171,15 @@ export const invalidateStaffPermissionsForUser = async ( c: Context, userId: number, ): Promise => { - const epoch = await readEpoch(c); + const memo = memoOf(c); + for (const type of STAFF_TYPES) { + memo.sets.delete(memoKey({ type, userId }, true)); + memo.sets.delete(memoKey({ type, userId }, false)); + } + // Read fresh, not from the memo: deleting under a generation another request + // has since moved past would leave the live entries in place. + memo.epoch = fetchEpoch(c); + const epoch = await memo.epoch; await c .get("cache") diff --git a/packages/vitnode/src/api/lib/with-pagination.test.ts b/packages/vitnode/src/api/lib/with-pagination.test.ts index 3267258014..c5659bb5dd 100644 --- a/packages/vitnode/src/api/lib/with-pagination.test.ts +++ b/packages/vitnode/src/api/lib/with-pagination.test.ts @@ -79,7 +79,11 @@ const pageRequest = async ({ table: rows, }); - return { ...seen.mock.calls[0][0], pageInfo }; + // A clamped page is read twice; the last read is the one that is served. + const lastCall = seen.mock.lastCall; + if (!lastCall) throw new Error("The page query never ran."); + + return { ...lastCall[0], pageInfo }; }; const pageFor = async (args: { diff --git a/packages/vitnode/src/api/lib/with-pagination.ts b/packages/vitnode/src/api/lib/with-pagination.ts index a6492c3113..695d004b41 100644 --- a/packages/vitnode/src/api/lib/with-pagination.ts +++ b/packages/vitnode/src/api/lib/with-pagination.ts @@ -360,8 +360,6 @@ export async function withPagination< ? and(baseWhere, cursorWhere) : (baseWhere ?? cursorWhere); - const totalCount = await fetchTotalCount(c, table, baseWhere); - /** * The cursor value, projected by the page query itself. * @@ -378,16 +376,31 @@ export async function withPagination< }; const pageSize = first ?? last ?? 50; + const limit = pageSize + 1; + const pageAt = async (offset: number) => + await query({ cursorSelection, limit, offset, where, orderBy }); + + // The count and the page run side by side. A numbered page is read at the + // offset that was asked for, and read again only when the count says it is + // past the end and has to be clamped onto the last page. + const requestedOffset = + requestedPage === undefined ? 0 : (requestedPage - 1) * pageSize; + const [totalCount, speculativeEdges] = await Promise.all([ + fetchTotalCount(c, table, baseWhere), + pageAt(requestedOffset), + ]); + const totalPages = Math.ceil(totalCount / pageSize); const currentPage = requestedPage === undefined ? null : Math.max(1, Math.min(requestedPage, totalPages || 1)); - const offset = currentPage === null ? 0 : (currentPage - 1) * pageSize; - const limit = pageSize + 1; - const edges = await query({ cursorSelection, limit, offset, where, orderBy }); + const edges = + currentPage === null || currentPage === requestedPage + ? speculativeEdges + : await pageAt((currentPage - 1) * pageSize); const requested = first ?? last ?? edges.length; const hasMore = edges.length > requested; diff --git a/packages/vitnode/src/api/middlewares/captcha.middleware.ts b/packages/vitnode/src/api/middlewares/captcha.middleware.ts index 40a0cc9474..c47330ecca 100644 --- a/packages/vitnode/src/api/middlewares/captcha.middleware.ts +++ b/packages/vitnode/src/api/middlewares/captcha.middleware.ts @@ -4,6 +4,16 @@ import { HTTPException } from "hono/http-exception"; import type { VitNodeApiConfig } from "../../vitnode.config"; +// A provider that hangs would otherwise hold the request (and its connection) +// open indefinitely. Five seconds is generous for a single verification call. +const CAPTCHA_VERIFY_TIMEOUT_MS = 5000; + +interface CaptchaVerification { + "error-codes"?: string[]; + score: number; + success: boolean; +} + const getResFromReCaptcha = async ({ token, userIp, @@ -12,7 +22,7 @@ const getResFromReCaptcha = async ({ captchaConfig: NonNullable["captcha"]>; token: string; userIp: string; -}): Promise<{ "error-codes"?: string[]; score: number; success: boolean }> => { +}): Promise => { // An install that configured a captcha but no secret key cannot verify // anything. Said here rather than left to the provider to reject a malformed // request: the answer is the same either way, but only one of them is a @@ -35,6 +45,7 @@ const getResFromReCaptcha = async ({ headers: { "Content-Type": "application/json", }, + signal: AbortSignal.timeout(CAPTCHA_VERIFY_TIMEOUT_MS), }, ); @@ -65,6 +76,7 @@ const getResFromReCaptcha = async ({ response: token, remoteip: userIp, }), + signal: AbortSignal.timeout(CAPTCHA_VERIFY_TIMEOUT_MS), }); const data: { @@ -102,11 +114,17 @@ export const captchaMiddleware = () => { }); } + // Fails closed: a timeout, a network error or an unparseable answer from + // the provider is a failed verification, never a pass. const res = await getResFromReCaptcha({ token, userIp: c.get("ipAddress"), captchaConfig, - }); + }).catch((): CaptchaVerification => ({ + success: false, + score: 0, + "error-codes": ["verification-unavailable"], + })); if (!res.success || res.score < 0.5) { throw new HTTPException(400, { diff --git a/packages/vitnode/src/api/middlewares/global.middleware.ts b/packages/vitnode/src/api/middlewares/global.middleware.ts index b65205e0f6..00a1326b3a 100644 --- a/packages/vitnode/src/api/middlewares/global.middleware.ts +++ b/packages/vitnode/src/api/middlewares/global.middleware.ts @@ -16,6 +16,7 @@ import { LocalEventsAdapter } from "@/api/adapters/events/local"; import { PostgresSearchAdapter } from "@/api/adapters/search/postgres"; import { DEFAULT_SESSION_COOKIE_NAME } from "@/api/lib/auth-cookie"; import { CacheModel } from "@/api/lib/cache"; +import { MemoryCacheStore } from "@/api/lib/cache-memory"; import { AIModel } from "@/api/models/ai"; import { EmailModel } from "@/api/models/email"; import { EventsModel } from "@/api/models/events"; @@ -229,6 +230,8 @@ export interface EnvVariablesVitNode { }; } +type CoreVariables = EnvVariablesVitNode["core"]; + export const globalMiddleware = ({ ai, authorization, @@ -489,30 +492,10 @@ export const globalMiddleware = ({ ), }); - return async (c: Context, next: Next) => { - if (!c.get("ipAddress")) { - c.set("ipAddress", resolveClientIp(c)); - } - c.set("db", dbProvider); - c.set("ai", new AIModel(c)); - c.set("cache", new CacheModel(cacheClient, c)); - c.set("email", new EmailModel(c)); - c.set("events", new EventsModel(c)); - c.set("i18n", new I18nModel(c)); - c.set("notifications", new NotificationsModel(c as Context)); - c.set("queue", new QueueModel(c)); - c.set("search", new SearchModel(c)); - c.set("storage", new StorageModel(c)); - c.set("realtime", realtime); - - // Resolved before `core` is set rather than per mint, so the integrations - // panel and the routes read the same value. Memoised, so this is one query - // on the first request of the process and nothing afterwards. - const contentPreviewSecret = hasPreviewableContentTypes - ? await ensureContentPreviewSecret(dbProvider) - : undefined; - - c.set("core", { + // Everything in `core` but the two values below is fixed once the plugins are + // in, so it is built once here - adapters, nested config objects and all. + const staticCore: Omit = + { ai, aiActions: aiActionsMetadata, i18n: i18nMetadata, @@ -546,8 +529,6 @@ export const globalMiddleware = ({ personalInformationFields: resolvePersonalInformationFields( users?.personalInformation, ), - contentPreviewSecret, - cronSecret: CONFIG.cronJobSecret, hasCronAdapter: !!cron, plugins: pluginsMetadata, installedPlugins, @@ -564,7 +545,55 @@ export const globalMiddleware = ({ contentModels: contentModelsMetadata, contentRevalidateOrigins: content?.revalidateOrigins, contentTypes: contentTypesMetadata, - }); + }; + let core: CoreVariables | undefined; + + // Without Redis, sessions, devices and staff permissions would otherwise hit + // the database on every request. One store per app, shared by its requests. + const memoryCache = cacheClient ? null : new MemoryCacheStore(); + + // The preview secret is memoised after the first request and the cron secret + // is read from the environment, so in practice this hands every request the + // same object. It is shared, so nothing may mutate it. + const coreFor = ( + contentPreviewSecret: string | undefined, + cronSecret: string, + ): CoreVariables => { + if ( + !core || + core.contentPreviewSecret !== contentPreviewSecret || + core.cronSecret !== cronSecret + ) { + core = { ...staticCore, contentPreviewSecret, cronSecret }; + } + + return core; + }; + + return async (c: Context, next: Next) => { + if (!c.get("ipAddress")) { + c.set("ipAddress", resolveClientIp(c)); + } + c.set("db", dbProvider); + c.set("ai", new AIModel(c)); + c.set("cache", new CacheModel(cacheClient, c, memoryCache)); + c.set("email", new EmailModel(c)); + c.set("events", new EventsModel(c)); + c.set("i18n", new I18nModel(c)); + c.set("notifications", new NotificationsModel(c as Context)); + c.set("queue", new QueueModel(c)); + c.set("search", new SearchModel(c)); + c.set("storage", new StorageModel(c)); + c.set("realtime", realtime); + + // Resolved before `core` is set rather than per mint, so the integrations + // panel and the routes read the same value. Memoised, so this is one query + // on the first request of the process and nothing afterwards. + const contentPreviewSecret = hasPreviewableContentTypes + ? await ensureContentPreviewSecret(dbProvider) + : undefined; + + c.set("core", coreFor(contentPreviewSecret, CONFIG.cronJobSecret)); // Whether a localized content type's `defaultLocale` names a row in // `core_languages` is a fact about the *installation*, so it cannot be @@ -585,6 +614,7 @@ export const globalMiddleware = ({ await next(); c.get("notifications").flushAfterResponse(); + c.get("email").flushAfterResponse(); }; }; diff --git a/packages/vitnode/src/api/models/device.ts b/packages/vitnode/src/api/models/device.ts index b6a42fa7e4..a61cf544b8 100644 --- a/packages/vitnode/src/api/models/device.ts +++ b/packages/vitnode/src/api/models/device.ts @@ -42,6 +42,14 @@ interface KnownDevice { publicId: string; } +// One lookup per request: session and admin-session resolution both ask for +// the device, and the answer cannot change mid-request (the cookie is the +// request's own). Keyed by the context so it is dropped with the request. +const existingDeviceByRequest = new WeakMap< + Context, + Promise +>(); + export class DeviceModel { constructor(c: Context) { this.c = c; @@ -81,6 +89,29 @@ export class DeviceModel { return this.c.req.header("User-Agent") ?? "node"; } + private async lookupExistingDevice(): Promise { + const publicId = this.getDeviceCookie(); + if (!publicId) return null; + + const now = new Date(); + const cached = await this.c + .get("cache") + .getSystem(deviceCacheKey(publicId)); + + if ( + cached && + !isDeviceTouchDue({ + stored: { ...cached, lastSeen: new Date(cached.lastSeen) }, + current: this.getFingerprint(), + now, + }) + ) { + return { id: cached.id, publicId }; + } + + return await this.resolveDevice(publicId, now); + } + private async resolveDevice( publicId: string, now: Date, @@ -149,26 +180,16 @@ export class DeviceModel { * row per request, for as long as anybody cared to keep sending them. */ async getExistingDeviceId(): Promise { - const publicId = this.getDeviceCookie(); - if (!publicId) return null; - - const now = new Date(); - const cached = await this.c - .get("cache") - .getSystem(deviceCacheKey(publicId)); + const remembered = existingDeviceByRequest.get(this.c); + if (remembered) return await remembered; - if ( - cached && - !isDeviceTouchDue({ - stored: { ...cached, lastSeen: new Date(cached.lastSeen) }, - current: this.getFingerprint(), - now, - }) - ) { - return { id: cached.id, publicId }; - } + const lookup = this.lookupExistingDevice(); + existingDeviceByRequest.set(this.c, lookup); + // A failed lookup is not remembered, so a retry in the same request can + // still succeed. + void lookup.catch(() => existingDeviceByRequest.delete(this.c)); - return await this.resolveDevice(publicId, now); + return await lookup; } /** diff --git a/packages/vitnode/src/api/models/email.ts b/packages/vitnode/src/api/models/email.ts index 5886b30523..184092c5a0 100644 --- a/packages/vitnode/src/api/models/email.ts +++ b/packages/vitnode/src/api/models/email.ts @@ -2,7 +2,11 @@ import type { Context, ContextVariableMap } from "hono"; import type React from "react"; import { HTTPException } from "hono/http-exception"; -import { render } from "react-email"; +import { render, toPlainText } from "react-email"; + +import type { EnvVitNode } from "@/api/middlewares/global.middleware"; + +import { processQueueTasksByIds } from "@/api/modules/queue/helpers/process-queue-tasks"; import type { DefaultTemplateEmailProps } from "../../emails/default-template"; @@ -70,6 +74,7 @@ export class EmailModel { } protected readonly c: Context; + private readonly queued: number[] = []; private requireProvider() { const provider = this.c.get("core").email?.adapter; @@ -126,14 +131,18 @@ export class EmailModel { }); } + // One React render: the plain-text part is the same conversion `render` + // runs for `{ plainText: true }`, applied to the HTML we already have. + const renderedHtml = await render(htmlContent); + return { to: emailTo, subject: typeof subject === "function" ? subject({ i18n: { locale, messages } }) : subject, - html: await render(htmlContent), - text: await render(htmlContent, { plainText: true }), + html: renderedHtml, + text: toPlainText(renderedHtml), replyTo, }; } @@ -157,12 +166,34 @@ export class EmailModel { return sendResult ?? {}; } + /** + * Deliver what this request queued right after the response, so a + * verification or password-reset email doesn't wait for the next queue + * cron tick. The queue row stays the durable fallback if this run fails. + */ + flushAfterResponse(): void { + const ids = this.queued.splice(0); + if (ids.length === 0) return; + + void processQueueTasksByIds(this.c as Context, ids).catch( + async (error: unknown) => { + await this.c + .get("log") + .warn( + `[Email] Immediate delivery deferred to the queue: ${error instanceof Error ? error.message : String(error)}`, + ); + }, + ); + } + async send(args: EmailModelSendArgs): Promise { this.requireProvider(); const email = await this.build(args); - await this.c.get("queue").dispatch({ + const { id } = await this.c.get("queue").dispatch({ name: "send-email", + // The task is core's even when a plugin route sends the email. + pluginId: "@vitnode/core", payload: { to: email.to, subject: email.subject, @@ -171,5 +202,6 @@ export class EmailModel { ...(email.replyTo ? { replyTo: email.replyTo } : {}), }, }); + if (id !== null) this.queued.push(id); } } diff --git a/packages/vitnode/src/api/models/notifications/admin.ts b/packages/vitnode/src/api/models/notifications/admin.ts index 5e3de8cf62..1596ac3b60 100644 --- a/packages/vitnode/src/api/models/notifications/admin.ts +++ b/packages/vitnode/src/api/models/notifications/admin.ts @@ -25,10 +25,13 @@ import { reconcileNotificationCounts, UNREAD_SQL } from "./inbox"; import { translateOr } from "./preferences"; import { createTranslatorCache } from "./render"; import { + expireNotificationOverviewTotals, getNotificationRegistry, getNotificationWorkers, isEmailConfigured, loadNotificationSettings, + NOTIFICATIONS_OVERVIEW_TOTALS_CACHE_KEY, + NOTIFICATIONS_OVERVIEW_TOTALS_TTL_SECONDS, NOTIFICATIONS_PLUGIN_ID, typeSettingsKey, } from "./shared"; @@ -40,6 +43,49 @@ const countBy = (rows: { count: number; key: K }[]) => { return counts; }; +interface NotificationOverviewTotals { + customizedMembers: number; + inboxItems: number; + unreadItems: number; +} + +const loadOverviewTotals = async ( + c: NotificationsContext, +): Promise => + await c + .get("cache") + .remember( + NOTIFICATIONS_OVERVIEW_TOTALS_CACHE_KEY, + NOTIFICATIONS_OVERVIEW_TOTALS_TTL_SECONDS, + async () => { + const db = c.get("db"); + // Unread comes from the per-member counters every inbox write keeps + // under the member's lock (and `reconcileNotifications` repairs), so it + // is one pass over members rather than a filtered pass over every item. + const [[inbox], [members]] = await Promise.all([ + db.select({ items: count() }).from(core_notifications), + db + .select({ + customized: + sql`count(*) filter (where ${core_notification_user_state.preferences} <> '{}'::jsonb)`.mapWith( + Number, + ), + unread: + sql`coalesce(sum(${core_notification_user_state.unreadCount}), 0)`.mapWith( + Number, + ), + }) + .from(core_notification_user_state), + ]); + + return { + customizedMembers: members.customized, + inboxItems: inbox.items, + unreadItems: members.unread, + }; + }, + ); + export const getNotificationsOverview = async (c: NotificationsContext) => { const db = c.get("db"); const core = c.get("core"); @@ -53,8 +99,7 @@ export const getNotificationsOverview = async (c: NotificationsContext) => { eventRows, [oldestPending], cronHealth, - [inbox], - [members], + totals, ] = await Promise.all([ loadNotificationSettings(db), createTranslatorCache(c)(c.get("admin")?.user.language), @@ -88,26 +133,15 @@ export const getNotificationsOverview = async (c: NotificationsContext) => { .orderBy(core_notification_events.createdAt) .limit(1), loadCronHealth(db), - db - .select({ - items: count(), - unread: sql`count(*) filter (where ${UNREAD_SQL})`.mapWith( - Number, - ), - }) - .from(core_notifications), - db - .select({ customized: count() }) - .from(core_notification_user_state) - .where(sql`${core_notification_user_state.preferences} <> '{}'::jsonb`), + loadOverviewTotals(c), ]); return { counts: { - customizedMembers: members.customized, - inboxItems: inbox.items, + customizedMembers: totals.customizedMembers, + inboxItems: totals.inboxItems, queuedEmails: countBy(deliveryRows).pending ?? 0, - unreadItems: inbox.unread, + unreadItems: totals.unreadItems, }, email: { adapterConfigured: !!core.email?.adapter, @@ -369,6 +403,7 @@ export const reconcileNotifications = async ( c, mismatches.map(row => row.userId), ); + await expireNotificationOverviewTotals(c); return { corrected, mismatched: mismatches.length }; }; diff --git a/packages/vitnode/src/api/models/notifications/danger.ts b/packages/vitnode/src/api/models/notifications/danger.ts index 8007b0ede1..bf98cbbec1 100644 --- a/packages/vitnode/src/api/models/notifications/danger.ts +++ b/packages/vitnode/src/api/models/notifications/danger.ts @@ -16,6 +16,7 @@ import { applyUnreadDeltas, lockUserStates } from "./fanout"; import { removeNotificationItems, UNREAD_SQL } from "./inbox"; import { deleteUnreferencedEvents } from "./maintenance"; import { + expireNotificationOverviewTotals, GLOBAL_SETTINGS_KEY, loadNotificationSettings, NOTIFICATIONS_PLUGIN_ID, @@ -150,6 +151,7 @@ export const markEverythingRead = async ( if (batch.count === 0) break; } + await expireNotificationOverviewTotals(c); await c.get("events").emit("notifications.read_all", { items, members }); return { items, members }; @@ -160,6 +162,7 @@ export const deleteAllNotifications = async ( ): Promise<{ events: number; items: number }> => { const items = await removeNotificationItems(c, { where: sql`true` }); const events = await deleteUnreferencedEvents(c.get("db")); + await expireNotificationOverviewTotals(c); await c.get("events").emit("notifications.deleted_all", { items }); @@ -179,6 +182,7 @@ export const resetMemberNotificationPreferences = async ( .where(sql`${core_notification_user_state.preferences} <> '{}'::jsonb`) .returning({ userId: core_notification_user_state.userId }); + await expireNotificationOverviewTotals(c); await c .get("events") .emit("notifications.preferences_reset", { members: rows.length }); diff --git a/packages/vitnode/src/api/models/notifications/email.ts b/packages/vitnode/src/api/models/notifications/email.ts index f4638ca954..14e2f5f676 100644 --- a/packages/vitnode/src/api/models/notifications/email.ts +++ b/packages/vitnode/src/api/models/notifications/email.ts @@ -37,6 +37,7 @@ import { checkNotificationAccess, createTranslatorCache, loadActors, + notificationEventColumns, presentNotificationEmail, resolveEvent, } from "./render"; @@ -231,7 +232,7 @@ const deliverNotificationEmail = async ( await db .select({ createdAt: core_notification_receipts.createdAt, - event: core_notification_events, + event: notificationEventColumns, eventId: core_notification_receipts.eventId, notificationId: core_notification_receipts.notificationId, seq: core_notification_receipts.seq, @@ -283,32 +284,40 @@ const deliverNotificationEmail = async ( ); const entries: (NotificationEmailEntry & { subject: string })[] = []; - for (const { event } of kept) { - const resolved = resolveEvent(c, event); - const allowed = resolved - ? await checkNotificationAccess({ - c, + // Every entry's plugin `access` hook at once; `Promise.all` keeps the order. + const presentedEntries = await Promise.all( + kept.map(async ({ event }) => { + const resolved = resolveEvent(c, event); + if (!resolved) return { event, presented: null }; + + const allowed = await checkNotificationAccess({ + c, + data: resolved.data, + event, + registered: resolved.registered, + userIds: [user.id], + }); + if (!allowed.has(user.id)) return { event, presented: null }; + + const actor = + event.actorId === null ? undefined : actors.get(event.actorId); + + return { + event, + presented: presentNotificationEmail(c, { + actors: { actorCount: actor ? 1 : 0, actors: actor ? [actor] : [] }, data: resolved.data, event, + eventCount: 1, + locale, registered: resolved.registered, - userIds: [user.id], - }) - : new Set(); - const actor = - event.actorId === null ? undefined : actors.get(event.actorId); - const presented = - resolved && allowed.has(user.id) - ? presentNotificationEmail(c, { - actors: { actorCount: actor ? 1 : 0, actors: actor ? [actor] : [] }, - data: resolved.data, - event, - eventCount: 1, - locale, - registered: resolved.registered, - t, - }) - : null; + t, + }), + }; + }), + ); + for (const { event, presented } of presentedEntries) { if (!presented) { dropped.push(event.id); continue; @@ -592,7 +601,7 @@ export const planNotificationDigests = async ( db .select({ createdAt: core_notification_receipts.createdAt, - event: core_notification_events, + event: notificationEventColumns, userId: core_notification_receipts.userId, }) .from(core_notification_receipts) diff --git a/packages/vitnode/src/api/models/notifications/fanout.ts b/packages/vitnode/src/api/models/notifications/fanout.ts index 7ad93b5a64..33aa47a25b 100644 --- a/packages/vitnode/src/api/models/notifications/fanout.ts +++ b/packages/vitnode/src/api/models/notifications/fanout.ts @@ -24,7 +24,7 @@ import type { } from "./shared"; import { dispatchEmailDrain } from "./email-queue"; -import { checkNotificationAccess } from "./render"; +import { checkNotificationAccess, notificationEventColumns } from "./render"; import { getNotificationRegistry, getNotificationWorkers, @@ -111,18 +111,18 @@ interface Candidates { userIds: number[]; } +const parseIdList = (value: string): number[] => + value === "" ? [] : value.split(",").map(Number); + const nextCandidates = ( - event: NotificationEventRow, - batchSize: number, + event: Pick, + { recipientCount, slice }: { recipientCount: number; slice: string }, ): Candidates => { - const userIds = event.recipientIds.slice( - event.recipientCursor, - event.recipientCursor + batchSize, - ); + const userIds = parseIdList(slice); const recipientCursor = event.recipientCursor + userIds.length; return { - done: recipientCursor >= event.recipientIds.length, + done: recipientCursor >= recipientCount, recipientCursor, userIds, }; @@ -184,15 +184,31 @@ const processBatch = async ( eventId: number, settings: NotificationSettingsSnapshot, ): Promise => { - const [event] = await tx - .select() + const { fanoutBatchSize } = getNotificationWorkers(c); + const { recipientCursor, recipientIds } = core_notification_events; + // Only this batch's slice of `recipientIds` (up to 100k ids) leaves the + // database, so a large audience costs linear, not quadratic, transfer. Arrays + // are 1-based and the bounds inclusive: `[cursor + 1 : cursor + size]` is + // exactly JS `slice(cursor, cursor + size)`, clamped at the end the same way. + // `array_to_string` hands it over as text, sidestepping driver array parsing. + const [row] = await tx + .select({ + event: notificationEventColumns, + recipientCount: sql`cardinality(${recipientIds})`.mapWith(Number), + slice: sql`array_to_string(${recipientIds}[(${recipientCursor} + 1):(${recipientCursor} + ${fanoutBatchSize}::integer)], ',')`, + }) .from(core_notification_events) .where(eq(core_notification_events.id, eventId)) .for("update"); - if (!event || event.status === "completed" || event.status === "failed") { + if ( + !row || + row.event.status === "completed" || + row.event.status === "failed" + ) { return { delivered: 0, done: true, states: [] }; } + const { event } = row; const registered = getNotificationRegistry(c).get(event.type); if (registered?.pluginId !== event.pluginId) { @@ -213,10 +229,7 @@ const processBatch = async ( return { delivered: 0, done: true, states: [] }; } - const candidates = nextCandidates( - event, - getNotificationWorkers(c).fanoutBatchSize, - ); + const candidates = nextCandidates(event, row); const eligible = await filterRecipients({ c, candidates: candidates.userIds, diff --git a/packages/vitnode/src/api/models/notifications/inbox.ts b/packages/vitnode/src/api/models/notifications/inbox.ts index 17e25c0f20..0c7706b631 100644 --- a/packages/vitnode/src/api/models/notifications/inbox.ts +++ b/packages/vitnode/src/api/models/notifications/inbox.ts @@ -1,4 +1,4 @@ -import { and, desc, eq, inArray, isNull, lt, or, sql } from "drizzle-orm"; +import { and, asc, desc, eq, inArray, isNull, sql } from "drizzle-orm"; import { HTTPException } from "hono/http-exception"; import { z } from "zod"; @@ -22,6 +22,7 @@ import { checkNotificationAccess, createTranslatorCache, loadActorSummaries, + notificationEventColumns, presentNotification, resolveEvent, } from "./render"; @@ -56,7 +57,14 @@ export interface NotificationListPage { nextCursor: null | string; } -const cursorSchema = z.object({ id: z.number().int(), t: z.number().int() }); +// `lastActivityAt` is a microsecond `timestamp`, so the cursor carries the +// database's own `::text` of it: a millisecond `Date` would round the boundary +// down and skip every row in the lost microseconds. +const TIMESTAMP_TEXT = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d{1,6})?$/; +const cursorSchema = z.object({ + id: z.number().int(), + ts: z.string().regex(TIMESTAMP_TEXT), +}); const encodeCursor = (cursor: z.infer): string => Buffer.from(JSON.stringify(cursor)).toString("base64url"); @@ -75,7 +83,9 @@ const decodeCursor = (raw: string) => { throw new HTTPException(400, { message: "Invalid pagination cursor." }); } - return { id: cursor.data.id, lastActivityAt: new Date(cursor.data.t) }; + // A row comparison, unlike the equivalent `a < x OR (a = x AND id < y)`, is + // something Postgres can seek the `(userId, lastActivityAt, id)` index with. + return sql`(${core_notifications.lastActivityAt}, ${core_notifications.id}) < (${cursor.data.ts}::timestamp, ${cursor.data.id}::bigint)`; }; export const getNotificationState = async ( @@ -114,13 +124,14 @@ export const listNotifications = async ( userId: number; }, ): Promise => { - const after = cursor ? decodeCursor(cursor) : null; + const after = cursor ? decodeCursor(cursor) : undefined; const rows = await c .get("db") .select({ + cursorAt: sql`${core_notifications.lastActivityAt}::text`, + event: notificationEventColumns, item: core_notifications, - event: core_notification_events, }) .from(core_notifications) .innerJoin( @@ -134,15 +145,7 @@ export const listNotifications = async ( unreadOnly ? UNREAD_SQL : undefined, category ? eq(core_notifications.category, category) : undefined, type ? eq(core_notifications.type, type) : undefined, - after - ? or( - lt(core_notifications.lastActivityAt, after.lastActivityAt), - and( - eq(core_notifications.lastActivityAt, after.lastActivityAt), - lt(core_notifications.id, after.id), - ), - ) - : undefined, + after, ), ) .orderBy( @@ -233,10 +236,7 @@ export const listNotifications = async ( items, nextCursor: rows.length > limit && last - ? encodeCursor({ - id: last.item.id, - t: last.item.lastActivityAt.getTime(), - }) + ? encodeCursor({ id: last.item.id, ts: last.cursorAt }) : null, }; }; @@ -244,7 +244,7 @@ export const listNotifications = async ( type ItemRow = typeof core_notifications.$inferSelect; const USERS_PER_RECONCILE_BATCH = 200; -const USERS_PER_REMOVE_BATCH = 50; +const ITEMS_PER_REMOVE_BATCH = 500; const isCounted = ( item: Pick, @@ -459,22 +459,36 @@ export const removeNotificationItems = async ( let removed = 0; for (;;) { + // Item ids in `lastActivityAt` order, so the retention sweep walks the + // `last_activity` index a page at a time. const targets = await c .get("db") - .select({ userId: core_notifications.userId }) + .select({ id: core_notifications.id, userId: core_notifications.userId }) .from(core_notifications) .where(where) - .groupBy(core_notifications.userId) - .limit(USERS_PER_REMOVE_BATCH); + .orderBy(asc(core_notifications.lastActivityAt)) + .limit(ITEMS_PER_REMOVE_BATCH); if (targets.length === 0) break; - const userIds = targets.map(row => row.userId); const batch = await c.get("db").transaction(async tx => { - await lockUserStates(tx, userIds); + await lockUserStates( + tx, + targets.map(row => row.userId), + ); + // `where` again, under the lock: an item that gained activity since the + // read above may no longer match. const deleted = await tx .delete(core_notifications) - .where(and(where, inArray(core_notifications.userId, userIds))) + .where( + and( + where, + inArray( + core_notifications.id, + targets.map(row => row.id), + ), + ), + ) .returning({ activitySeq: core_notifications.activitySeq, archivedAt: core_notifications.archivedAt, @@ -498,7 +512,7 @@ export const removeNotificationItems = async ( sendNotificationStates(c, batch.updates); removed += batch.count; - if (batch.count === 0) break; + if (targets.length < ITEMS_PER_REMOVE_BATCH || batch.count === 0) break; } return removed; diff --git a/packages/vitnode/src/api/models/notifications/render.ts b/packages/vitnode/src/api/models/notifications/render.ts index cdb4bccced..621defc399 100644 --- a/packages/vitnode/src/api/models/notifications/render.ts +++ b/packages/vitnode/src/api/models/notifications/render.ts @@ -25,7 +25,34 @@ import type { NotificationsContext } from "./shared"; import { getNotificationRegistry } from "./shared"; -export type NotificationEventRow = typeof core_notification_events.$inferSelect; +/** + * The event columns every read path needs. `recipientIds` is left out on + * purpose: it holds up to 100k ids (~400KB) and only fan-out reads it, one slice + * at a time. `lastError` is write-only bookkeeping. + */ +export const notificationEventColumns = { + actorId: core_notification_events.actorId, + allowSelf: core_notification_events.allowSelf, + completedAt: core_notification_events.completedAt, + createdAt: core_notification_events.createdAt, + data: core_notification_events.data, + deliveredCount: core_notification_events.deliveredCount, + groupKey: core_notification_events.groupKey, + id: core_notification_events.id, + idempotencyKey: core_notification_events.idempotencyKey, + pluginId: core_notification_events.pluginId, + recipientCursor: core_notification_events.recipientCursor, + schemaVersion: core_notification_events.schemaVersion, + status: core_notification_events.status, + subjectId: core_notification_events.subjectId, + subjectType: core_notification_events.subjectType, + type: core_notification_events.type, +}; + +export type NotificationEventRow = Omit< + typeof core_notification_events.$inferSelect, + "lastError" | "recipientIds" +>; export interface NotificationActor { avatarColor: string; diff --git a/packages/vitnode/src/api/models/notifications/shared.ts b/packages/vitnode/src/api/models/notifications/shared.ts index 9050d77d82..798705326c 100644 --- a/packages/vitnode/src/api/models/notifications/shared.ts +++ b/packages/vitnode/src/api/models/notifications/shared.ts @@ -70,6 +70,19 @@ export const loadNotificationSettings = async ( return { global: normalizeGlobalSettings(global), policies }; }; +// The overview's totals scan whole tables, and an AdminCP tab left open polls +// them. A minute of staleness is fine for a dashboard; the bulk actions that +// change them by a lot expire the entry so their result shows at once. +export const NOTIFICATIONS_OVERVIEW_TOTALS_CACHE_KEY = + "notifications:overview:totals"; +export const NOTIFICATIONS_OVERVIEW_TOTALS_TTL_SECONDS = 60; + +export const expireNotificationOverviewTotals = async ( + c: NotificationsContext, +): Promise => { + await c.get("cache").delete(NOTIFICATIONS_OVERVIEW_TOTALS_CACHE_KEY); +}; + export const getNotificationWorkers = ( c: NotificationsContext, ): NotificationWorkerSettings => @@ -82,8 +95,9 @@ export const sendNotificationStates = ( c: NotificationsContext, states: Iterable, ): void => { - const realtime = c.get("realtime"); - for (const { userId, ...message } of states) { - realtime.sendToUser(userId, notificationsStateChannel, message); - } + // One pub/sub publish for the whole batch. + c.get("realtime").sendToUsers( + notificationsStateChannel, + Array.from(states, ({ userId, ...data }) => ({ data, userId })), + ); }; diff --git a/packages/vitnode/src/api/models/search.test.ts b/packages/vitnode/src/api/models/search.test.ts index 61a1568038..f078d04e23 100644 --- a/packages/vitnode/src/api/models/search.test.ts +++ b/packages/vitnode/src/api/models/search.test.ts @@ -1,5 +1,7 @@ +import type { SQL } from "drizzle-orm"; import type { Context } from "hono"; +import { PgDialect } from "drizzle-orm/pg-core"; import { describe, expect, it, vi } from "vitest"; import type { RegisteredContentType } from "@/content/registry"; @@ -50,7 +52,7 @@ const createContext = ( ) => { const onConflictDoUpdate = vi.fn().mockResolvedValue(undefined); const values = vi.fn< - (row: { content: string; isPublic: boolean; pluginId: string }) => { + (rows: { content: string; isPublic: boolean; pluginId: string }[]) => { onConflictDoUpdate: typeof onConflictDoUpdate; } >(() => ({ onConflictDoUpdate })); @@ -89,7 +91,7 @@ describe("SearchModel", () => { await new SearchModel(c).index(doc); expect(insert).toHaveBeenCalledWith(core_search_index); - const row = values.mock.calls[0][0]; + const row = values.mock.calls[0][0][0]; expect(row.content).toBe("Hello world"); expect(row.pluginId).toBe("core"); expect(row.isPublic).toBe(true); @@ -118,7 +120,7 @@ describe("SearchModel", () => { await new SearchModel(c).index({ ...doc, pluginId: "@vitnode/example" }); - expect(values.mock.calls[0][0].pluginId).toBe("@vitnode/example"); + expect(values.mock.calls[0][0][0].pluginId).toBe("@vitnode/example"); expect(provider.index).toHaveBeenCalledWith( c, expect.objectContaining({ pluginId: "@vitnode/example" }), @@ -131,7 +133,7 @@ describe("SearchModel", () => { await new SearchModel(c).index(doc); - expect(values.mock.calls[0][0].pluginId).toBe("@vitnode/example"); + expect(values.mock.calls[0][0][0].pluginId).toBe("@vitnode/example"); expect(provider.index).toHaveBeenCalledWith( c, expect.objectContaining({ pluginId: "@vitnode/example" }), @@ -146,7 +148,7 @@ describe("SearchModel", () => { await new SearchModel(c).index({ ...doc, pluginId: " " }); - expect(values.mock.calls[0][0].pluginId).toBe("@vitnode/example"); + expect(values.mock.calls[0][0][0].pluginId).toBe("@vitnode/example"); }); it("falls back to core for a blank owner outside a plugin request", async () => { @@ -155,7 +157,7 @@ describe("SearchModel", () => { await new SearchModel(c).index({ ...doc, pluginId: "" }); - expect(values.mock.calls[0][0].pluginId).toBe("core"); + expect(values.mock.calls[0][0][0].pluginId).toBe("core"); }); it("falls back to core outside a plugin request", async () => { @@ -164,7 +166,7 @@ describe("SearchModel", () => { await new SearchModel(c).index(doc); - expect(values.mock.calls[0][0].pluginId).toBe("core"); + expect(values.mock.calls[0][0][0].pluginId).toBe("core"); }); it("resolves every document in a bulk write", async () => { @@ -178,7 +180,9 @@ describe("SearchModel", () => { { ...doc, itemId: 3 }, ]); - expect(values.mock.calls.map(call => call[0].pluginId)).toEqual([ + // One multi-row upsert for the whole batch. + expect(values).toHaveBeenCalledTimes(1); + expect(values.mock.calls[0][0].map(row => row.pluginId)).toEqual([ "@vitnode/example", "@vitnode/blog", "@vitnode/core", @@ -199,9 +203,12 @@ describe("SearchModel", () => { await new SearchModel(c).index(doc); const { onConflictDoUpdate } = values.mock.results[0].value; - expect(onConflictDoUpdate.mock.calls[0][0].set).toMatchObject({ - pluginId: "@vitnode/example", - }); + const { set } = onConflictDoUpdate.mock.calls[0][0] as { + set: { pluginId: SQL }; + }; + expect(new PgDialect().sqlToQuery(set.pluginId).sql).toBe( + 'excluded."pluginId"', + ); }); }); @@ -507,7 +514,7 @@ describe("item ids under every strategy", () => { title: "Event", }); - expect(values.mock.calls[0][0]).toMatchObject({ itemId: BEYOND_SAFE }); + expect(values.mock.calls[0][0][0]).toMatchObject({ itemId: BEYOND_SAFE }); expect(provider.index).toHaveBeenCalledWith( c, expect.objectContaining({ itemId: BEYOND_SAFE }), diff --git a/packages/vitnode/src/api/models/search.ts b/packages/vitnode/src/api/models/search.ts index 6b5c6a5b08..bfea5d3fc1 100644 --- a/packages/vitnode/src/api/models/search.ts +++ b/packages/vitnode/src/api/models/search.ts @@ -30,7 +30,7 @@ import { storageUrlOf } from "@/api/lib/storage-url"; import { contentIdFromKey, contentIdKey } from "@/content/ids"; import { contentLocalesMatch } from "@/content/locale"; import { core_files } from "@/database/files"; -import { core_search_index } from "@/database/search"; +import { core_search_index, core_search_popularity } from "@/database/search"; import { core_users } from "@/database/users"; import { containsLikePattern } from "@/lib/api/like-pattern"; import { parseImageDimensions } from "@/lib/api/upload"; @@ -40,6 +40,10 @@ const authorAvatarFile = alias(core_files, "search_author_avatar_file"); const SEARCH_TOP_AUTHORS_TTL_SECONDS = 5 * 60; +// Rows per multi-row upsert: 17 columns × 500 stays far below Postgres' 65,535 +// bind-parameter cap while still turning a rebuild page into one round trip. +const BULK_UPSERT_CHUNK = 500; + export interface SearchDocument { authorIds?: readonly number[]; containerId?: number; @@ -335,6 +339,16 @@ export interface SearchProviderApiPlugin { itemId: ContentId, languageCode?: string, ): Promise; + /** + * Removes many items of one type, in every language - what deleting a + * container means for its children. Optional: without it, {@link SearchModel} + * falls back to one `delete` per item. + */ + deleteMany?: ( + c: Context, + itemType: string, + itemIds: ContentId[], + ) => Promise; index: (c: Context, doc: SearchDocument) => Promise; name: string; ping?: (c: Context) => Promise; @@ -511,38 +525,53 @@ export class SearchModel { }; } - private async upsertRow(doc: SearchDocument): Promise { - const row = toRow(doc); - - await this.c - .get("db") - .insert(core_search_index) - .values(row) - .onConflictDoUpdate({ - target: [ - core_search_index.itemType, - core_search_index.itemId, - core_search_index.languageCode, - ], - set: { - // Included so a rebuild corrects the owner of a row written before the - // indexer declared one, rather than leaving the first writer's guess. - pluginId: row.pluginId, - authorIds: row.authorIds, - title: row.title, - content: row.content, - containerType: row.containerType, - containerId: row.containerId, - url: row.url, - isPublic: row.isPublic, - isHidden: row.isHidden, - imageId: row.imageId, - metadata: row.metadata, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - indexedAt: row.indexedAt, - }, - }); + /** + * One multi-row upsert per chunk. A statement may not touch the same row + * twice, so a batch that names one `(itemType, itemId, languageCode)` more + * than once keeps the last copy - what writing them one by one would leave. + */ + private async upsertRows(docs: SearchDocument[]): Promise { + const byKey = new Map>(); + for (const doc of docs) { + const row = toRow(doc); + const key = JSON.stringify([row.itemType, row.itemId, row.languageCode]); + byKey.delete(key); + byKey.set(key, row); + } + const rows = [...byKey.values()]; + + for (let start = 0; start < rows.length; start += BULK_UPSERT_CHUNK) { + await this.c + .get("db") + .insert(core_search_index) + .values(rows.slice(start, start + BULK_UPSERT_CHUNK)) + .onConflictDoUpdate({ + target: [ + core_search_index.itemType, + core_search_index.itemId, + core_search_index.languageCode, + ], + set: { + // Included so a rebuild corrects the owner of a row written before + // the indexer declared one, rather than leaving the first writer's + // guess. + pluginId: sql`excluded."pluginId"`, + authorIds: sql`excluded."authorIds"`, + title: sql`excluded."title"`, + content: sql`excluded."content"`, + containerType: sql`excluded."containerType"`, + containerId: sql`excluded."containerId"`, + url: sql`excluded."url"`, + isPublic: sql`excluded."isPublic"`, + isHidden: sql`excluded."isHidden"`, + imageId: sql`excluded."imageId"`, + metadata: sql`excluded."metadata"`, + createdAt: sql`excluded."createdAt"`, + updatedAt: sql`excluded."updatedAt"`, + indexedAt: sql`excluded."indexedAt"`, + }, + }); + } } private withFallbacks(params: SearchQueryParams): SearchQueryParams { @@ -671,10 +700,7 @@ export class SearchModel { this.resolveOwner({ ...doc, content: stripHtml(doc.content) }), ); - for (const doc of clean) { - await this.upsertRow(doc); - } - + await this.upsertRows(clean); await this.provider().bulkIndex(this.c, clean); } @@ -758,24 +784,55 @@ export class SearchModel { containerType: string; itemType: string; }): Promise { - const rows = await this.c - .get("db") - .selectDistinct({ itemId: core_search_index.itemId }) - .from(core_search_index) + const db = this.c.get("db"); + // Every language of every item filed under the container, in one statement. + const deleted = await db + .delete(core_search_index) .where( and( eq(core_search_index.itemType, itemType), - eq(core_search_index.containerType, containerType), - eq(core_search_index.containerId, containerId), + inArray( + core_search_index.itemId, + db + .select({ itemId: core_search_index.itemId }) + .from(core_search_index) + .where( + and( + eq(core_search_index.itemType, itemType), + eq(core_search_index.containerType, containerType), + eq(core_search_index.containerId, containerId), + ), + ), + ), + ), + ) + .returning({ itemId: core_search_index.itemId }); + + const itemKeys = [...new Set(deleted.map(row => row.itemId))]; + if (itemKeys.length === 0) return; + + await db + .delete(core_search_popularity) + .where( + and( + eq(core_search_popularity.itemType, itemType), + inArray(core_search_popularity.itemId, itemKeys), ), ); const contentTypes = this.c.get("core")?.contentTypes ?? []; - for (const { itemId } of rows) { - await this.delete( - itemType, - searchItemIdFromKey(contentTypes, itemType, itemId), - ); + const itemIds = itemKeys.map(key => + searchItemIdFromKey(contentTypes, itemType, key), + ); + const provider = this.provider(); + if (provider.deleteMany) { + await provider.deleteMany(this.c, itemType, itemIds); + + return; + } + + for (const itemId of itemIds) { + await provider.delete(this.c, itemType, itemId); } } @@ -788,7 +845,7 @@ export class SearchModel { const commentParent = await this.newCommentParent(clean); - await this.upsertRow(clean); + await this.upsertRows([clean]); await this.provider().index(this.c, clean); if (commentParent) { diff --git a/packages/vitnode/src/api/modules/admin/debug/routes/search-status.route.ts b/packages/vitnode/src/api/modules/admin/debug/routes/search-status.route.ts index ffb397a87e..4366d5d124 100644 --- a/packages/vitnode/src/api/modules/admin/debug/routes/search-status.route.ts +++ b/packages/vitnode/src/api/modules/admin/debug/routes/search-status.route.ts @@ -83,7 +83,7 @@ export const searchStatusDebugAdminRoute = buildRoute({ // `pluginId` comes along so a collection whose indexer is gone can still name // its owner. An item type has one owner, so the aggregate is a formality - // `max` picks deterministically if rows ever disagree mid-rebuild. - const indexedByType = await db + const indexedByTypeQuery = db .select({ itemType: core_search_index.itemType, documents: count(), @@ -94,13 +94,11 @@ export const searchStatusDebugAdminRoute = buildRoute({ .from(core_search_index) .groupBy(core_search_index.itemType); - const statsByType = new Map(indexedByType.map(row => [row.itemType, row])); - // Per language, so a rebuild that stopped halfway through one locale is // visible as that locale rather than as a slightly-low total. The // language-agnostic rows (`""`) are dropped: they are not a language, and // listing them as one would put an unnamed row in every collection. - const byLanguage = await db + const byLanguageQuery = db .select({ itemType: core_search_index.itemType, documents: count(), @@ -112,24 +110,10 @@ export const searchStatusDebugAdminRoute = buildRoute({ .groupBy(core_search_index.itemType, core_search_index.languageCode) .orderBy(desc(count())); - const languagesByType = new Map< - string, - { documents: number; languageCode: string; lastIndexedAt: Date | null }[] - >(); - for (const row of byLanguage) { - const entries = languagesByType.get(row.itemType) ?? []; - entries.push({ - documents: row.documents, - languageCode: row.languageCode, - lastIndexedAt: row.lastIndexedAt, - }); - languagesByType.set(row.itemType, entries); - } - // Newest first, and bounded: this is a "what went wrong lately" panel, not a // log viewer. `LIKE 'prefix%'` needs no escaping - the prefix contains // neither `%` nor `_`. - const syncErrors = await db + const syncErrorsQuery = db .select({ id: core_logs.id, pluginId: core_logs.pluginId, @@ -146,6 +130,28 @@ export const searchStatusDebugAdminRoute = buildRoute({ .orderBy(desc(core_logs.id)) .limit(SYNC_ERROR_LIMIT); + const [indexedByType, byLanguage, syncErrors] = await Promise.all([ + indexedByTypeQuery, + byLanguageQuery, + syncErrorsQuery, + ]); + + const statsByType = new Map(indexedByType.map(row => [row.itemType, row])); + + const languagesByType = new Map< + string, + { documents: number; languageCode: string; lastIndexedAt: Date | null }[] + >(); + for (const row of byLanguage) { + const entries = languagesByType.get(row.itemType) ?? []; + entries.push({ + documents: row.documents, + languageCode: row.languageCode, + lastIndexedAt: row.lastIndexedAt, + }); + languagesByType.set(row.itemType, entries); + } + // Start from every registered indexer so a collection with nothing indexed // yet still appears; then fold in any indexed type without a live indexer. const itemTypes = [ diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts index 0778dc1588..4f7ca69e57 100644 --- a/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts @@ -150,69 +150,65 @@ export const listRolesAdminRoute = buildRoute({ }); const roleIds = data.edges.map(role => role.id); - const names = roleIds.length - ? await c - .get("db") - .select({ - itemId: core_languages_words.itemId, - languageCode: core_languages_words.languageCode, - value: core_languages_words.value, - }) - .from(core_languages_words) - .where( - and( - eq(core_languages_words.tableName, "core_roles"), - eq(core_languages_words.variable, "name"), - eq(core_languages_words.pluginCode, "core"), - inArray(core_languages_words.itemId, roleIds), - ), - ) - : []; - const primaryCounts = roleIds.length - ? await c - .get("db") - .select({ - roleId: core_users.roleId, - total: count(), - }) - .from(core_users) - .where(inArray(core_users.roleId, roleIds)) - .groupBy(core_users.roleId) - : []; - const secondaryCounts = roleIds.length - ? await c - .get("db") - .select({ - roleId: core_users_secondary_roles.roleId, - total: count(), - }) - .from(core_users_secondary_roles) - .innerJoin( - core_users, - eq(core_users.id, core_users_secondary_roles.userId), - ) - .where( - and( - inArray(core_users_secondary_roles.roleId, roleIds), - ne(core_users.roleId, core_users_secondary_roles.roleId), - ), - ) - .groupBy(core_users_secondary_roles.roleId) - : []; - const [adminEntryRoles, moderatorEntryRoles] = roleIds.length + const db = c.get("db"); + const [ + names, + primaryCounts, + secondaryCounts, + adminEntryRoles, + moderatorEntryRoles, + ] = roleIds.length ? await Promise.all([ - c - .get("db") + db + .select({ + itemId: core_languages_words.itemId, + languageCode: core_languages_words.languageCode, + value: core_languages_words.value, + }) + .from(core_languages_words) + .where( + and( + eq(core_languages_words.tableName, "core_roles"), + eq(core_languages_words.variable, "name"), + eq(core_languages_words.pluginCode, "core"), + inArray(core_languages_words.itemId, roleIds), + ), + ), + db + .select({ + roleId: core_users.roleId, + total: count(), + }) + .from(core_users) + .where(inArray(core_users.roleId, roleIds)) + .groupBy(core_users.roleId), + db + .select({ + roleId: core_users_secondary_roles.roleId, + total: count(), + }) + .from(core_users_secondary_roles) + .innerJoin( + core_users, + eq(core_users.id, core_users_secondary_roles.userId), + ) + .where( + and( + inArray(core_users_secondary_roles.roleId, roleIds), + ne(core_users.roleId, core_users_secondary_roles.roleId), + ), + ) + .groupBy(core_users_secondary_roles.roleId), + db .selectDistinct({ roleId: core_admin_permissions.roleId }) .from(core_admin_permissions) .where(inArray(core_admin_permissions.roleId, roleIds)), - c - .get("db") + db .selectDistinct({ roleId: core_moderators_permissions.roleId }) .from(core_moderators_permissions) .where(inArray(core_moderators_permissions.roleId, roleIds)), ]) - : [[], []]; + : [[], [], [], [], []]; const adminRoleIds = new Set([ ...data.edges.filter(role => role.root).map(role => role.id), ...adminEntryRoles.map(row => row.roleId), diff --git a/packages/vitnode/src/api/modules/admin/staff/lib/resolve-staff-edges.ts b/packages/vitnode/src/api/modules/admin/staff/lib/resolve-staff-edges.ts index 2323f70bf0..890748c778 100644 --- a/packages/vitnode/src/api/modules/admin/staff/lib/resolve-staff-edges.ts +++ b/packages/vitnode/src/api/modules/admin/staff/lib/resolve-staff-edges.ts @@ -19,9 +19,6 @@ interface RawStaffEdge { export const resolveStaffEdges = async (c: Context, edges: RawStaffEdge[]) => { const currentUser = c.get("admin")?.user; - const currentUserRoleIds = currentUser - ? new Set(await getUserRoleIds(c, currentUser)) - : new Set(); const entryRoleIds = edges .map(edge => edge.roleId) @@ -30,34 +27,43 @@ export const resolveStaffEdges = async (c: Context, edges: RawStaffEdge[]) => { .map(edge => edge.userId) .filter((id): id is number => id != null); - const entryRoles = entryRoleIds.length - ? await c - .get("db") - .select({ - id: core_roles.id, - color: core_roles.color, - prefix: core_roles.prefix, - }) - .from(core_roles) - .where(inArray(core_roles.id, entryRoleIds)) - : []; + const loadEntryRoles = async () => + entryRoleIds.length + ? await c + .get("db") + .select({ + id: core_roles.id, + color: core_roles.color, + prefix: core_roles.prefix, + }) + .from(core_roles) + .where(inArray(core_roles.id, entryRoleIds)) + : []; - const users = userIds.length - ? await c - .get("db") - .select({ - id: core_users.id, - name: core_users.name, - nameCode: core_users.nameCode, - avatarColor: core_users.avatarColor, - roleId: core_users.roleId, - roleColor: core_roles.color, - rolePrefix: core_roles.prefix, - }) - .from(core_users) - .leftJoin(core_roles, eq(core_roles.id, core_users.roleId)) - .where(inArray(core_users.id, userIds)) - : []; + const loadUsers = async () => + userIds.length + ? await c + .get("db") + .select({ + id: core_users.id, + name: core_users.name, + nameCode: core_users.nameCode, + avatarColor: core_users.avatarColor, + roleId: core_users.roleId, + roleColor: core_roles.color, + rolePrefix: core_roles.prefix, + }) + .from(core_users) + .leftJoin(core_roles, eq(core_roles.id, core_users.roleId)) + .where(inArray(core_users.id, userIds)) + : []; + + const [currentUserRoleIdList, entryRoles, users] = await Promise.all([ + currentUser ? getUserRoleIds(c, currentUser) : [], + loadEntryRoles(), + loadUsers(), + ]); + const currentUserRoleIds = new Set(currentUserRoleIdList); const roleNames = await resolveRoleNames(c, [ ...entryRoleIds, diff --git a/packages/vitnode/src/api/modules/ai/cron/alt.cron.ts b/packages/vitnode/src/api/modules/ai/cron/alt.cron.ts index 7d89dd3e4c..d0b2a7c10f 100644 --- a/packages/vitnode/src/api/modules/ai/cron/alt.cron.ts +++ b/packages/vitnode/src/api/modules/ai/cron/alt.cron.ts @@ -18,7 +18,11 @@ export const aiQueueCron = buildCron({ schedule: "* * * * *", handler: async c => { await processQueueTasks(c, { + // Paid model calls: two tasks a minute, one at a time, rather than + // draining the queue like the default worker does. batchSize: 2, + concurrency: 1, + maxBatches: 1, lockKey: "queue:process:ai", queues: [ALT_QUEUE], }); diff --git a/packages/vitnode/src/api/modules/content/content.module.ts b/packages/vitnode/src/api/modules/content/content.module.ts index 72ff467cb7..2618b00e53 100644 --- a/packages/vitnode/src/api/modules/content/content.module.ts +++ b/packages/vitnode/src/api/modules/content/content.module.ts @@ -4,11 +4,16 @@ import { contentLiveWebSocket } from "@/content/server/live/socket"; import { contentEditorialCleanupCron } from "./cron/content-editorial-cleanup.cron"; import { contentScheduleEffectsQueueTask } from "./tasks/content-schedule-effects.task"; import { contentScheduleQueueTask } from "./tasks/content-schedule.task"; +import { contentSearchDependentsQueueTask } from "./tasks/content-search-dependents.task"; export const contentModule = buildModule({ name: "content", routes: [], cronJobs: [contentEditorialCleanupCron], - queueTasks: [contentScheduleQueueTask, contentScheduleEffectsQueueTask], + queueTasks: [ + contentScheduleQueueTask, + contentScheduleEffectsQueueTask, + contentSearchDependentsQueueTask, + ], webSockets: [contentLiveWebSocket], }); diff --git a/packages/vitnode/src/api/modules/content/tasks/content-search-dependents.task.ts b/packages/vitnode/src/api/modules/content/tasks/content-search-dependents.task.ts new file mode 100644 index 0000000000..b437945759 --- /dev/null +++ b/packages/vitnode/src/api/modules/content/tasks/content-search-dependents.task.ts @@ -0,0 +1,19 @@ +import { buildQueueTask } from "@/api/lib/queue"; +import { + CONTENT_QUEUE_TASK_SEARCH_DEPENDENTS, + contentSearchDependentsPayloadSchema, + runContentSearchDependentsTask, +} from "@/content/server/search-sync"; + +export const contentSearchDependentsQueueTask = buildQueueTask({ + name: CONTENT_QUEUE_TASK_SEARCH_DEPENDENTS, + description: + "Reindex the rest of a record's dependents - comments carrying its title or URL - after an edit whose request wrote only the first page.", + maxAttempts: 5, + handler: async (c, payload) => { + await runContentSearchDependentsTask( + c, + contentSearchDependentsPayloadSchema.parse(payload), + ); + }, +}); diff --git a/packages/vitnode/src/api/modules/cron/cron/clean.cron.ts b/packages/vitnode/src/api/modules/cron/cron/clean.cron.ts index d42e93bfff..b378387c25 100644 --- a/packages/vitnode/src/api/modules/cron/cron/clean.cron.ts +++ b/packages/vitnode/src/api/modules/cron/cron/clean.cron.ts @@ -5,6 +5,7 @@ import { eq, lt, notExists, sql } from "drizzle-orm"; import type { EnvVitNode } from "@/api/middlewares/global.middleware"; import { buildCron } from "@/api/lib/cron"; +import { deleteExpiredQueueTasks } from "@/api/modules/queue/helpers/process-queue-tasks"; import { core_admin_sessions } from "@/database/admins"; import { core_sessions, @@ -36,7 +37,7 @@ export const isOrphanedDevice = ( export const cleanCron = buildCron({ name: "clean", - description: "Clean up expired sessions and tokens", + description: "Clean up expired sessions, tokens and finished queue tasks", // Run every 1 hour schedule: "0 * * * *", handler: async c => { @@ -61,5 +62,8 @@ export const cleanCron = buildCron({ .delete(core_users_confirm_emails) .where(lt(core_users_confirm_emails.expiresAt, new Date())); }); + + // Outside the transaction: each batch commits on its own. + await deleteExpiredQueueTasks(c); }, }); diff --git a/packages/vitnode/src/api/modules/cron/routes/cron.route.ts b/packages/vitnode/src/api/modules/cron/routes/cron.route.ts index e0b6c95276..5e70674ffc 100644 --- a/packages/vitnode/src/api/modules/cron/routes/cron.route.ts +++ b/packages/vitnode/src/api/modules/cron/routes/cron.route.ts @@ -1,4 +1,4 @@ -import { eq } from "drizzle-orm"; +import { and, eq, isNull } from "drizzle-orm"; import { z } from "zod"; import { buildRoute } from "@/api/builders"; @@ -48,6 +48,13 @@ export const runCronRoute = buildRoute({ cronFromDb, ); + const cronFromDbMap = new Map( + cronFromDb.map(dbJob => [ + `${dbJob.pluginId}:${dbJob.module}:${dbJob.name}`, + dbJob, + ]), + ); + if (newJobs.length > 0) { try { const newJobsValues = newJobs.map(job => ({ @@ -60,7 +67,16 @@ export const runCronRoute = buildRoute({ schedule: job.schedule, })); - await db.insert(core_cron).values(newJobsValues); + const inserted = await db + .insert(core_cron) + .values(newJobsValues) + .returning(); + for (const dbJob of inserted) { + cronFromDbMap.set( + `${dbJob.pluginId}:${dbJob.module}:${dbJob.name}`, + dbJob, + ); + } } catch (error) { await c.get("log").error(`Error inserting new cron jobs: ${error}`); } @@ -73,30 +89,36 @@ export const runCronRoute = buildRoute({ } if (jobsToExecute.length > 0) { - const cronFromDbMap = new Map( - cronFromDb.map(dbJob => [ - `${dbJob.pluginId}:${dbJob.module}:${dbJob.name}`, - dbJob, - ]), - ); - const executionPromises = jobsToExecute.map(async job => { + const dbJob = cronFromDbMap.get( + `${job.pluginId}:${job.module}:${job.name}`, + ); + + // Every instance's ticker hits this route each minute. Claim the run + // first (compare-and-set on the `lastRun` we read) so exactly one + // instance executes a due job; the others see 0 rows and skip it. + if (dbJob) { + const [claimed] = await db + .update(core_cron) + .set({ + lastRun: now, + nextRun: getNextCronRunDate(job.schedule, now), + }) + .where( + and( + eq(core_cron.id, dbJob.id), + dbJob.lastRun + ? eq(core_cron.lastRun, dbJob.lastRun) + : isNull(core_cron.lastRun), + ), + ) + .returning({ id: core_cron.id }); + if (!claimed) return { success: false, jobName: job.name }; + } + try { await job.handler(c); - const jobKey = `${job.pluginId}:${job.module}:${job.name}`; - const dbJob = cronFromDbMap.get(jobKey); - - if (dbJob) { - await db - .update(core_cron) - .set({ - lastRun: now, - nextRun: getNextCronRunDate(job.schedule, now), - }) - .where(eq(core_cron.id, dbJob.id)); - } - return { success: true, jobName: job.name }; } catch (error) { await c @@ -105,6 +127,17 @@ export const runCronRoute = buildRoute({ `Error executing cron job "${job.pluginId}:${job.module}:${job.name}": ${error}`, ); + // Hand the claim back so a failed job is retried on the next tick. + if (dbJob) { + await db + .update(core_cron) + .set({ lastRun: dbJob.lastRun, nextRun: dbJob.nextRun }) + .where( + and(eq(core_cron.id, dbJob.id), eq(core_cron.lastRun, now)), + ) + .catch(() => undefined); + } + return { success: false, jobName: job.name, error }; } }); diff --git a/packages/vitnode/src/api/modules/moderation/routes/approvals.routes.ts b/packages/vitnode/src/api/modules/moderation/routes/approvals.routes.ts index ff40165afa..c8bbf778ca 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/approvals.routes.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/approvals.routes.ts @@ -202,7 +202,17 @@ export const myApprovalsRoute = buildRoute({ path: "/approvals/mine", description: "The signed-in member's own submissions and what happened to them", - request: { query: z.object(pageQuery) }, + request: { + query: z.object({ + ...pageQuery, + pluginId: z + .string() + .min(1) + .max(255) + .optional() + .openapi({ example: "@vitnode/forum" }), + }), + }, responses: { 200: json(pageSchema, "One page of the member's submissions"), 401: { description: "Sign in first" }, diff --git a/packages/vitnode/src/api/modules/moderation/routes/list.route.ts b/packages/vitnode/src/api/modules/moderation/routes/list.route.ts index b0db5a1729..5d5294215d 100644 --- a/packages/vitnode/src/api/modules/moderation/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/moderation/routes/list.route.ts @@ -119,30 +119,32 @@ export const listReportsRoute = buildRoute({ .offset((page - 1) * pageSize), ]); - const related = rows.length - ? await c - .get("db") - .select({ - pluginId: table.pluginId, - targetId: table.targetId, - targetType: table.targetType, - value: count(), - }) - .from(table) - .where( - and( - viewableReportsCondition(c, moderator.set), - reportTargetTriplets(rows), - ), - ) - .groupBy(table.pluginId, table.targetType, table.targetId) - : []; + const [related, people] = await Promise.all([ + rows.length + ? c + .get("db") + .select({ + pluginId: table.pluginId, + targetId: table.targetId, + targetType: table.targetType, + value: count(), + }) + .from(table) + .where( + and( + viewableReportsCondition(c, moderator.set), + reportTargetTriplets(rows), + ), + ) + .groupBy(table.pluginId, table.targetType, table.targetId) + : [], + loadReportPeople( + c, + rows.map(row => row.assigneeId), + ), + ]); const registry = c.get("core").moderation; - const people = await loadReportPeople( - c, - rows.map(row => row.assigneeId), - ); return c.json( { diff --git a/packages/vitnode/src/api/modules/queue/helpers/process-queue-tasks.ts b/packages/vitnode/src/api/modules/queue/helpers/process-queue-tasks.ts index ba1db33faa..04980d53fe 100644 --- a/packages/vitnode/src/api/modules/queue/helpers/process-queue-tasks.ts +++ b/packages/vitnode/src/api/modules/queue/helpers/process-queue-tasks.ts @@ -13,6 +13,7 @@ import { sql, } from "drizzle-orm"; +import type { QueueTaskConfig } from "@/api/lib/queue"; import type { EnvVitNode } from "@/api/middlewares/global.middleware"; import { QueueDeferError } from "@/api/lib/queue"; @@ -20,8 +21,10 @@ import { core_queue } from "@/database/queue"; import { resolveQueueTaskOutcome } from "@/lib/api/resolve-queue-task-outcome"; const QUEUE_BATCH_SIZE = 25; +const QUEUE_CONCURRENCY = 5; const QUEUE_LOCK_KEY = "queue:process"; const QUEUE_LOCK_TTL_SECONDS = 55; +const QUEUE_DRAIN_BUDGET_MS = 45_000; const QUEUE_RETENTION_DAYS = 7; const DEFAULT_QUEUE_LEASE_SECONDS = 600; @@ -29,7 +32,11 @@ const DEDICATED_QUEUES = ["ai"] as const; interface ProcessQueueOptions { batchSize?: number; + /** How many tasks of one batch run at the same time. */ + concurrency?: number; lockKey?: string; + /** Stop after this many batches even when more tasks are waiting. */ + maxBatches?: number; queues?: string[]; } @@ -136,64 +143,122 @@ const claimQueueTasks = async ( .returning(); }); +interface QueueTaskResult { + availableAt: Date; + completedAt: Date | null; + deferred: boolean; + id: number; + lastError: null | string; + status: QueueRow["status"]; +} + +const runQueueTask = async ( + c: Context, + task: QueueRow, + handlerMap: Map, +): Promise => { + const key = `${task.pluginId}:${task.name}`; + const registered = handlerMap.get(key); + let error: null | string = null; + + if (!registered) { + error = `No handler registered for queue task "${key}"`; + await c.get("log").warn(error); + } else { + try { + await registered.handler(c, task.payload); + } catch (err) { + if (err instanceof QueueDeferError) { + return { + availableAt: err.until, + completedAt: null, + deferred: true, + id: task.id, + lastError: err.message, + status: "pending", + }; + } + error = err instanceof Error ? err.message : String(err); + await c.get("log").error(`Queue task "${key}" failed: ${error}`); + } + } + + const outcome = resolveQueueTaskOutcome({ + attempts: task.attempts, + maxAttempts: task.maxAttempts, + error, + }); + + return { + availableAt: outcome.availableAt ?? task.availableAt, + completedAt: outcome.completedAt ?? null, + deferred: false, + id: task.id, + lastError: outcome.lastError, + status: outcome.status, + }; +}; + +const timestampParam = (value: Date | null): SQL => + value ? sql`${value.toISOString()}::timestamp` : sql`NULL::timestamp`; + +// One statement for the whole batch. A deferred task gives back the attempt +// its claim took. +const saveQueueTaskResults = async ( + c: Context, + results: QueueTaskResult[], +): Promise => { + if (results.length === 0) return; + + await c.get("db").execute(sql` + UPDATE ${core_queue} AS q + SET + "status" = v.status, + "lastError" = v."lastError", + "availableAt" = v."availableAt", + "completedAt" = v."completedAt", + "reservedAt" = NULL, + "attempts" = CASE WHEN v.deferred THEN GREATEST(q."attempts" - 1, 0) ELSE q."attempts" END, + "updatedAt" = ${timestampParam(new Date())} + FROM (VALUES ${sql.join( + results.map( + result => + sql`(${result.id}::integer, ${result.status}::varchar, ${result.lastError}::text, ${timestampParam(result.availableAt)}, ${timestampParam(result.completedAt)}, ${result.deferred}::boolean)`, + ), + sql`, `, + )}) AS v(id, status, "lastError", "availableAt", "completedAt", deferred) + WHERE q."id" = v.id + `); +}; + const runClaimedQueueTasks = async ( c: Context, claimed: QueueRow[], + concurrency = QUEUE_CONCURRENCY, ): Promise => { if (claimed.length === 0) return; - const db = c.get("db"); const handlerMap = new Map( c.get("core").queue.map(task => [`${task.pluginId}:${task.name}`, task]), ); - - for (const task of claimed) { - const key = `${task.pluginId}:${task.name}`; - const registered = handlerMap.get(key); - let error: null | string = null; - - if (!registered) { - error = `No handler registered for queue task "${key}"`; - await c.get("log").warn(error); - } else { - try { - await registered.handler(c, task.payload); - } catch (err) { - if (err instanceof QueueDeferError) { - await db - .update(core_queue) - .set({ - attempts: sql`GREATEST(${core_queue.attempts} - 1, 0)`, - availableAt: err.until, - lastError: err.message, - reservedAt: null, - status: "pending", - }) - .where(eq(core_queue.id, task.id)); - continue; - } - error = err instanceof Error ? err.message : String(err); - await c.get("log").error(`Queue task "${key}" failed: ${error}`); - } + const results: QueueTaskResult[] = []; + let next = 0; + // `runQueueTask` never rejects for a handler failure, so a plain worker pool + // is enough: each worker pulls the next task until the batch is drained. + const worker = async () => { + while (next < claimed.length) { + const task = claimed[next++]; + results.push(await runQueueTask(c, task, handlerMap)); } + }; - const outcome = resolveQueueTaskOutcome({ - attempts: task.attempts, - maxAttempts: task.maxAttempts, - error, - }); - - await db - .update(core_queue) - .set({ - status: outcome.status, - lastError: outcome.lastError, - availableAt: outcome.availableAt ?? task.availableAt, - completedAt: outcome.completedAt ?? null, - reservedAt: null, - }) - .where(eq(core_queue.id, task.id)); - } + await Promise.all( + Array.from( + { length: Math.max(1, Math.min(concurrency, claimed.length)) }, + worker, + ), + ); + await saveQueueTaskResults(c, results); }; export const processQueueTasksByIds = async ( @@ -214,7 +279,9 @@ export const processQueueTasks = async ( c: Context, { batchSize = QUEUE_BATCH_SIZE, + concurrency = QUEUE_CONCURRENCY, lockKey = QUEUE_LOCK_KEY, + maxBatches = Number.POSITIVE_INFINITY, queues, }: ProcessQueueOptions = {}, ): Promise => { @@ -222,33 +289,77 @@ export const processQueueTasks = async ( .get("cache") .acquireLock(lockKey, QUEUE_LOCK_TTL_SECONDS); if (!gotLock) return; + const lockedAt = Date.now(); try { - const db = c.get("db"); - const now = new Date(); - - await recoverAbandonedQueueTasks(c, now); + await recoverAbandonedQueueTasks(c, new Date()); const queueFilter = queues ? inArray(core_queue.queue, queues) : notInArray(core_queue.queue, [...DEDICATED_QUEUES]); - await runClaimedQueueTasks( - c, - await claimQueueTasks(c, queueFilter, now, batchSize), - ); - - const cutoff = new Date( - now.getTime() - QUEUE_RETENTION_DAYS * 24 * 60 * 60 * 1000, - ); - await db + + // Keep claiming until the queue is empty, but never start a batch after + // the drain budget: that leaves the rest of the lock TTL for the batch in + // flight, so the lock is still ours whenever we claim. A batch that runs + // past the TTL is still safe - claims use `FOR UPDATE SKIP LOCKED`. + for ( + let batch = 0; + batch < maxBatches && Date.now() - lockedAt < QUEUE_DRAIN_BUDGET_MS; + batch++ + ) { + const claimed = await claimQueueTasks( + c, + queueFilter, + new Date(), + batchSize, + ); + if (claimed.length === 0) break; + + await runClaimedQueueTasks(c, claimed, concurrency); + } + } finally { + // Past the TTL the key may already belong to another instance's run. + if (Date.now() - lockedAt < QUEUE_LOCK_TTL_SECONDS * 1000) { + await c.get("cache").releaseLock(lockKey); + } + } +}; + +const QUEUE_RETENTION_BATCH_SIZE = 1000; +const QUEUE_RETENTION_MAX_BATCHES = 100; + +/** + * Remove finished tasks older than the retention window, a bounded batch at a + * time so one hourly run never holds a long lock on a large queue table. + */ +export const deleteExpiredQueueTasks = async ( + c: Context, +): Promise => { + const db = c.get("db"); + const cutoff = new Date( + Date.now() - QUEUE_RETENTION_DAYS * 24 * 60 * 60 * 1000, + ); + + for (let batch = 0; batch < QUEUE_RETENTION_MAX_BATCHES; batch++) { + const rows = await db .delete(core_queue) .where( - and( - inArray(core_queue.status, ["completed", "failed"]), - lt(core_queue.completedAt, cutoff), + inArray( + core_queue.id, + db + .select({ id: core_queue.id }) + .from(core_queue) + .where( + and( + inArray(core_queue.status, ["completed", "failed"]), + lt(core_queue.completedAt, cutoff), + ), + ) + .limit(QUEUE_RETENTION_BATCH_SIZE), ), - ); - } finally { - await c.get("cache").releaseLock(lockKey); + ) + .returning({ id: core_queue.id }); + + if (rows.length < QUEUE_RETENTION_BATCH_SIZE) break; } }; diff --git a/packages/vitnode/src/components/date-format.tsx b/packages/vitnode/src/components/date-format.tsx index 76b8996309..497707f4f7 100644 --- a/packages/vitnode/src/components/date-format.tsx +++ b/packages/vitnode/src/components/date-format.tsx @@ -37,7 +37,7 @@ export const DateFormat = ({ if (now.getTime() - dateToFormat.getTime() < 604800000) { return ( - + {format.relativeTime(dateToFormat, now)} diff --git a/packages/vitnode/src/components/form/auto-form.tsx b/packages/vitnode/src/components/form/auto-form.tsx index b66e2b0cc3..08440bafc8 100644 --- a/packages/vitnode/src/components/form/auto-form.tsx +++ b/packages/vitnode/src/components/form/auto-form.tsx @@ -6,8 +6,7 @@ import { useForm, useSelector, } from "@tanstack/react-form"; -import { animate, useReducedMotion } from "motion/react"; -import { useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { useTranslations } from "use-intl"; import z from "zod"; @@ -22,7 +21,6 @@ import { type InputParams, isRequiredPath, } from "../../lib/helpers/auto-form"; -import { SHAKE_KEYFRAMES, SHAKE_TRANSITION } from "../../lib/motion"; import { Button } from "../ui/button"; import { DialogClose, DialogFooter, useDialog } from "../ui/dialog"; import { Field } from "../ui/field"; @@ -118,6 +116,21 @@ const usesFormValues = >( typeof item.hidden === "function" || (item.children?.some(child => usesFormValues(child)) ?? false); +// Joined into a string so the form re-renders only when visibility flips, +// not on every keystroke. +const hiddenIdsOf = >( + items: ItemAutoFormProps[], + values: z.input, +): string => + items + .flatMap(item => { + if (item.hidden?.(values)) return [item.id]; + const nested = item.children ? hiddenIdsOf(item.children, values) : ""; + + return nested ? [nested] : []; + }) + .join("\n"); + export const AutoFormFieldSlot = ({ component, ...props @@ -139,6 +152,17 @@ const focusTargetOf = (fieldElement: HTMLElement): HTMLElement | null => `[aria-invalid=true]:is(${FOCUSABLE_SELECTOR})`, ) ?? fieldElement.querySelector(FOCUSABLE_SELECTOR); +// Mirrors SHAKE_KEYFRAMES in lib/motion, run through WAAPI so the form does +// not pull motion's animation engine into every bundle that renders it. +const SHAKE_FRAMES: Keyframe[] = [0, -8, 8, -6, 6, -3, 3, 0].map(x => ({ + easing: "ease-in-out", + transform: `translateX(${x}px)`, +})); +const SHAKE_DURATION_MS = 400; + +const prefersReducedMotion = () => + window.matchMedia("(prefers-reduced-motion: reduce)").matches; + const belongsToField = (fieldName: string, itemId: string) => fieldName === itemId || fieldName.startsWith(`${itemId}.`) || @@ -188,34 +212,29 @@ export type AutoFormOnSubmit> = ( }, ) => Promise | void; -const useRevealInvalidFields = ( - formRef: React.RefObject, +const revealInvalidFields = ( + root: HTMLFormElement | null, onRevealTab: (tab: string) => void, ) => { - const shouldReduceMotion = useReducedMotion(); + if (!root) return; - return () => { - const root = formRef.current; - if (!root) return; + const invalidFields = outermostInvalidFields(root); + const [firstInvalidField] = invalidFields; + if (!firstInvalidField) return; - const invalidFields = outermostInvalidFields(root); - const [firstInvalidField] = invalidFields; - if (!firstInvalidField) return; + const tabOfField = firstInvalidField.closest( + "[data-autoform-tab]", + )?.dataset.autoformTab; + if (tabOfField) onRevealTab(tabOfField); - const tabOfField = firstInvalidField.closest( - "[data-autoform-tab]", - )?.dataset.autoformTab; - if (tabOfField) onRevealTab(tabOfField); - - requestAnimationFrame(() => { - focusTargetOf(firstInvalidField)?.focus(); - }); + requestAnimationFrame(() => { + focusTargetOf(firstInvalidField)?.focus(); + }); - if (shouldReduceMotion) return; - for (const field of invalidFields) { - void animate(field, SHAKE_KEYFRAMES, SHAKE_TRANSITION); - } - }; + if (prefersReducedMotion()) return; + for (const field of invalidFields) { + field.animate(SHAKE_FRAMES, SHAKE_DURATION_MS); + } }; const invalidTabsOf = >({ @@ -316,14 +335,20 @@ export function AutoForm>({ const t = useTranslations("core.global"); const formRef = useRef(null); const [activeTab, setActiveTab] = useState(tabs?.[0]?.value); - const revealInvalidFields = useRevealInvalidFields(formRef, setActiveTab); - const jsonSchema: z.core.JSONSchema.JSONSchema = z.toJSONSchema(formSchema); - const inputParams = getZodInputParams(jsonSchema); + const { defaultValues, inputParams, jsonSchema } = useMemo(() => { + const schema: z.core.JSONSchema.JSONSchema = z.toJSONSchema(formSchema); + + return { + defaultValues: getDefaults(schema), + inputParams: getZodInputParams(schema), + jsonSchema: schema, + }; + }, [formSchema]); const validator: AutoFormValidator = formSchema; const form = useForm({ canSubmitWhenInvalid: canSubmitWhenInvalid ?? true, - defaultValues: getDefaults(jsonSchema), + defaultValues, onSubmit: async ({ formApi, meta, value }) => { const parsedValues = formSchema.safeParse(value); if (!parsedValues.success) return; @@ -338,7 +363,9 @@ export function AutoForm>({ } }, onSubmitInvalid: () => { - requestAnimationFrame(revealInvalidFields); + requestAnimationFrame(() => { + revealInvalidFields(formRef.current, setActiveTab); + }); }, onSubmitMeta: emptySubmitMeta, validators: { @@ -349,9 +376,10 @@ export function AutoForm>({ }); const hasConditionalFields = fields.some(item => usesFormValues(item)); - const watchedValues = useSelector(form.store, state => - hasConditionalFields ? state.values : undefined, + const hiddenIds = useSelector(form.store, state => + hasConditionalFields ? hiddenIdsOf(fields, state.values) : "", ); + const hiddenIdSet = new Set(hiddenIds ? hiddenIds.split("\n") : []); const formMode = mode ?? "onTouched"; const invalidTabs = useSelector(form.store, state => invalidTabsOf({ fields, formMode, state, tabs }), @@ -359,11 +387,8 @@ export function AutoForm>({ const invalidTabValues = new Set(invalidTabs.split("\n")); const isSubmitting = useSelector(form.store, state => state.isSubmitting); - const isFieldVisible = (item: ItemAutoFormProps) => { - if (!item.hidden || !watchedValues) return true; - - return !item.hidden(watchedValues); - }; + const isFieldVisible = (item: ItemAutoFormProps) => + !hiddenIdSet.has(item.id); const renderField = (item: ItemAutoFormProps) => { const params = getNestedParam(inputParams, item.id); diff --git a/packages/vitnode/src/components/image-crop/image-crop-fields.tsx b/packages/vitnode/src/components/image-crop/image-crop-fields.tsx index 8c34fd6434..f97c6fc738 100644 --- a/packages/vitnode/src/components/image-crop/image-crop-fields.tsx +++ b/packages/vitnode/src/components/image-crop/image-crop-fields.tsx @@ -17,6 +17,7 @@ import type { LoadedSource } from "./crop-render"; import type { ImageCrop } from "./use-image-crop"; import { + clampOffset, clampZoom, displayedSize, MAX_ZOOM, @@ -63,6 +64,9 @@ const useFrameSize = ( return size; }; +const imageTransform = (offset: Offset) => + `translate(calc(-50% + ${offset.x}px), calc(-50% + ${offset.y}px))`; + const CIRCLE_MASK_PATH = "M0 0h100v100H0Z M50 0a50 50 0 1 0 0 100 50 50 0 1 0 0-100Z"; @@ -96,7 +100,14 @@ const CropStage = ({ }) => { const t = useTranslations("core.global.image_crop"); const frameRef = React.useRef(null); - const dragRef = React.useRef(null); + const imageRef = React.useRef(null); + // A drag moves the image straight through the DOM and commits the offset + // once on release, so the dialog does not re-render on every pointer move. + const dragRef = React.useRef(null); const frame = useFrameSize(frameRef, (from, to) => { onOffsetChange(current => rescaleOffset(current, from, to), to); @@ -123,6 +134,15 @@ const CropStage = ({ onOffsetChange(current => ({ x: current.x + dx, y: current.y + dy })); }; + const endDrag = (event: React.PointerEvent) => { + const active = dragRef.current; + dragRef.current = null; + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + if (active) onOffsetChange(() => active.latest); + }; + return (
{ event.currentTarget.setPointerCapture(event.pointerId); dragRef.current = { + latest: offset, origin: offset, start: { x: event.clientX, y: event.clientY }, }; @@ -169,16 +191,20 @@ const CropStage = ({ const active = dragRef.current; if (!active) return; - const next = { - x: active.origin.x + event.clientX - active.start.x, - y: active.origin.y + event.clientY - active.start.y, - }; - onOffsetChange(() => next); - }} - onPointerUp={event => { - dragRef.current = null; - event.currentTarget.releasePointerCapture(event.pointerId); + active.latest = clampOffset( + { + x: active.origin.x + event.clientX - active.start.x, + y: active.origin.y + event.clientY - active.start.y, + }, + source.size, + frame, + zoom, + ); + if (imageRef.current) { + imageRef.current.style.transform = imageTransform(active.latest); + } }} + onPointerUp={endDrag} ref={frameRef} role="application" style={{ @@ -191,10 +217,11 @@ const CropStage = ({ alt="" className="pointer-events-none absolute top-1/2 left-1/2 max-w-none" draggable={false} + ref={imageRef} src={source.url} style={{ height: shown.height, - transform: `translate(calc(-50% + ${offset.x}px), calc(-50% + ${offset.y}px))`, + transform: imageTransform(offset), width: shown.width, }} /> diff --git a/packages/vitnode/src/components/reveal-panel.tsx b/packages/vitnode/src/components/reveal-panel.tsx index fa99cb6377..9c69a4bce7 100644 --- a/packages/vitnode/src/components/reveal-panel.tsx +++ b/packages/vitnode/src/components/reveal-panel.tsx @@ -1,5 +1,7 @@ -import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { AnimatePresence, useReducedMotion } from "motion/react"; +import * as m from "motion/react-m"; +import { MotionFeatures } from "@/components/motion-features"; import { REVEAL_EXIT_TRANSITION, REVEAL_HIDDEN, @@ -21,24 +23,26 @@ export const RevealPanel = ({ const shouldReduceMotion = useReducedMotion(); return ( - - {open ? ( - - {children} - - ) : null} - + + + {open ? ( + + {children} + + ) : null} + + ); }; diff --git a/packages/vitnode/src/components/switchers/themes/theme-switcher.tsx b/packages/vitnode/src/components/switchers/themes/theme-switcher.tsx index 582fef9889..a92cc0b953 100644 --- a/packages/vitnode/src/components/switchers/themes/theme-switcher.tsx +++ b/packages/vitnode/src/components/switchers/themes/theme-switcher.tsx @@ -19,8 +19,8 @@ export const ThemeSwitcher = () => { size="icon" variant="ghost" > - - + + ); }; diff --git a/packages/vitnode/src/components/table/content.tsx b/packages/vitnode/src/components/table/content.tsx index 8e4785e3b1..ee3d27d95c 100644 --- a/packages/vitnode/src/components/table/content.tsx +++ b/packages/vitnode/src/components/table/content.tsx @@ -22,13 +22,12 @@ import { GroupHeadingRowDataTable, groupRowsDataTable } from "./group"; import { useDataTableUrl } from "./navigation"; import { PaginationDataTable } from "./pagination"; import { + ReorderFallbackBodyDataTable, ReorderHandleDataTable, ReorderHeaderDataTable, - ReorderProviderDataTable, - SortableRowGroupDataTable, - useRowOrderDataTable, -} from "./reorder"; +} from "./reorder-handle"; import { isTableInStoredOrder } from "./reorder-state"; +import { useRowOrderDataTable } from "./row-order"; import { SearchDataTable } from "./search"; import { BulkActionsDataTable, @@ -125,56 +124,12 @@ const ToolbarDataTable = ({ ); }; -const ReorderableBodyDataTable = ({ - filters, - order, - renderRow, - reorder, - reorderable, - rows, - searchParams, -}: Pick, "filters" | "order"> & { - renderRow: (row: T) => React.ReactNode; - reorder: (activeId: DataTableRowId, overId: DataTableRowId) => void; - reorderable: NonNullable["reorderable"]>; - rows: T[]; - searchParams: URLSearchParams; -}) => { - const isReorderDisabled = - reorderable.disabled === true || - !isTableInStoredOrder(searchParams, { - defaultOrder: { - column: String(order.defaultOrder.column), - order: order.defaultOrder.order, - }, - filterIds: filters?.map(filter => filter.id) ?? [], - }); - const rowLabelOf = (id: DataTableRowId) => { - const row = rows.find(item => item.id === id); - - return row ? reorderable.getRowLabel?.(row) : undefined; - }; - - return ( - row.id)} - labelOf={rowLabelOf} - onMove={reorder} - > - {rows.map((row, index) => ( - - {renderRow(row)} - - ))} - - ); -}; +// dnd-kit is only downloaded by tables that can actually be reordered. +const ReorderableBodyDataTable = React.lazy(async () => + import("./reorder").then(module => ({ + default: module.ReorderableBodyDataTable, + })), +); export function ContentDataTable({ bulkActions, @@ -200,7 +155,8 @@ export function ContentDataTable({ edges, onReorder: reorderable?.onReorder, }); - const rowIds = rows.map(row => row.id); + // Memoized on the rows so the selection context does not change every render. + const rowIds = React.useMemo(() => rows.map(row => row.id), [rows]); const expansion = useRowExpansionDataTable({ defaultExpanded: expandable?.defaultExpanded, rowIds, @@ -252,15 +208,30 @@ export function ContentDataTable({ if (reorderable) { return ( - + + } + > + filter.id) ?? [], + }) + } + onMove={reorder} + rows={rows.map(row => ({ + content: renderRow(row), + id: row.id, + label: reorderable.getRowLabel?.(row), + }))} + /> + ); } diff --git a/packages/vitnode/src/components/table/data-table-content.tsx b/packages/vitnode/src/components/table/data-table-content.tsx index 6d353c486b..d989fb0641 100644 --- a/packages/vitnode/src/components/table/data-table-content.tsx +++ b/packages/vitnode/src/components/table/data-table-content.tsx @@ -5,7 +5,7 @@ import type { ExpandableDataTable } from "./expansion"; import type { FilterDataTable } from "./filters"; import type { GroupByDataTable } from "./group"; import type { PaginationDataTable } from "./pagination"; -import type { ReorderableDataTable } from "./reorder"; +import type { ReorderableDataTable } from "./row-order"; import type { SearchDataTable } from "./search"; import { Skeleton } from "../ui/skeleton"; diff --git a/packages/vitnode/src/components/table/reorder-handle.tsx b/packages/vitnode/src/components/table/reorder-handle.tsx new file mode 100644 index 0000000000..c91fbb4f93 --- /dev/null +++ b/packages/vitnode/src/components/table/reorder-handle.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { useTranslations } from "use-intl"; + +import type { DataTableTMin } from "./data-table-content"; + +export const SortableRowHandleContext = + React.createContext(null); + +export const ROW_GROUP_CLASS = + "[&:last-child>tr:last-child]:border-0 [&:last-child>tr:last-child>td]:border-b-0 [&>tr>td]:border-b"; + +export function ReorderHeaderDataTable() { + const t = useTranslations("core.global.data_table"); + + return {t("reorder_column")}; +} + +export function ReorderHandleDataTable() { + return <>{React.use(SortableRowHandleContext)}; +} + +// Holds the handle's place, so rows keep their height while the drag-and-drop +// chunk loads. +const HANDLE_PLACEHOLDER = ( +