diff --git a/.github/workflows/bump_publish.yml b/.github/workflows/bump_publish.yml index 2c87e3e845..5610029d12 100644 --- a/.github/workflows/bump_publish.yml +++ b/.github/workflows/bump_publish.yml @@ -32,6 +32,11 @@ on: required: false type: boolean +env: + # Every package this workflow publishes. The publish steps and the update + # manifest both read it, so the manifest never names a package npm did not get. + PUBLISHED_PACKAGES: "create-vitnode-app @vitnode/core @vitnode/config @vitnode/blog @vitnode/nodemailer @vitnode/resend @vitnode/node-cron" + jobs: bump-version: name: "Bump Version" @@ -39,6 +44,8 @@ jobs: permissions: contents: write id-token: write + outputs: + sha: ${{ steps.published.outputs.sha }} steps: - name: Checkout @@ -83,21 +90,21 @@ jobs: - name: Publish canary if: ${{ (github.event.inputs.mode == 'bump_and_publish' || github.event.inputs.mode == 'publish') && github.event.inputs.release == 'canary' }} - run: pnpm publish --filter create-vitnode-app --filter @vitnode/core --filter @vitnode/config --filter @vitnode/blog --filter @vitnode/nodemailer --filter @vitnode/resend --filter @vitnode/node-cron --tag canary --no-git-checks --access public + run: pnpm publish $(printf -- "--filter %s " $PUBLISHED_PACKAGES) --tag canary --no-git-checks --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: true - name: Publish release candidate if: ${{ (github.event.inputs.mode == 'bump_and_publish' || github.event.inputs.mode == 'publish') && github.event.inputs.release == 'release-candidate' }} - run: pnpm publish --filter create-vitnode-app --filter @vitnode/core --filter @vitnode/config --filter @vitnode/blog --filter @vitnode/nodemailer --filter @vitnode/resend --filter @vitnode/node-cron --tag rc --no-git-checks --access public + run: pnpm publish $(printf -- "--filter %s " $PUBLISHED_PACKAGES) --tag rc --no-git-checks --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: true - name: Publish stable if: ${{ (github.event.inputs.mode == 'bump_and_publish' || github.event.inputs.mode == 'publish') && github.event.inputs.release == 'stable' }} - run: pnpm publish --filter create-vitnode-app --filter @vitnode/core --filter @vitnode/config --filter @vitnode/blog --filter @vitnode/nodemailer --filter @vitnode/resend --filter @vitnode/node-cron --tag latest --no-git-checks --access public + run: pnpm publish $(printf -- "--filter %s " $PUBLISHED_PACKAGES) --tag latest --no-git-checks --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: true @@ -113,3 +120,48 @@ jobs: run: gh release create ${{ steps.version-bump.outputs.newTag }} --generate-notes env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Remember the published commit + id: published + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + publish-update-manifest: + name: "Publish update manifest" + needs: bump-version + # Only after npm has the packages: a bump-only run publishes nothing, so it + # advertises nothing. + if: ${{ github.event.inputs.mode == 'bump_and_publish' || github.event.inputs.mode == 'publish' }} + runs-on: ubuntu-latest + permissions: + contents: write + # Shared with the security updates workflow: one writer of updates.json at a time. + concurrency: + group: vitnode-update-manifest + cancel-in-progress: false + + steps: + - name: Checkout the published commit + uses: actions/checkout@v7 + with: + ref: ${{ needs.bump-version.outputs.sha }} + + - uses: pnpm/action-setup@v6.1.0 + name: Install pnpm + with: + version: 11.9.0 + + - name: Install Node.js + uses: actions/setup-node@v7.0.0 + with: + node-version: 24 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Add the published versions to updates.json + run: pnpm run release:update-manifest release + env: + RELEASE_TYPE: ${{ github.event.inputs.release }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-security-updates.yml b/.github/workflows/publish-security-updates.yml new file mode 100644 index 0000000000..18dd822a41 --- /dev/null +++ b/.github/workflows/publish-security-updates.yml @@ -0,0 +1,50 @@ +name: Publish security updates + +on: + pull_request: + paths: + - "security/security-updates.json" + push: + branches: + - canary + paths: + - "security/security-updates.json" + workflow_dispatch: + +jobs: + security-updates: + name: "Validate and publish security updates" + runs-on: ubuntu-latest + permissions: + contents: write + # Shared with the release workflow: one writer of updates.json at a time. + concurrency: + group: vitnode-update-manifest + cancel-in-progress: false + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - uses: pnpm/action-setup@v6.1.0 + name: Install pnpm + with: + version: 11.9.0 + + - name: Install Node.js + uses: actions/setup-node@v7.0.0 + with: + node-version: 24 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Validate security/security-updates.json + run: pnpm run release:update-manifest check + + - name: Publish security updates to updates.json + if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/canary' }} + run: pnpm run release:update-manifest security-updates + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/api/migrations/20261015193600_add_core_update_checks/migration.sql b/apps/api/migrations/20261015193600_add_core_update_checks/migration.sql new file mode 100644 index 0000000000..4728618bba --- /dev/null +++ b/apps/api/migrations/20261015193600_add_core_update_checks/migration.sql @@ -0,0 +1,33 @@ +CREATE TABLE "core_update_lock" ( + "name" varchar(50) PRIMARY KEY, + "owner" varchar(64), + "expiresAt" timestamp, + "lastManualAt" timestamp +); +--> statement-breakpoint +ALTER TABLE "core_update_lock" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "core_update_results" ( + "componentId" varchar(214) PRIMARY KEY, + "sourceUrl" varchar(2048) NOT NULL, + "channel" varchar(20), + "versions" jsonb NOT NULL, + "hasUnknownVersion" boolean DEFAULT false NOT NULL, + "coreVersion" varchar(256), + "result" jsonb, + "lastAttemptAt" timestamp NOT NULL, + "lastSuccessAt" timestamp, + "lastError" jsonb +); +--> statement-breakpoint +ALTER TABLE "core_update_results" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "core_update_sources" ( + "url" varchar(2048) PRIMARY KEY, + "etag" varchar(512), + "lastModified" varchar(128), + "document" jsonb, + "lastAttemptAt" timestamp NOT NULL, + "lastSuccessAt" timestamp, + "lastError" jsonb +); +--> statement-breakpoint +ALTER TABLE "core_update_sources" ENABLE ROW LEVEL SECURITY; \ No newline at end of file diff --git a/apps/api/migrations/20261015193600_add_core_update_checks/snapshot.json b/apps/api/migrations/20261015193600_add_core_update_checks/snapshot.json new file mode 100644 index 0000000000..ae418ac893 --- /dev/null +++ b/apps/api/migrations/20261015193600_add_core_update_checks/snapshot.json @@ -0,0 +1,20146 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "62c3a2be-f65f-40be-94cf-ae96f691a04c", + "prevIds": [ + "efc06477-7761-4b25-85d1-ede72f9306ce" + ], + "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_navigation", + "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": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'header'", + "generated": null, + "identity": null, + "name": "location", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(120)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "presetId", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "href", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isOpenInNewTab", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "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": "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": "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 + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_user_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": "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": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_navigation_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_navigation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "location", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_navigation_location_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_navigation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_navigation_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_navigation" + }, + { + "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": "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": "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": "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_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": [ + "parentId" + ], + "schemaTo": "public", + "tableTo": "core_navigation", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_navigation_parentId_core_navigation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_navigation" + }, + { + "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_navigation_pkey", + "schema": "public", + "table": "core_navigation", + "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/web/content/docs/content-engine/content-visibility.mdx b/apps/web/content/docs/content-engine/content-visibility.mdx index 06abc5dded..d21c360ef7 100644 --- a/apps/web/content/docs/content-engine/content-visibility.mdx +++ b/apps/web/content/docs/content-engine/content-visibility.mdx @@ -141,7 +141,7 @@ The route tells whose record it is by its `id`, so `id` must be in `publicApi.fi Everyone else, signed in or not, still gets a `404`. Old slugs never redirect to a hidden record, and the list, sitemap and `hreflang` alternates leave it out for everybody. -Search learns that a record is hidden when it is hidden or unhidden. Records hidden before you updated VitNode show up for their authors and editors only after a reindex: **AdminCP → Advanced → Search**, then **Reindex** on the collection, such as `blog.post`. See [Search](/docs/content-engine/search). +Search learns that a record is hidden when it is hidden or unhidden. Records hidden before you updated VitNode show up for their authors and editors only after a reindex: **Debug Panel → Search** (`/admin/core/debug/search`), then **Reindex** on the collection, such as `blog.post`. See [Search](/docs/content-engine/search). Hiding changes nothing else. `status` and `publishedAt` stay as they are, so unhiding a published record puts it back with its original date. Publishing, unpublishing and scheduled publishes never change visibility, so publishing a hidden record keeps it off the site until someone unhides it. Restoring a revision never changes visibility either. diff --git a/apps/web/content/docs/content-engine/production.mdx b/apps/web/content/docs/content-engine/production.mdx index 759a268d26..0275704d81 100644 --- a/apps/web/content/docs/content-engine/production.mdx +++ b/apps/web/content/docs/content-engine/production.mdx @@ -56,7 +56,7 @@ publicApi: { ## Check content health -**AdminCP → Advanced → Search** shows how much of each content type is in the search index. Use **Reindex** on a collection, or **Rebuild index**, after a bulk import or a failed sync. +**Debug Panel → Search** in the AdminCP (`/admin/core/debug/search`) shows how much of each content type is in the search index. Use **Reindex** on a collection, or **Rebuild index**, after a bulk import or a failed sync. Both buttons need the **Rebuild and clear the search index** staff permission (`system` → `can_manage_search`). A rebuild clears the collection and refills it in pages of 200 records. It is a queued job, so it only runs while [cron](/docs/dev/cron) is running. @@ -156,7 +156,7 @@ The two records then stay in step: - The document stores the related record as its container: `containerType` is the target's content type id and `containerId` its record id. - When the related record's search title changes, or it is published, unpublished, hidden or unhidden, every record that borrows its title is re-indexed. Deleting it removes their documents. -Records saved before you turned this on need a **Reindex** of the collection, or **Rebuild index** in **AdminCP → Advanced → Search**. +Records saved before you turned this on need a **Reindex** of the collection, or **Rebuild index** in **Debug Panel → Search** (`/admin/core/debug/search`). ## Say where a record lives diff --git a/apps/web/content/docs/dev/advanced/auth.mdx b/apps/web/content/docs/dev/advanced/auth.mdx index 02b712a475..cbd98334b3 100644 --- a/apps/web/content/docs/dev/advanced/auth.mdx +++ b/apps/web/content/docs/dev/advanced/auth.mdx @@ -46,11 +46,11 @@ export const vitNodeApiConfig = buildApiConfig({ The AdminCP is the keys to the kingdom, so its session is deliberately short-lived. Staff are asked to sign in again when: - **They go quiet for an hour.** Every AdminCP request pushes the expiry an hour ahead, so an admin who keeps working is never interrupted. Leave the tab alone for an hour and it signs itself out and lands on the sign-in page, with a toast explaining why. Tune the window with `adminCookieExpires`. -- **They close every AdminCP tab.** Open AdminCP tabs keep a heartbeat going. Open the AdminCP again after every tab was closed and VitNode ends the old session instead of letting it back in. A page reload, or opening one more AdminCP tab while another is still open, is not affected. This check is skipped when `cookieDomain` is set: a browser can only see tabs on its own origin, so an AdminCP tab open on another subdomain would look closed. +- **They close every AdminCP tab for an hour.** Open AdminCP tabs keep a heartbeat going. Open the AdminCP again more than an hour after the last tab was closed and VitNode ends the old session instead of letting it back in. A page reload, or opening one more AdminCP tab while another is still open, is not affected. This check is skipped when `cookieDomain` is set: a browser can only see tabs on its own origin, so an AdminCP tab open on another subdomain would look closed. - **They close the browser.** The `vitnode_auth_admin` cookie has no expiry date, so the browser drops it when it shuts down. - Moving from the AdminCP to the public site in your only AdminCP tab stops the heartbeat. Come back within about 15 seconds and you are still signed in; any later and you sign in again. Open the public site in a new tab to keep your AdminCP session. + Moving from the AdminCP to the public site in your only AdminCP tab stops the heartbeat. Come back within an hour and you are still signed in; any later and you sign in again. Background tabs the browser throttles or freezes count as open for the same hour. --- diff --git a/apps/web/content/docs/dev/advanced/queue.mdx b/apps/web/content/docs/dev/advanced/queue.mdx index b8eed76735..7a1efb7165 100644 --- a/apps/web/content/docs/dev/advanced/queue.mdx +++ b/apps/web/content/docs/dev/advanced/queue.mdx @@ -101,13 +101,13 @@ A task still `processing` after 15 minutes (`QUEUE_STALE_PROCESSING_MINUTES`) be --- -## AdminCP Queue Monitor +## Inspect tasks in the Debug Panel -{/* Image prompt: VitNode AdminCP Queue Management screen at /admin/core/advanced/queue. Table displaying queued jobs with status badges (pending, processing, completed, failed), attempts count, payload JSON preview, and a retry action button. Dark theme, 1440x900. */} +{/* Image prompt: VitNode AdminCP Debug Panel at /admin/core/debug/queue with the "Queue" section open in the side menu. Table of queue tasks with columns Task, Queue, Status (pending, processing, completed, failed badges), Attempts, Available At, Created At and Last Error, plus a Status filter above it. Dark theme, 1440x900. */} -Inspect and debug tasks at **Core → Advanced → Queue** (`/admin/core/advanced/queue`): -- Monitor pending, active, and failed jobs in real time. -- View failure error traces and retry failed tasks with one click. +Open **Debug Panel → Queue** in the AdminCP (`/admin/core/debug/queue`), or type "Queue tasks" into the ⌘K admin search. The table lists every task with its queue, status, attempts, when it is next available, when it was created and its last error. Filter by status to find the failed ones. + +The section is read-only. A failed task stays failed, so fix the cause and dispatch it again from your code. Viewing it needs the `queue → can_view` staff permission. --- diff --git a/apps/web/content/docs/dev/advanced/redis.mdx b/apps/web/content/docs/dev/advanced/redis.mdx index 1c18db137f..a1388cd734 100644 --- a/apps/web/content/docs/dev/advanced/redis.mdx +++ b/apps/web/content/docs/dev/advanced/redis.mdx @@ -75,11 +75,11 @@ See [Cache API data](/docs/dev/cache/api) for invalidation and the full method l --- -## Verifying Redis in AdminCP +## Check Redis in the AdminCP -{/* Image prompt: VitNode AdminCP System -> Diagnostics screen at /admin/core/advanced/debug. System health card shows Redis status "Connected" with latency graph, memory usage, and ping metrics. Dark theme, 1440x900. */} +{/* Image prompt: VitNode AdminCP Debug Panel at /admin/core/debug/cache with the "Cache" section open. Engine "Redis" with a green "Redis connected" status, "Realtime across instances: Yes", and a "Clear cache" button. Dark theme, 1440x900. */} -Check Redis connection health anytime under **Core → Advanced → Debug** (`/admin/core/advanced/debug`). +Open **Debug Panel → Cache** in the AdminCP (`/admin/core/debug/cache`). It shows whether Redis is connected, unreachable or not configured (then the cache lives in memory), and whether realtime events reach every instance. An unreachable Redis also shows up on the Debug Panel's **Overview**. ## Learn More diff --git a/apps/web/content/docs/dev/ai/admin.mdx b/apps/web/content/docs/dev/ai/admin.mdx index 69327f44d7..bcc653c7e3 100644 --- a/apps/web/content/docs/dev/ai/admin.mdx +++ b/apps/web/content/docs/dev/ai/admin.mdx @@ -17,7 +17,7 @@ The AdminCP **Artificial Intelligence (AI)** section (`/admin/core/ai`) is where | Overview | `/admin/core/ai` | Spend against the budget, a forecast, trends and breakdowns. | | Settings sheet | `/admin/core/ai?settings=open` | Global switch, budgets, rate limits, retention, ALT text. | | Actions | `/admin/core/ai/actions` | Every registered AI feature: switch, model, limits. | -| History | `/admin/core/ai/history` | Every run: who, which model, tokens and cost. | +| Debug Panel → AI logs | `/admin/core/debug/ai` | Every run: who, which model, tokens and cost. | | Roles → Artificial intelligence (AI) tab | `/admin/core/users/roles` | A role's AI permissions, monthly points and daily limits. | | User → AI card | `/admin/core/users/{id}` | One member's exception: blocked, unlimited or own points. | | AI usage | `/settings/ai` (member) | A member's own points and what each AI feature cost them. | @@ -162,9 +162,9 @@ A member with several roles gets the **largest** allowance (never the sum), the The **AI access** card on a member's AdminCP page (**Users → a member**) gives them their own rule, ahead of their roles: **Block AI features**, **Unlimited points**, or a personal monthly allowance. It replaces what their roles give until you remove it. -## History +## AI logs -**Artificial Intelligence (AI) → History** lists every run with its action, status, origin (Members or Background jobs), model, cost and points. Filter by status, origin, action or model. **Details** shows: +**Debug Panel → AI logs** (`/admin/core/debug/ai`) lists every run with its action, status, origin (Members or Background jobs), model, cost and points. Filter by status, origin, action or model. **Details** shows: - tokens in and out, cost, cost source and what was charged or reserved; - every provider call, with retries and fallbacks listed as separate calls; @@ -172,7 +172,7 @@ The **AI access** card on a member's AdminCP page (**Users → a member**) gives - whether an editor accepted the suggestion (tracking only, never billing). - History records what a run took and what it cost. It never keeps the prompt, + The AI logs record what a run took and what it cost. It never keeps the prompt, the input text or the generated text. The same is true on the member's page. diff --git a/apps/web/content/docs/dev/ai/automatic-alt.mdx b/apps/web/content/docs/dev/ai/automatic-alt.mdx index 75d8bb2d2d..fc3ad39a35 100644 --- a/apps/web/content/docs/dev/ai/automatic-alt.mdx +++ b/apps/web/content/docs/dev/ai/automatic-alt.mdx @@ -202,7 +202,7 @@ curl -X POST https://example.com/api/@vitnode/core/cron \ -H "Authorization: Bearer $CRON_SECRET" ``` -Set a real `CRON_SECRET`. Each call runs every job whose schedule came due since its last run. A slower tick still runs the hourly sweep; it just works the queue less often (2 images per tick). Check **Last Run** at `/admin/core/advanced/cron`. See [Cron over REST](/docs/dev/cron/rest-api) and [Node CRON adapter](/docs/dev/cron/node-cron). +Set a real `CRON_SECRET`. Each call runs every job whose schedule came due since its last run. A slower tick still runs the hourly sweep; it just works the queue less often (2 images per tick). Check the last run in the **Time** column of **Debug Panel → Cron & queue** (`/admin/core/debug/cron`). See [Cron over REST](/docs/dev/cron/rest-api) and [Node CRON adapter](/docs/dev/cron/node-cron). ## Edit ALT text through the API diff --git a/apps/web/content/docs/dev/cron/custom-adapter.mdx b/apps/web/content/docs/dev/cron/custom-adapter.mdx index 4180939ada..ab008ba923 100644 --- a/apps/web/content/docs/dev/cron/custom-adapter.mdx +++ b/apps/web/content/docs/dev/cron/custom-adapter.mdx @@ -165,9 +165,9 @@ npm run dev -Open `/admin/core/advanced/cron` and watch the `process-queue` row - it is -scheduled `* * * * *`, so a healthy adapter keeps its **Last Run** under a minute -old. If it stays on _Never_, your `schedule()` ran but your tick is not landing: +Open **Debug Panel → Cron & queue** (`/admin/core/debug/cron`) and watch the +`process-queue` row - it is scheduled `* * * * *`, so a healthy adapter keeps its +last run under a minute old. If the **Time** column keeps saying _Last Never_, your `schedule()` ran but your tick is not landing: check the origin, then the secret, then whether the API can reach itself. diff --git a/apps/web/content/docs/dev/cron/index.mdx b/apps/web/content/docs/dev/cron/index.mdx index 5989789fa1..cf3ab61e94 100644 --- a/apps/web/content/docs/dev/cron/index.mdx +++ b/apps/web/content/docs/dev/cron/index.mdx @@ -75,14 +75,14 @@ curl -X POST https://your-domain.com/api/@vitnode/core/cron -H "Authorization: --- -## AdminCP Management +## Manage cron jobs in the Debug Panel -{/* Image prompt: VitNode AdminCP Cron management page at /admin/core/advanced/cron. Data table displaying scheduled jobs with columns: Name, Schedule, Status, Last Run, Next Run, and an action button "Run Now". Dark theme, 1440x900. */} +{/* Image prompt: VitNode AdminCP Debug Panel at /admin/core/debug/cron with the "Cron & queue" section open in the side menu. A green "All jobs on schedule" scheduler card with last run, next run and job count, and below it a cron jobs table with a search box and columns Name, Plugin and Time (schedule, "On schedule" badge, "Last … · Next …"), plus a play button per row. Dark theme, 1440x900. */} -Manage and inspect cron jobs in the AdminCP at **Core → Advanced → Cron Jobs** (`/admin/core/advanced/cron`): +Open **Debug Panel → Cron & queue** in the AdminCP (`/admin/core/debug/cron`), or type "Cron jobs" into the ⌘K admin search. Viewing it needs the `cron → can_view` staff permission. -- View status, schedules, and last execution logs. -- Click **Run Now** to execute any job manually on demand. +- The table lists every job with its **Name**, **Plugin** and **Time**: the schedule, a status badge, and the last and next run. It is sorted by next run, with a search box to find a job. +- The play button on a row (**Run Job Now**) runs that job on demand. It needs the `cron → can_run` permission. ### Health warnings @@ -90,7 +90,7 @@ Cron fails quietly by nature: nobody notices a job that didn't run. So VitNode w A job is **overdue** when its next scheduled run is more than 15 minutes in the past. Never-run jobs count from the moment they were registered. -The scheduler card at the top of the page sums it up: +The scheduler card at the top of the section sums it up: | What you see | What it means | | --- | --- | @@ -100,7 +100,7 @@ The scheduler card at the top of the page sums it up: Next to it you get the last run, the next run and how many jobs are registered. The table below lists every job with an **Overdue**, **On schedule** or **Waiting for first run** status. -The same check powers the **Cron Jobs** and **Queue Tasks** cards at **Core → System → Integrations**. +The same check powers the Debug Panel **Overview** and the **Cron Jobs** and **Queue Tasks** cards at **Core → System → Integrations**. --- diff --git a/apps/web/content/docs/dev/cron/node-cron.mdx b/apps/web/content/docs/dev/cron/node-cron.mdx index 9c8cb5e6c1..a856cd0398 100644 --- a/apps/web/content/docs/dev/cron/node-cron.mdx +++ b/apps/web/content/docs/dev/cron/node-cron.mdx @@ -93,13 +93,14 @@ npm run dev ### Verify the first tick -Open `/admin/core/advanced/cron` and watch the `clean` row - core's hourly -session cleanup, which every installation has. Within a minute of the restart -**Last Run** stops saying _Never_ and **Next Run** fills in. +Open **Debug Panel → Cron & queue** (`/admin/core/debug/cron`) and watch the +`clean` row - core's hourly session cleanup, which every installation has. +Within a minute of the restart its **Time** column stops saying _Last Never_ and +the next run fills in. Nothing else needs to happen for the clock to be proven: `process-queue` runs -every minute, so its **Last Run** should never be more than a minute old while -the adapter is alive. +every minute, so its last run should never be more than a minute old while the +adapter is alive. @@ -148,8 +149,8 @@ Three consequences worth writing down: `http://localhost:3000`. This repo's standalone API listens on **8000** - which is exactly why `apps/api/.env.example` sets the variable - so leaving it unset sends every tick to port 3000, where the web app lives. Symptom: the - scheduler is configured, the boot log is clean, and **Last Run** stays _Never_ - forever. + scheduler is configured, the boot log is clean, and the **Time** column keeps + saying _Last Never_ forever. @@ -158,8 +159,8 @@ Three consequences worth writing down: before the logger even exists, and a mismatched secret raises an `HTTPException` that the API's error handler returns without logging. So the failure you are most likely to hit writes nothing to the console and nothing - to the [Debug Panel](/docs/dev/debugging/logging). The AdminCP's **Last Run** - column is the health check. + to the [Debug Panel](/docs/dev/debugging/logging). The last run in the + **Time** column of **Debug Panel → Cron & queue** is the health check. diff --git a/apps/web/content/docs/dev/cron/rest-api.mdx b/apps/web/content/docs/dev/cron/rest-api.mdx index 8e4fcf3869..b9860af660 100644 --- a/apps/web/content/docs/dev/cron/rest-api.mdx +++ b/apps/web/content/docs/dev/cron/rest-api.mdx @@ -111,10 +111,11 @@ daily jobs honest and a poor way to drain a queue. ### Verify it from the AdminCP Send the request by hand once. A `200` with a job count means the endpoint -accepted you; open `/admin/core/advanced/cron` and the rows you expected to run -now show a **Last Run** of a moment ago. +accepted you; open **Debug Panel → Cron & queue** (`/admin/core/debug/cron`) +and the rows you expected to run now show a last run of a moment ago in the +**Time** column. -Then leave the scheduler to it and check back. **Last Run** drifting behind a +Then leave the scheduler to it and check back. A last run drifting behind a job's schedule is the symptom of a tick that is not arriving, and it is the only signal that reflects an external scheduler - see the gotcha about the Integrations screen below. @@ -169,8 +170,8 @@ Integrations screen below. registers a `cron` adapter - it is reading the config, not counting ticks. Tick from outside and that card stays grey however healthy the schedule is, and the queue card with it, because the queue's status is derived from cron's. - The honest signal for an external scheduler is the **Last Run** column on - `/admin/core/advanced/cron`. + The honest signal for an external scheduler is the last run in the **Time** + column of **Debug Panel → Cron & queue** (`/admin/core/debug/cron`). A generated route emits one lifecycle event per successful mutation, after the diff --git a/apps/web/content/docs/dev/i18n/localized-urls.mdx b/apps/web/content/docs/dev/i18n/localized-urls.mdx index 5102fc6498..288b12ba02 100644 --- a/apps/web/content/docs/dev/i18n/localized-urls.mdx +++ b/apps/web/content/docs/dev/i18n/localized-urls.mdx @@ -361,8 +361,9 @@ Details live in [Content Delivery and SEO](/docs/content-engine/public-pages) an Search stores each result's URL when it indexes it. After you change - `routePaths`, `domains` or `localePrefix`, rebuild the index in **Core → - Advanced → Search** so old results pick up the new URLs. + `routePaths`, `domains` or `localePrefix`, rebuild the index in **Debug + Panel → Search** (`/admin/core/debug/search`) so old results pick up the + new URLs. ## Validation errors diff --git a/apps/web/content/docs/dev/meta.json b/apps/web/content/docs/dev/meta.json index 8422262259..f3583f4d6f 100644 --- a/apps/web/content/docs/dev/meta.json +++ b/apps/web/content/docs/dev/meta.json @@ -42,6 +42,7 @@ "websocket", "advanced", "---Operate---", + "updates", "debugging", "swagger", "contribution", diff --git a/apps/web/content/docs/dev/notifications/queue-and-scale.mdx b/apps/web/content/docs/dev/notifications/queue-and-scale.mdx index d613c7e17c..f5ac359463 100644 --- a/apps/web/content/docs/dev/notifications/queue-and-scale.mdx +++ b/apps/web/content/docs/dev/notifications/queue-and-scale.mdx @@ -8,7 +8,7 @@ Notifications run on VitNode's database-backed [queue](/docs/dev/advanced/queue) ## Queue tasks and cron jobs -All of these belong to `@vitnode/core` and appear in **AdminCP → Advanced → Queue**. +All of these belong to `@vitnode/core`. Tasks appear in **Debug Panel → Queue** (`/admin/core/debug/queue`) and cron jobs in **Debug Panel → Cron & queue** (`/admin/core/debug/cron`). | Name | Kind | Does | | ------------------------ | --------------------- | ----------------------------------------------------------------------- | diff --git a/apps/web/content/docs/dev/notifications/troubleshooting.mdx b/apps/web/content/docs/dev/notifications/troubleshooting.mdx index 450ea6c2af..ae7200a61c 100644 --- a/apps/web/content/docs/dev/notifications/troubleshooting.mdx +++ b/apps/web/content/docs/dev/notifications/troubleshooting.mdx @@ -17,7 +17,7 @@ A publishing request starts delivery right after its response. Everything else i 1. **The cron adapter is not calling the queue worker.** In development there is no `CRON_SECRET`, so queued tasks wait until you run them. See [Cron](/docs/dev/cron). 2. **Notifications are paused.** The AdminCP notifications screen shows a **Notifications are paused** banner. See [Use the Settings sheet](/docs/dev/notifications/admincp#use-the-settings-sheet). -3. **The delivery task failed.** **AdminCP → Advanced → Queue** lists failed `notifications-fanout` tasks with the error. +3. **The delivery task failed.** **Debug Panel → Queue** in the AdminCP (`/admin/core/debug/queue`) lists failed `notifications-fanout` tasks with their last error. Filter by the **Failed** status to find them. 4. **The member never gets it.** The actor is skipped unless you pass `allowSelf: true`, and members who turned the type off or fail your `access` check get nothing. See [Choose the recipients](/docs/dev/notifications/publishing#choose-the-recipients). ## Emails are not sending diff --git a/apps/web/content/docs/dev/plugins/build.mdx b/apps/web/content/docs/dev/plugins/build.mdx index 28013e7205..1bc1c65026 100644 --- a/apps/web/content/docs/dev/plugins/build.mdx +++ b/apps/web/content/docs/dev/plugins/build.mdx @@ -78,7 +78,7 @@ You don't need `isolatedDeclarations` or explicit return types. Inferred types c ## Generated manifest -Before compiling, `vitnode build` also writes `src/manifest.gen.ts`. It exports `manifest`, which holds the plugin's `name` (as `id`), `version`, `description`, `author` and `license` from `package.json`, and imports the title and Lucide icon from the plugin's own `src/plugin-info.ts`. `vitnode dev` writes it on start and again whenever `package.json` changes. It is the single source of the plugin id, passed to `buildPlugin()`, `buildApiPlugin()` and `bindApiPlugin()`, and it is what the [AdminCP plugin catalog](/docs/dev/plugins/catalog) shows, so commit it with the rest of `src`. +Before compiling, `vitnode build` also writes `src/manifest.gen.ts`. It exports `manifest`, which holds the plugin's `name` (as `id`), `version`, `description`, `author` and `license` from `package.json`, and imports the title, Lucide icon and update source from the plugin's own `src/plugin-info.ts`. `vitnode dev` writes it on start and again whenever `package.json` changes. It is the single source of the plugin id, passed to `buildPlugin()`, `buildApiPlugin()` and `bindApiPlugin()`, and it is what the [AdminCP plugin catalog](/docs/dev/plugins/catalog) shows, so commit it with the rest of `src`. ## What a plugin package needs diff --git a/apps/web/content/docs/dev/plugins/catalog.mdx b/apps/web/content/docs/dev/plugins/catalog.mdx index fccafdf553..9848e9fb33 100644 --- a/apps/web/content/docs/dev/plugins/catalog.mdx +++ b/apps/web/content/docs/dev/plugins/catalog.mdx @@ -16,7 +16,7 @@ import catalog from './admin-plugins-catalog.webp' withoutBackground /> -The catalog only reports what is installed. It does not check for updates, install or remove plugins, or switch them on and off. One thing at a time. +The catalog reports what is installed. It also shows available updates and security updates for core and for plugins that declare an update source - see [Update checks](/docs/dev/updates). It never installs or removes plugins, or switches them on and off. ## Add your plugin to the catalog @@ -155,6 +155,10 @@ A side that reports no version is unknown, not a mismatch. A frontend inventory entry carrying the manifest of another package throws at startup: the API checks that `manifest.id` equals the entry's `id`. +## Update and security information + +Add `updates.url` to your plugin's `src/plugin-info.ts` and the catalog row also shows new releases and security updates. Setup is covered in [Publish updates for your plugin](/docs/dev/updates/plugin-authors); what administrators see is in [Check for updates in the AdminCP](/docs/dev/updates/admin). + ## Who can see the catalog -The page and its endpoint, `GET /api/@vitnode/core/admin/plugins`, require the **Plugins → View installed plugins and their versions** permission (`plugins.can_view` in the `@vitnode/core` module). Staff without it don't see the link, get a not-found page on a direct visit, and a `403` from the API. Root administrators have it automatically. +The page and its endpoint, `GET /api/@vitnode/core/admin/plugins`, require the **Plugins → View installed plugins and their versions** permission (`plugins.can_view` in the `@vitnode/core` module). Staff without it don't see the link, get a not-found page on a direct visit, and a `403` from the API. Root administrators have it automatically. The **Check for updates** button additionally needs `plugins.can_check_updates`. diff --git a/apps/web/content/docs/dev/search-elasticsearch.mdx b/apps/web/content/docs/dev/search-elasticsearch.mdx index 7df73dc9dd..33a25ee3e0 100644 --- a/apps/web/content/docs/dev/search-elasticsearch.mdx +++ b/apps/web/content/docs/dev/search-elasticsearch.mdx @@ -68,13 +68,13 @@ also accepts `username` and `password` for a self-hosted secured cluster. ### Rebuild the mirror -Restart the API, then open **Core → Advanced → Search** in the AdminCP and run -**Rebuild index**. New changes are mirrored automatically; the rebuild fills +Restart the API, then open **Debug Panel → Search** in the AdminCP +(`/admin/core/debug/search`) and run **Rebuild index**. New changes are mirrored automatically; the rebuild fills the historical records already in Postgres. Rebuilding (and clearing a collection) needs the **Rebuild and clear the search index** permission -(`system:can_manage_search`) - viewing the page alone is not enough. +(`system:can_manage_search`) - viewing the section alone is not enough. -{/* Image prompt: VitNode AdminCP search page showing Elasticsearch as connected, one “Rebuild index” button, a progress state, and collection counts. Dark theme, 1440x900. */} +{/* Image prompt: VitNode AdminCP Debug Panel at /admin/core/debug/search showing the Search section with Elasticsearch as the engine, a “Rebuild index” button in the header, and collection counts. Dark theme, 1440x900. */} diff --git a/apps/web/content/docs/dev/search.mdx b/apps/web/content/docs/dev/search.mdx index a20b72d5ec..a145d4fe68 100644 --- a/apps/web/content/docs/dev/search.mdx +++ b/apps/web/content/docs/dev/search.mdx @@ -218,14 +218,18 @@ A document you index yourself with `c.get('search').index()` keeps its `url` exa --- -## AdminCP Search Management +## Manage the search index in the Debug Panel -{/* Image prompt: VitNode AdminCP Search settings screen at /admin/core/advanced/search. Dashboard displaying search index statistics, engine status (PostgreSQL / Elasticsearch), registered item types with indexed document counts, and a "Rebuild Index" button. Dark theme, 1440x900. */} +{/* Image prompt: VitNode AdminCP Debug Panel at /admin/core/debug/search with the "Search" section open in the side menu. Header with a "Rebuild index" button, a row of stat cards (status, engine PostgreSQL, indexed items, last indexed), and a collections table with a search box, indexed counts and a "Reindex" button per row. Dark theme, 1440x900. */} -Manage search status at **Core → Advanced → Search** (`/admin/core/advanced/search`): +Open **Debug Panel → Search** in the AdminCP (`/admin/core/debug/search`), or type "Search index" into the ⌘K admin search: -- View indexed record counts across all collections. -- Trigger background rebuilds of specific collections or the entire site. +- The stat cards show the index status, the engine, how many items are indexed and when the last one was indexed. Recent sync errors appear below them. +- The collections table lists indexed counts per collection, with a search box to find one. +- **Reindex** rebuilds one collection, and **Rebuild index** in the header rebuilds all of them. +- **Remove documents** shows up instead of **Reindex** on a collection with no rebuild indexer. It clears that collection's documents, and live plugin writes may add them back. + +Opening the section needs `system → can_view`. The rebuild, reindex and remove buttons need `system → can_manage_search`. A rebuild is a queued job, so it only runs once [cron](/docs/dev/cron) is running. Until then it waits in the queue, patient but unproductive. diff --git a/apps/web/content/docs/dev/updates/admin-plugin-updates.webp b/apps/web/content/docs/dev/updates/admin-plugin-updates.webp new file mode 100644 index 0000000000..1ce3d25552 Binary files /dev/null and b/apps/web/content/docs/dev/updates/admin-plugin-updates.webp differ diff --git a/apps/web/content/docs/dev/updates/admin.mdx b/apps/web/content/docs/dev/updates/admin.mdx new file mode 100644 index 0000000000..9561318bb2 --- /dev/null +++ b/apps/web/content/docs/dev/updates/admin.mdx @@ -0,0 +1,81 @@ +--- +title: Check for updates in the AdminCP +description: Where VitNode shows available updates and security updates, how the daily check and the Check for updates button work, and what each status means. +icon: ShieldCheck +--- + +import { ImgDocs } from '@/components/fumadocs/img' + +import pluginUpdates from './admin-plugin-updates.webp' + +**AdminCP → System → Plugins** shows, next to every installed version, whether a newer release is published and whether a security update affects it. Core gets its own card above the plugin list. + + + +The security update in the screenshot is illustrative demo data. + +## Before you begin + +- A [cron adapter or external trigger](/docs/dev/cron) must be running. The daily check is a cron job; without a scheduler nothing is checked automatically. +- The API server needs outbound HTTPS (port 443) to the update manifests - for VitNode itself, `raw.githubusercontent.com`. Requests go out directly; HTTP proxy variables such as `HTTPS_PROXY` are not used. +- Run `vitnode db migrate` after upgrading. The check stores its results in three small tables: `core_update_sources`, `core_update_results` and `core_update_lock`. + +## The daily check + +The cron job `@vitnode/core:cron:check_updates` runs at **03:17 every day in the API server's time zone** (the `TZ` environment variable - UTC on most hosts and containers). A newly registered job has never run, so the first scheduler tick after you deploy runs the first check. Until then every row says _Not checked yet_. + +Each run fetches every distinct manifest once, even when several plugins share it, with a 10 second timeout, a 1 MB size limit and at most four sources at a time. Opening the catalog never contacts a publisher - it only reads stored results. + +## Check now + +Staff with **Plugins → Check for plugin and core updates now** (`plugins.can_check_updates`) see a **Check for updates** button. It runs the same check as the cron job and refreshes the page when it finishes. + +- Only one check runs at a time across all API instances. A second click while one runs says _A check is already running_. +- A manual check can run once every two minutes. A click inside that window says how many seconds to wait. +- The endpoint is `POST /api/@vitnode/core/admin/plugins/updates/check`; it answers `409` while a check runs and `429` during the cooldown. + +Viewing the results only needs **Plugins → View installed plugins and their versions** (`plugins.can_view`). + +## Read the results + +| Status | What it means | +| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Not checked yet | No check has covered this package yet. | +| Up to date | Every version you run is at or above the channel's newest release. | +| Update available: 2.1.3 | A newer release is on your channel. Release notes link to it when the publisher gave one. | +| Requires a newer core | The release declares a core range your core is outside. If an older release on the channel fits, it is named. | +| Compatibility unknown | The publisher did not say which core the release needs. VitNode does not guess. | +| Update checking unavailable | The package declares no update source. | +| Last check failed | The source could not be read. The last good result stays visible, with its date. | +| Not checked for this deployment yet | You redeployed, changed channel or upgraded core since the last check. Results are recomputed from the stored manifest until the next check confirms them. | + +When the web app and the API run different versions of a plugin, the existing mismatch warning stays, and both versions are checked. A vulnerable web app version is reported even if the API's version is fine. + +## Security warnings + +A red **Security update** box with a shield icon appears when a version you run is inside a security update's affected range. It names the security update, the affected version - with _(API)_ or _(web app)_ when they differ - and either _Fixed in 2.1.3_ or _No fixed version has been published yet_. **Details** opens the publisher's page. + +Security updates are checked even when your channel has no newer release. A patch release on its own is never shown as a security fix; only a matching security update is. + +## Choose a channel + +By default each package follows the channel of the version you run: `2.1.0` is **stable**, `2.1.0-rc.1` is **rc** and `2.1.0-canary.4` is **canary**. VitNode never moves you between channels or suggests a downgrade. To follow another channel on purpose, set it in the API config: + +```ts title="src/vitnode.api.config.ts" +export const vitNodeApiConfig = buildApiConfig({ + updates: { + channel: 'stable', + }, + // ...the rest of your API config +}) +``` + +An install without outbound network access can switch checks off with `updates: { enabled: false }`. The catalog then says so instead of showing results. + +## Check the result + +Open **System → Plugins** and press **Check for updates**. A toast reports how many updates and security updates were found, and the core card shows _Last checked_ with the current time. diff --git a/apps/web/content/docs/dev/updates/index.mdx b/apps/web/content/docs/dev/updates/index.mdx new file mode 100644 index 0000000000..89b577afe2 --- /dev/null +++ b/apps/web/content/docs/dev/updates/index.mdx @@ -0,0 +1,25 @@ +--- +title: Update checks +description: How VitNode tells administrators about new versions and security updates for core and plugins, using public JSON manifests checked once a day. +icon: RefreshCw +--- + +VitNode checks once a day whether a newer version of core or any installed plugin is published, and whether the version you run is affected by a published security update. The results appear in **AdminCP → System → Plugins**. + +Each package says where its **update manifest** lives - a public JSON file on GitHub Raw or any other HTTPS host. VitNode reads those files from the server. It does not ask npm, and it does not install, download or change anything. + +## Pick a guide + +| You want to... | Read | +| ----------------------------------------------------------------------- | -------------------------------------------------------------------- | +| See updates and security warnings, run a check, choose a channel | [Check for updates in the AdminCP](/docs/dev/updates/admin) | +| Make your own plugin report updates | [Publish updates for your plugin](/docs/dev/updates/plugin-authors) | +| Look up every field of the JSON format | [Update manifest reference](/docs/dev/updates/manifest) | +| Warn installations about a vulnerability | [Publish a security update](/docs/dev/updates/security-updates) | +| Understand how VitNode's own release workflow updates the official feed | [Official release feed](/docs/dev/updates/official-releases) | + +## What the checks can and cannot tell you + +- **Informational only.** A check never installs an update or edits your dependencies. You upgrade the way you always do, then rebuild and redeploy. +- **Only what publishers declare.** A package without an update source shows _Update checking unavailable_. Private and local plugins work exactly as before. +- **Not a vulnerability scanner.** A security warning means a publisher listed your installed version in a security update. No warning is not proof a version is secure, and dependencies of your plugins are not checked at all. Keep using `pnpm audit` or your usual scanner for those. diff --git a/apps/web/content/docs/dev/updates/manifest.mdx b/apps/web/content/docs/dev/updates/manifest.mdx new file mode 100644 index 0000000000..23e0d9b7ef --- /dev/null +++ b/apps/web/content/docs/dev/updates/manifest.mdx @@ -0,0 +1,82 @@ +--- +title: Update manifest reference +description: Every field of the VitNode update manifest JSON - packages, channels, releases and security updates - with validation rules and how versions are compared. +icon: FileCode +--- + +An update manifest is one public JSON file per publisher. It can describe any number of packages. VitNode validates it completely before using any of it; an invalid or unsupported file is reported as a failed check, and the last valid copy keeps being used. + +## Shape + +```json title="updates.json" +{ + "schemaVersion": 1, + "packages": { + "@acme/site-notes": { + "channels": { + "stable": "2.1.3", + "canary": "2.2.0-canary.4" + }, + "releases": { + "2.1.3": { + "publishedAt": "2026-10-15T12:00:00Z", + "requiresCore": ">=2.1.0 <3.0.0", + "url": "https://github.com/acme/site-notes/releases/tag/v2.1.3" + }, + "2.2.0-canary.4": {} + }, + "securityUpdates": [ + { + "id": "NOTES-2026-001", + "affected": ">=2.0.0 <2.1.3", + "fixed": "2.1.3", + "url": "https://github.com/acme/site-notes/security/advisories/GHSA-xxxx-xxxx-xxxx" + } + ] + } + } +} +``` + +The values above are illustrative, not a real release or vulnerability. + +## Top level + +| Name | Type | Required | Description | +| --------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------- | +| `schemaVersion` | `1` | Yes | Format version. Any other value fails with _uses a format this VitNode does not support_. | +| `packages` | object | Yes | Keyed by npm package name - the plugin's manifest ID. At most 500 packages. Unknown keys anywhere are rejected. | + +## Package + +| Name | Type | Required | Default | Description | +| ------------ | ------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `channels` | object | No | `{}` | `stable`, `rc` and/or `canary`, each a version. Every one must have an entry in `releases`. `stable` cannot be a prerelease. A package does not have to publish every channel. | +| `releases` | object | No | `{}` | Keyed by exact SemVer version. At most 2000. | +| `securityUpdates` | array | No | `[]` | Independent of channels and releases. At most 500, with unique `id`s. | + +## Release + +| Name | Type | Required | Description | +| -------------- | -------------- | -------- | ------------------------------------------------------------------------------------------- | +| `publishedAt` | ISO 8601 date | No | Shown as _released ..._. Informational only. | +| `requiresCore` | SemVer range | No | The `@vitnode/core` versions this release supports. Missing means compatibility is unknown. | +| `url` | `https://` URL | No | Shown as **Release notes**. | + +## Security update + +| Name | Type | Required | Description | +| ---------- | -------------- | -------- | ------------------------------------------------------------ | +| `id` | string | Yes | Letters, digits, `.`, `_`, `:` or `-`, up to 100 characters. | +| `affected` | SemVer range | Yes | Installed versions inside this range get the red warning. | +| `fixed` | SemVer version | No | Leave it out when no fix exists yet. | +| `url` | `https://` URL | Yes | Shown as **Details**. | + +Every security update is a security update - there are no severity levels. Explain the issue on the security update page; links are treated as untrusted, and only `https://` links become clickable. + +## How versions are compared + +- Comparisons use SemVer, not string order: `2.10.0` is newer than `2.9.0`, and `2.1.0-canary.4` is older than `2.1.0`. +- The selected channel is the installation's `updates.channel`, or else the channel of the installed version. An installed version newer than the channel's target is never offered a "downgrade". +- `requiresCore` is checked against the running core version. When the channel's newest release needs a newer core, VitNode names the newest release on the same channel that is newer than yours and explicitly declares support for your core, if one is listed. +- Security update ranges include prereleases: see [how ranges match prereleases](/docs/dev/updates/security-updates#how-affected-ranges-match-prereleases). diff --git a/apps/web/content/docs/dev/updates/meta.json b/apps/web/content/docs/dev/updates/meta.json new file mode 100644 index 0000000000..122df555f3 --- /dev/null +++ b/apps/web/content/docs/dev/updates/meta.json @@ -0,0 +1,14 @@ +{ + "title": "Updates & Security", + "description": "Daily update checks and security updates for VitNode core and plugins, read from public JSON manifests", + "icon": "RefreshCw", + "pages": [ + "index", + "admin", + "---Publish updates---", + "plugin-authors", + "manifest", + "security-updates", + "official-releases" + ] +} diff --git a/apps/web/content/docs/dev/updates/official-releases.mdx b/apps/web/content/docs/dev/updates/official-releases.mdx new file mode 100644 index 0000000000..8563f07e78 --- /dev/null +++ b/apps/web/content/docs/dev/updates/official-releases.mdx @@ -0,0 +1,54 @@ +--- +title: Official release feed +description: How VitNode's release workflow publishes the official update manifest after npm publication, where publishedAt comes from, and how reruns and security updates are handled. +icon: Workflow +--- + +VitNode core and the official plugins read the feed at `https://raw.githubusercontent.com/aXenDeveloper/vitnode/update-manifest/updates.json`. A GitHub Actions job writes it after every successful npm publish. Maintainers never edit it by hand. + +## How a release updates the feed + +The **Bump and Publish npm packages** workflow has a **Publish update manifest** job. It runs only in the `bump_and_publish` and `publish` modes, and only after the publish job succeeds - a `bump` run publishes nothing, so it advertises nothing. + +1. It checks out the exact commit that was published. +2. From `PUBLISHED_PACKAGES` - the same list the publish steps use - it keeps the packages whose manifest points at this feed as `updates.url` - `src/plugin-info.ts` for a plugin, `src/manifest.ts` for core (today `@vitnode/core` and `@vitnode/blog`). +3. It reads each version from `package.json`. A plugin's `@vitnode/core` dependency becomes the release's `requiresCore`, resolving `workspace:*` the way pnpm does on publish. +4. It adds the releases and moves only the channel of this run: **canary**, **release-candidate** (`rc`) or **stable**. A channel never moves backwards. +5. It commits `updates.json` to the `update-manifest` branch. That branch exists only for the feed, so it outlives release branches. + +## Where publishedAt comes from + +`publishedAt` and the release `url` come from the GitHub Release tagged `v` (`gh release view`). Nobody types a date. When the run skipped release notes (`skip_release_github_patch_notes`) or the release is still a draft, both fields are left out - the date is never invented, and the time of the workflow run is never used instead. + +## Reruns and partial failures + +- **Reruns are idempotent.** An existing release entry keeps its `publishedAt` and `url`; a rerun only fills in fields that were missing, for example after the GitHub Release was created later. A run that changes nothing pushes nothing. +- **Concurrent runs** share the `vitnode-update-manifest` concurrency group, and every push is fetched, merged and retried if another run got there first. +- **If npm publishing fails**, the job does not run. Fix the cause and re-run the workflow in `publish` mode. +- **If only the manifest job fails**, use **Re-run failed jobs**: it publishes the same commit again. + +## Add a security update to the official feed + +Security updates are not part of releases. Edit `security/security-updates.json` in a pull request: + +```json title="security/security-updates.json" +{ + "schemaVersion": 1, + "securityUpdates": { + "@vitnode/blog": [ + { + "id": "VITNODE-BLOG-2026-001", + "affected": ">=2.0.0 <2.1.3", + "fixed": "2.1.3", + "url": "https://github.com/aXenDeveloper/vitnode/security/advisories/GHSA-xxxx-xxxx-xxxx" + } + ] + } +} +``` + +The **Publish security updates** workflow validates the file on the pull request. After it merges into `canary`, the workflow replaces the security updates in `updates.json` with exactly what the file lists; releases and channels stay untouched. The example above is illustrative - only real security updates belong in that file. Writing ranges is covered in [Publish a security update](/docs/dev/updates/security-updates). + +## Check the result + +After a release or a security update change, open the feed URL above. Within a few minutes of GitHub's cache, it shows the new version under `channels` and its entry under `releases`. diff --git a/apps/web/content/docs/dev/updates/plugin-authors.mdx b/apps/web/content/docs/dev/updates/plugin-authors.mdx new file mode 100644 index 0000000000..d56df0fd1d --- /dev/null +++ b/apps/web/content/docs/dev/updates/plugin-authors.mdx @@ -0,0 +1,99 @@ +--- +title: Publish updates for your plugin +description: Declare an update source in your plugin's src/plugin-info.ts and host a small JSON manifest so VitNode installations see your new releases and security updates. +icon: Upload +--- + +Add one field to your plugin's `src/plugin-info.ts` and publish a JSON file next to your code. Every installation of your plugin then shows your new releases and security updates in **AdminCP → System → Plugins**. + + + + +### Declare the update source + +Add `updates.url` to the plugin's `src/plugin-info.ts`, next to its title and icon: + +```ts title="plugins/site-notes/src/plugin-info.ts" +import type { PluginInfo } from "@vitnode/core/lib/plugin-manifest"; + +export const pluginInfo: PluginInfo = { + title: "Site Notes", + icon: "sticky-note", + updates: { + url: "https://raw.githubusercontent.com/acme/site-notes/main/updates.json", + }, +}; +``` + +The URL has to be `https://`, without a user name, password, query string or fragment. It ends up in browser-visible metadata, so it must never carry a token. + + + + +### Rebuild the plugin + +Run `vitnode build` (or keep `vitnode dev` running). `src/manifest.gen.ts` picks the URL up from `pluginInfo` as `updates`, next to the version, so both the API and the web app's plugin inventory know where to look. + + + + +### Publish the manifest + +Commit `updates.json` to the branch the URL points at. The smallest useful file names your package, a channel and that release: + +```json title="updates.json" +{ + "schemaVersion": 1, + "packages": { + "@acme/site-notes": { + "channels": { "stable": "1.2.0" }, + "releases": { + "1.2.0": { + "publishedAt": "2026-10-01T12:00:00Z", + "requiresCore": ">=2.0.0 <3.0.0", + "url": "https://github.com/acme/site-notes/releases/tag/v1.2.0" + } + } + } + } +} +``` + +The package key must equal the `name` in `package.json`. `publishedAt`, `requiresCore` and `url` are optional; without `requiresCore` installations see _Compatibility unknown_. Every field is described in the [manifest reference](/docs/dev/updates/manifest). + + + + +## Check the result + +Install the plugin in an app, open **System → Plugins** and press **Check for updates**. With the plugin at `1.1.0` the row says _Update available: 1.2.0_; at `1.2.0` it says _Up to date_. + +## Several packages, one manifest + +A publisher with several plugins can keep one file. Point every plugin at the same URL and list each package under `packages`. VitNode downloads a shared file once per check. + +```json title="updates.json" +{ + "schemaVersion": 1, + "packages": { + "@acme/site-notes": { + "channels": { "stable": "1.2.0" }, + "releases": { "1.2.0": {} } + }, + "@acme/polls": { + "channels": { "stable": "0.4.1", "canary": "0.5.0-canary.2" }, + "releases": { "0.4.1": {}, "0.5.0-canary.2": {} } + } + } +} +``` + +## Hosting on GitHub Raw + +`https://raw.githubusercontent.com////updates.json` works well: it is public, served over HTTPS and supports `ETag`, so an unchanged file costs a `304 Not Modified`. Raw files are cached for a few minutes, so a fresh release can take that long to appear. Keep the file on a branch that is never deleted. VitNode's own feed lives on a dedicated branch for that reason. + +Any other HTTPS host works too, as long as it is publicly reachable on port 443. Redirects are followed up to three times, only to other public HTTPS addresses. + +## Private and local plugins + +Leave `updates` out of `src/plugin-info.ts`. The plugin works as before, and the catalog shows _Update checking unavailable_ for it. Nothing is sent anywhere for a plugin without an update source. diff --git a/apps/web/content/docs/dev/updates/security-updates.mdx b/apps/web/content/docs/dev/updates/security-updates.mdx new file mode 100644 index 0000000000..cd28e6a833 --- /dev/null +++ b/apps/web/content/docs/dev/updates/security-updates.mdx @@ -0,0 +1,48 @@ +--- +title: Publish a security update +description: Add a security update to an update manifest so VitNode installations running an affected version see a red warning in the AdminCP, with or without a fixed version. +icon: ShieldAlert +--- + +A security update in your update manifest turns every affected installation's catalog row red within a day - or right away when an administrator presses **Check for updates**. VitNode compares each installed version, API and web app separately, with the security update's `affected` range. + +## Add a security update + +Add an entry to the package's `securityUpdates` array: + +```json title="updates.json" +{ + "id": "NOTES-2026-001", + "affected": ">=2.0.0 <2.1.3", + "fixed": "2.1.3", + "url": "https://github.com/acme/site-notes/security/advisories/GHSA-xxxx-xxxx-xxxx" +} +``` + +Write `id`, `affected` and `url` every time. There are no severity levels or summaries: a security update is a security update, and the details live on the page `url` points to. Add `fixed` once a release contains the fix; installations then read _Fixed in 2.1.3_. The example is illustrative, not a real vulnerability. + +Security updates do not depend on channels or releases. An installation is warned even when its channel has no newer release, and even when you no longer publish that channel at all. + +## Announce a vulnerability without a fix + +Publish the security update without `fixed`. Affected installations see _No fixed version has been published yet_. Put mitigation steps on the security update page, such as disabling a feature. When the fix ships, add `fixed` - and widen or narrow `affected` if the release that fixes it changes the picture. + +## How affected ranges match prereleases + +Ranges are matched with prereleases included, by SemVer order: + +| Installed | `affected: ">=2.0.0 <2.1.3"` | Why | +| ---------------- | ---------------------------- | --------------------------------------------------- | +| `2.1.0` | affected | Inside the range. | +| `2.1.0-canary.4` | affected | `2.1.0-canary.4` sorts between `2.0.0` and `2.1.3`. | +| `2.1.3-canary.1` | affected | A prerelease of `2.1.3` sorts before `2.1.3`. | +| `2.1.3` | not affected | The upper bound is exclusive. | +| `2.0.0-rc.1` | not affected | Prereleases of `2.0.0` sort before `2.0.0`. | + +If the fix already landed in `2.1.3-canary.2`, say so in the range: `">=2.0.0 <2.1.3-canary.2"`. When in doubt, keep a prerelease inside the range - a false warning is cheaper than a missed one. + +## What a warning does not mean + +A warning means the publisher listed that version. No warning is not proof that a version is secure, and VitNode does not scan your plugins' own dependencies. A patch release without a security update is not shown as a security release. + +For VitNode's own packages, security updates are maintained in `security/security-updates.json` in the VitNode repository; see [Official release feed](/docs/dev/updates/official-releases#add-an-security update-to-the-official-feed). diff --git a/apps/web/content/docs/guides/forum.mdx b/apps/web/content/docs/guides/forum.mdx index 89323bd047..319c553199 100644 --- a/apps/web/content/docs/guides/forum.mdx +++ b/apps/web/content/docs/guides/forum.mdx @@ -127,7 +127,7 @@ on their authors' profile timelines. A comment is headed by its topic's title and stays out of public results while its topic is not public - see [Borrow the title from a related record](/docs/content-engine/search#borrow-the-title-from-a-related-record). Upgrading a forum that already has posts? Rebuild the index once in -**AdminCP → Advanced → Search**. +**Debug Panel → Search** (`/admin/core/debug/search`). ## Choose who publishes straight away diff --git a/apps/web/src/locales/@vitnode/core/pl.json b/apps/web/src/locales/@vitnode/core/pl.json index 38441dbe71..3a66b4cfed 100644 --- a/apps/web/src/locales/@vitnode/core/pl.json +++ b/apps/web/src/locales/@vitnode/core/pl.json @@ -64,7 +64,6 @@ "admin": { "advanced": { "cron": { - "desc": "Zarządzaj i monitoruj zaplanowane zadania.", "health": { "facts": { "jobs": "Zadania", @@ -97,28 +96,27 @@ } }, "lastRun": { - "never": "Nigdy", - "title": "Ostatnie uruchomienie" + "never": "Nigdy" }, "name": "Nazwa", "nextRun": { - "never": "Nigdy", - "title": "Następne uruchomienie" + "never": "Nigdy" }, "plugin": "Wtyczka", - "schedule": "Harmonogram", "search": "Szukaj zadań…", "status": { "on_schedule": "Zgodnie z harmonogramem", "overdue": "Zaległe", - "title": "Status", "waiting": "Czeka na pierwsze uruchomienie" + }, + "time": { + "title": "Czas", + "last": "Ostatnio ", + "next": "Następnie " } - }, - "title": "Zadania cron" + } }, "queue": { - "desc": "Monitoruj zadania w tle oczekujące na przetworzenie przez proces cron.", "list": { "attempts": "Próby", "availableAt": "Dostępne od", @@ -134,8 +132,7 @@ "failed": "Nieudane", "pending": "Oczekujące", "processing": "Przetwarzanie" - }, - "title": "Zadania w kolejce" + } } }, "dashboard": { @@ -227,6 +224,141 @@ } }, "debug": { + "title": "Panel debugowania", + "desc": "Sprawdź stan instalacji, jej logi i pracę w tle.", + "unavailable": "Nie udało się wczytać tej sekcji. Spróbuj ponownie za chwilę.", + "nav": { + "label": "Sekcje panelu debugowania", + "menu": "Sekcja", + "systems": "Systemy", + "diagnostics": "Diagnostyka", + "overview": "Przegląd", + "vitnode": "VitNode", + "plugins": "Wtyczki", + "cron": "Cron i kolejka", + "cache": "Pamięć podręczna", + "search": "Wyszukiwanie", + "services": "Usługi", + "logs": "Przeglądarka logów", + "queue": "Kolejka", + "ai": "Logi AI" + }, + "tone": { + "critical": "Krytyczne", + "warn": "Wymaga uwagi", + "ok": "Sprawne", + "off": "Wyłączone" + }, + "overview": { + "attention": "{count, plural, one {# rzecz wymaga uwagi} few {# rzeczy wymagają uwagi} many {# rzeczy wymaga uwagi} other {# rzeczy wymaga uwagi}}", + "healthy": "Wszystko działa poprawnie", + "summary": "{critical, plural, =0 {Brak problemów krytycznych} one {# problem krytyczny} few {# problemy krytyczne} many {# problemów krytycznych} other {# problemów krytycznych}} · {warnings, plural, =0 {brak ostrzeżeń} one {# ostrzeżenie} few {# ostrzeżenia} many {# ostrzeżeń} other {# ostrzeżeń}}", + "recheck": "Sprawdź ponownie", + "systems": { + "title": "Systemy", + "desc": "Wszystko, co VitNode sprawdził w tej instalacji." + } + }, + "diagnostics": { + "logs": "Każde ostrzeżenie i błąd zapisane przez API, od najnowszych.", + "queue": "Zadania w tle i to, co się z nimi stało.", + "ai": "Każde zapytanie do AI z modelem, statusem i kosztem." + }, + "security": { + "alert": { + "title": "Dostępna aktualizacja bezpieczeństwa: VitNode {version}", + "desc": "Problem dotyczy VitNode {version}. Zaktualizuj jak najszybciej.", + "review": "Zobacz aktualizację" + }, + "panel": { + "title": "Dostępna aktualizacja bezpieczeństwa", + "desc": "Problem dotyczy VitNode {version}. Zaktualizuj do {fixed} jak najszybciej.", + "desc_no_fix": "Problem dotyczy VitNode {version}. Poprawiona wersja nie została jeszcze opublikowana.", + "details": "Szczegóły", + "version": "Wersja", + "released": "wydano ", + "release_notes": "Informacje o wydaniu" + } + }, + "sections": { + "vitnode": { + "version": "Wersja", + "channel": "Kanał aktualizacji", + "auto": "Automatyczny", + "last_checked": "Ostatnie sprawdzenie aktualizacji", + "never": "Nigdy" + }, + "cache": { + "engine": "Silnik", + "redis": "Redis", + "memory": "W pamięci", + "cross_instance": "Czas rzeczywisty między instancjami", + "yes": "Tak", + "no": "Nie" + }, + "services": { + "active": "Aktywna", + "not_configured": "Nieskonfigurowana", + "set_up": "Skonfiguruj" + } + }, + "health": { + "vitnode": { + "subtitle": "Wersja {version}", + "security_update": "Aktualizacja bezpieczeństwa: VitNode {version}", + "update_available": "Dostępny VitNode {version}", + "source_unreachable": "Nie udało się odczytać źródła aktualizacji", + "not_checked": "Aktualizacje nie zostały jeszcze sprawdzone", + "up_to_date": "Działa najnowsza wersja ({version})" + }, + "plugins": { + "subtitle": "{count, plural, one {# zainstalowana wtyczka} few {# zainstalowane wtyczki} many {# zainstalowanych wtyczek} other {# zainstalowanych wtyczek}}", + "ok": "{count, plural, one {Wtyczka jest aktualna} few {Wszystkie # wtyczki są aktualne} many {Wszystkie # wtyczek jest aktualnych} other {Wszystkie # wtyczek jest aktualnych}}", + "problem": { + "security_update": "Aktualizacja bezpieczeństwa", + "version_mismatch": "Wersje API i web się różnią", + "update_available": "Aktualizacja do {version}", + "source_unreachable": "Źródło aktualizacji niedostępne" + }, + "summary": { + "security_update": "{count, plural, one {# wtyczka wymaga aktualizacji bezpieczeństwa} few {# wtyczki wymagają aktualizacji bezpieczeństwa} many {# wtyczek wymaga aktualizacji bezpieczeństwa} other {# wtyczek wymaga aktualizacji bezpieczeństwa}}", + "version_mismatch": "{count, plural, one {# wtyczka ma różne wersje API i web} few {# wtyczki mają różne wersje API i web} many {# wtyczek ma różne wersje API i web} other {# wtyczek ma różne wersje API i web}}", + "update_available": "{count, plural, one {Dostępna # aktualizacja wtyczki} few {Dostępne # aktualizacje wtyczek} many {Dostępnych # aktualizacji wtyczek} other {Dostępnych # aktualizacji wtyczek}}", + "source_unreachable": "{count, plural, one {# źródło aktualizacji wtyczki jest niedostępne} few {# źródła aktualizacji wtyczek są niedostępne} many {# źródeł aktualizacji wtyczek jest niedostępnych} other {# źródeł aktualizacji wtyczek jest niedostępnych}}" + } + }, + "cron": { + "subtitle_adapter": "Działa w procesie", + "subtitle_external": "Uruchamiany z zewnątrz", + "insecure_secret": "CRON_SECRET nie jest ustawiony lub ma wartość domyślną, więc uruchomienia cron są odrzucane", + "stopped": "Harmonogram przestał działać", + "overdue": "{count, plural, one {# zadanie cron jest opóźnione} few {# zadania cron są opóźnione} many {# zadań cron jest opóźnionych} other {# zadań cron jest opóźnionych}}", + "failed_tasks": "{count, plural, one {# zadanie w kolejce zakończyło się błędem} few {# zadania w kolejce zakończyły się błędem} many {# zadań w kolejce zakończyło się błędem} other {# zadań w kolejce zakończyło się błędem}}", + "ok": "{count, plural, one {# zadanie cron zgodnie z harmonogramem} few {# zadania cron zgodnie z harmonogramem} many {# zadań cron zgodnie z harmonogramem} other {# zadań cron zgodnie z harmonogramem}}" + }, + "cache": { + "subtitle_redis": "Redis", + "subtitle_memory": "W pamięci", + "redis_down": "Redis jest niedostępny", + "redis_connected": "Połączono z Redis", + "memory": "Redis nie jest skonfigurowany, więc pamięć podręczna działa w pamięci" + }, + "search": { + "subtitle": "Silnik: {engine}", + "engine_down": "Silnik wyszukiwania {engine} nie odpowiada", + "incomplete": "{count, plural, one {# kolekcja jest niekompletna} few {# kolekcje są niekompletne} many {# kolekcji jest niekompletnych} other {# kolekcji jest niekompletnych}}", + "sync_errors": "{count, plural, one {# niedawny błąd synchronizacji} few {# niedawne błędy synchronizacji} many {# niedawnych błędów synchronizacji} other {# niedawnych błędów synchronizacji}}", + "ok": "{count, plural, one {# zaindeksowany element} few {# zaindeksowane elementy} many {# zaindeksowanych elementów} other {# zaindeksowanych elementów}}" + }, + "services": { + "subtitle": "Skonfigurowano {active} z {total}", + "email": "E-mail", + "storage": "Przechowywanie plików", + "ai": "Modele AI", + "captcha": "Captcha", + "websocket": "WebSocket" + } + }, "actions": { "clear_cache": { "confirm": "Tak, wyczyść pamięć podręczną", @@ -236,7 +368,6 @@ "title": "Czy na pewno chcesz wyczyścić pamięć podręczną?" } }, - "desc": "Sprawdzaj logi, błędy i inne informacje debugowania.", "logs": { "content": "Treść", "created_at": "Utworzono", @@ -265,42 +396,16 @@ "title": "Więcej szczegółów" }, "plugin": "Wtyczka", - "status_code": "Kod statusu", - "title": "Logi systemowe", "type": "Typ", "types": { "debug": "Debug", "error": "Błąd", "warn": "Ostrzeżenie" } - }, - "queue": { - "counts": { - "completed": "Ukończone", - "failed": "Nieudane", - "pending": "Oczekujące", - "processing": "Przetwarzanie" - }, - "empty": "Brak aktywnych zadań w kolejce.", - "list": { - "attempts": "Próby", - "availableAt": "Dostępne od", - "name": "Zadanie", - "queue": "Kolejka", - "status": "Status" - }, - "title": "Zadania w kolejce" - }, - "title": "Panel debugowania" + } }, "global": { "nav": { - "advanced": { - "cron": "Zadania cron", - "queue": "Zadania w kolejce", - "search": "Szukaj", - "title": "Zaawansowane" - }, "core": "Rdzeń", "dashboard": "Pulpit", "staff": { @@ -320,7 +425,8 @@ "home_page": "Strona główna", "log_out": "Wyloguj się", "report_issue": "Zgłoś problem", - "website": "Strona VitNode" + "website": "Strona VitNode", + "debug_status": "{count, plural, one {# problem wymaga uwagi} few {# problemy wymagają uwagi} many {# problemów wymaga uwagi} other {# problemów wymaga uwagi}}" }, "users": { "list": "Lista użytkowników", @@ -330,8 +436,12 @@ "ai": { "title": "Sztuczna Inteligencja (AI)", "overview": "Przegląd", - "actions": "Akcje", - "history": "Historia" + "actions": "Akcje" + }, + "debug": { + "cron": "Zadania cron", + "queue": "Zadania w kolejce", + "search": "Indeks wyszukiwania" } }, "search": { @@ -873,7 +983,98 @@ }, "search": "Szukaj wtyczek...", "title": "Wtyczki", - "version_unknown": "Nieznana wersja" + "version_unknown": "Nieznana wersja", + "core": { + "title": "Rdzeń VitNode", + "version": "Zainstalowana wersja {version}" + }, + "updates": { + "check": { + "button": "Sprawdź aktualizacje", + "success": { + "title": "Sprawdzanie aktualizacji zakończone", + "desc": "Znaleziono {updates, plural, =0 {0 aktualizacji} one {# aktualizację} few {# aktualizacje} many {# aktualizacji} other {# aktualizacji}} i {securityUpdates, plural, =0 {0 aktualizacji bezpieczeństwa} one {# aktualizację bezpieczeństwa} few {# aktualizacje bezpieczeństwa} many {# aktualizacji bezpieczeństwa} other {# aktualizacji bezpieczeństwa}}." + }, + "partial": { + "title": "Nie udało się odczytać niektórych źródeł aktualizacji", + "desc": "Liczba nieudanych źródeł: {count}. Wcześniejsze wyniki dla nich zostały zachowane." + }, + "running": { + "title": "Sprawdzanie już trwa", + "desc": "Trwa inne sprawdzanie - codzienne albo uruchomione przez kogoś innego. Jego wyniki pojawią się tutaj po zakończeniu." + }, + "cooldown": { + "title": "Sprawdzono przed chwilą", + "desc": "Spróbuj ponownie za {seconds} s." + }, + "disabled": { + "title": "Sprawdzanie aktualizacji jest wyłączone", + "desc": "Ta instalacja ustawia updates.enabled na false w konfiguracji API." + }, + "error": { + "title": "Nie udało się rozpocząć sprawdzania", + "desc": "API nie odpowiedziało. Spróbuj ponownie za chwilę." + } + }, + "summary": { + "last_checked": "Ostatnio sprawdzono ", + "disabled": "Sprawdzanie aktualizacji jest wyłączone w tej instalacji.", + "channel": "Kanał: {channel} (ustawiony w konfiguracji API)" + }, + "status": { + "not_checked": "Jeszcze nie sprawdzono", + "unavailable": "Sprawdzanie aktualizacji niedostępne", + "disabled": "Sprawdzanie aktualizacji jest wyłączone", + "up_to_date": "Aktualna", + "update_available": "Dostępna aktualizacja: {version}", + "package_missing": "Brak w manifeście aktualizacji wydawcy", + "channel_missing": "Brak opublikowanych wydań na kanale {channel}", + "unknown_channel": "Zainstalowana wersja nie należy do żadnego znanego kanału. Ustaw updates.channel w konfiguracji API, aby go wybrać.", + "version_unknown": "Nieznana zainstalowana wersja - nie można potwierdzić, że jest aktualna" + }, + "released": "wydana ", + "release_notes": "Informacje o wydaniu", + "compatibility": { + "compatible": "Wydawca deklaruje obsługę rdzenia {range}.", + "incompatible": "Wymaga nowszego rdzenia ({range}); ta instalacja używa {core}.", + "unknown": "Zgodność z Twoją wersją rdzenia jest nieznana - wydawca jej nie zadeklarował.", + "alternative": "{version} to najnowsze wydanie na tym kanale, które deklaruje obsługę Twojego rdzenia." + }, + "stale": "Jeszcze nie sprawdzono dla tego wdrożenia - wyniki pochodzą z danych wydawcy pobranych .", + "failed": "Ostatnie sprawdzanie nie powiodło się: {reason}", + "failed_with_previous": "Ostatnie sprawdzanie nie powiodło się: {reason} Widoczny jest ostatni udany wynik z .", + "checked_at": "Sprawdzono ", + "error": { + "blocked_address": "źródło aktualizacji wskazuje na adres prywatny lub lokalny.", + "http_error": "źródło aktualizacji odpowiedziało kodem HTTP {detail}.", + "invalid_json": "źródło aktualizacji nie jest poprawnym JSON-em.", + "invalid_manifest": "manifest aktualizacji jest nieprawidłowy ({detail}).", + "invalid_url": "adres źródła aktualizacji jest niedozwolony ({detail}).", + "network": "nie udało się połączyć ze źródłem aktualizacji.", + "timeout": "źródło aktualizacji nie odpowiedziało na czas.", + "too_large": "manifest aktualizacji jest większy niż 1 MB.", + "too_many_redirects": "źródło aktualizacji przekierowało zbyt wiele razy.", + "unsupported_schema": "manifest aktualizacji używa formatu nieobsługiwanego przez tę wersję VitNode ({detail})." + }, + "security": { + "label": "Aktualizacje bezpieczeństwa", + "title": "Aktualizacja bezpieczeństwa {id}", + "affects": "Dotyczy zainstalowanej wersji {versions} (zakres {range}).", + "fixed": "Naprawiono w {version}.", + "no_fix": "Poprawiona wersja nie została jeszcze opublikowana.", + "details": "Szczegóły" + }, + "channel": { + "stable": "stabilny", + "rc": "kandydat do wydania", + "canary": "canary" + }, + "side": { + "api": "API", + "frontend": "aplikacja webowa" + }, + "version_on_side": "{version} ({side})" + } } }, "user": { @@ -1406,8 +1607,6 @@ "plugin": "Wtyczka" }, "history": { - "title": "Historia AI", - "desc": "Każda operacja AI: kto ją uruchomił, który model odpowiedział i ile to kosztowało.", "charged_reservation": "Pobrano {amount} (rezerwacja)", "filters": { "all": "Wszystkie", @@ -2752,7 +2951,6 @@ "dismiss": "Odrzuć", "title": "Zadania w tle wymagają skonfigurowanego adaptera cron" }, - "desc": "Silnik wyszukiwania treści i status indeksu.", "healthy": "Działa", "never": "Nigdy", "rebuild": "Przebuduj indeks", @@ -2771,7 +2969,6 @@ "statusHealthy": "Wszystkie systemy działają poprawnie", "statusUnhealthy": "Wyszukiwarka niedostępna" }, - "title": "Szukaj", "unhealthy": "Niedostępny" }, "desc": "Przeszukaj wszystko w społeczności.", @@ -2928,5 +3125,6 @@ "description": "Przeredagowuje zaznaczony tekst: skraca, poprawia, upraszcza albo zmienia ton." } } - } + }, + "@vitnode/core:plugins:can_check_updates": "Sprawdzanie aktualizacji wtyczek i rdzenia na żądanie" } diff --git a/apps/web/src/page-cache.gen.ts b/apps/web/src/page-cache.gen.ts index ac0163d4b3..6ad5252a0f 100644 --- a/apps/web/src/page-cache.gen.ts +++ b/apps/web/src/page-cache.gen.ts @@ -23,11 +23,16 @@ export const pageCacheRoutes: PageCacheRouteTable = [ { path: '/admin/core/staff/moderators/edit/:id' }, { path: '/admin/core/staff/admins/create' }, { path: '/admin/core/staff/moderators/create' }, - { path: '/admin/core/advanced/cron' }, - { path: '/admin/core/advanced/queue' }, - { path: '/admin/core/advanced/search' }, { path: '/admin/core/ai/actions' }, - { path: '/admin/core/ai/history' }, + { path: '/admin/core/debug/ai' }, + { path: '/admin/core/debug/cache' }, + { path: '/admin/core/debug/cron' }, + { path: '/admin/core/debug/logs' }, + { path: '/admin/core/debug/plugins' }, + { path: '/admin/core/debug/queue' }, + { path: '/admin/core/debug/search' }, + { path: '/admin/core/debug/services' }, + { path: '/admin/core/debug/vitnode' }, { path: '/admin/core/staff/admins' }, { path: '/admin/core/staff/moderators' }, { path: '/admin/core/system/files' }, diff --git a/apps/web/src/plugin-inventory.gen.ts b/apps/web/src/plugin-inventory.gen.ts index 51ed5ffbeb..889784b265 100644 --- a/apps/web/src/plugin-inventory.gen.ts +++ b/apps/web/src/plugin-inventory.gen.ts @@ -31,6 +31,7 @@ export const frontendPlugins: FrontendPluginInventory = [ }, license: "MIT", version: "2.0.0-canary.13", + updates: { url: "https://raw.githubusercontent.com/aXenDeveloper/vitnode/update-manifest/updates.json" }, }, }, { @@ -47,6 +48,7 @@ export const frontendPlugins: FrontendPluginInventory = [ }, license: "MIT", version: "2.0.0-canary.13", + updates: null, }, }, { @@ -63,6 +65,7 @@ export const frontendPlugins: FrontendPluginInventory = [ }, license: "MIT", version: "2.0.0-canary.13", + updates: null, }, }, ] diff --git a/package.json b/package.json index a94db3dfbe..f87f1db208 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "scripts": { "release": "tsx ./scripts/bump-version/bump-version.ts", + "release:update-manifest": "tsx ./scripts/update-manifest/publish-update-manifest.ts", "db:migrate": "turbo db:migrate", "db:prepare": "turbo db:prepare", "docker:dev": "turbo docker:dev", diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json index 57a2051b7b..4a740b0da9 100644 --- a/packages/vitnode/package.json +++ b/packages/vitnode/package.json @@ -65,6 +65,7 @@ "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "@types/sanitize-html": "^2.16.2", + "@types/semver": "^7.8.0", "@vitejs/plugin-react": "^6.1.2", "@vitest/coverage-v8": "^5.0.3", "@vitnode/config": "workspace:*", @@ -289,6 +290,7 @@ "react-scan": "^0.5.7", "redis": "^6.3.0", "sanitize-html": "^2.18.0", + "semver": "^7.8.5", "shadcn": "^4.21.4", "sharp": "^0.35.5", "use-debounce": "^10.1.1", diff --git a/packages/vitnode/scripts/update-manifest/merge.test.ts b/packages/vitnode/scripts/update-manifest/merge.test.ts new file mode 100644 index 0000000000..454c046f0b --- /dev/null +++ b/packages/vitnode/scripts/update-manifest/merge.test.ts @@ -0,0 +1,261 @@ +import { describe, expect, it } from "vitest"; + +import type { UpdateManifest } from "../../src/lib/updates/manifest"; + +import { + applySecurityUpdates, + emptyUpdateManifest, + mergeRelease, + parseSecurityUpdatesInput, + readPublishedManifest, + requiresCoreFrom, + serializeUpdateManifest, +} from "./merge"; + +const RELEASE = { + publishedAt: "2026-10-15T10:00:00Z", + url: "https://github.com/acme/vitnode/releases/tag/v2.1.0", +}; + +const SECURITY_UPDATE = { + affected: "<2.1.0", + fixed: "2.1.0", + id: "VN-2026-001", + url: "https://github.com/acme/vitnode/security/advisories/1", +}; + +const stable = ( + manifest: UpdateManifest, + packages: { id: string; requiresCore?: string; version: string }[], + githubRelease: null | typeof RELEASE = RELEASE, +) => mergeRelease(manifest, { channel: "stable", githubRelease, packages }); + +describe("mergeRelease", () => { + it("adds the published packages, with the GitHub Release metadata", () => { + expect( + stable(emptyUpdateManifest(), [ + { id: "@vitnode/core", version: "2.1.0" }, + { id: "@vitnode/blog", requiresCore: "^2.1.0", version: "2.1.0" }, + ]).packages, + ).toEqual({ + "@vitnode/blog": { + securityUpdates: [], + channels: { stable: "2.1.0" }, + releases: { "2.1.0": { ...RELEASE, requiresCore: "^2.1.0" } }, + }, + "@vitnode/core": { + securityUpdates: [], + channels: { stable: "2.1.0" }, + releases: { "2.1.0": RELEASE }, + }, + }); + }); + + it("leaves publishedAt and url out without a GitHub Release", () => { + expect( + stable( + emptyUpdateManifest(), + [{ id: "@vitnode/core", version: "2.1.0" }], + null, + ).packages["@vitnode/core"]?.releases, + ).toEqual({ "2.1.0": {} }); + }); + + it("writes the same bytes when a run is repeated", () => { + const once = stable(emptyUpdateManifest(), [ + { id: "@vitnode/core", version: "2.1.0" }, + ]); + + expect( + serializeUpdateManifest( + stable(once, [{ id: "@vitnode/core", version: "2.1.0" }], { + publishedAt: "2026-10-16T08:00:00Z", + url: "https://github.com/acme/vitnode/releases/tag/rerun", + }), + ), + ).toBe(serializeUpdateManifest(once)); + }); + + it("fills in what a rerun after the GitHub Release learned", () => { + const withoutRelease = stable( + emptyUpdateManifest(), + [{ id: "@vitnode/core", version: "2.1.0" }], + null, + ); + + expect( + stable(withoutRelease, [{ id: "@vitnode/core", version: "2.1.0" }]) + .packages["@vitnode/core"]?.releases["2.1.0"], + ).toEqual(RELEASE); + }); + + it("never moves a channel backwards when an older release is rerun", () => { + const newer = stable(emptyUpdateManifest(), [ + { id: "@vitnode/core", version: "2.1.0" }, + ]); + + const merged = stable(newer, [{ id: "@vitnode/core", version: "2.0.5" }]); + + expect(merged.packages["@vitnode/core"]).toMatchObject({ + channels: { stable: "2.1.0" }, + releases: { "2.0.5": {}, "2.1.0": {} }, + }); + }); + + it("leaves other packages, channels and securityUpdates untouched", () => { + const before: UpdateManifest = { + packages: { + "@acme/other": { + securityUpdates: [], + channels: { stable: "1.0.0" }, + releases: { "1.0.0": {} }, + }, + "@vitnode/core": { + securityUpdates: [SECURITY_UPDATE], + channels: { canary: "2.1.0-canary.1", stable: "2.0.0" }, + releases: { "2.0.0": {}, "2.1.0-canary.1": {} }, + }, + }, + schemaVersion: 1, + }; + + const merged = mergeRelease(before, { + channel: "canary", + githubRelease: null, + packages: [{ id: "@vitnode/core", version: "2.1.0-canary.2" }], + }); + + expect(merged.packages["@acme/other"]).toEqual( + before.packages["@acme/other"], + ); + expect(merged.packages["@vitnode/core"]).toMatchObject({ + securityUpdates: [SECURITY_UPDATE], + channels: { canary: "2.1.0-canary.2", stable: "2.0.0" }, + }); + }); + + it.each([ + [ + "a prerelease on the stable channel", + "stable", + "2.1.0-rc.1", + /does not belong on the stable channel/, + ], + [ + "a stable version on the canary channel", + "canary", + "2.1.0", + /does not belong on the canary channel/, + ], + ["a version that is not SemVer", "stable", "v2.1", /not SemVer/], + ] as const)("rejects %s", (_, channel, version, error) => { + expect(() => + mergeRelease(emptyUpdateManifest(), { + channel, + githubRelease: null, + packages: [{ id: "@vitnode/core", version }], + }), + ).toThrow(error); + }); +}); + +describe("readPublishedManifest", () => { + it.each([null, "", " \n"])("starts a new manifest from %j", text => { + expect(readPublishedManifest(text)).toEqual(emptyUpdateManifest()); + }); + + it.each([ + ["text that is not JSON", "{", /not JSON/], + ["an invalid manifest", '{"schemaVersion":1,"packages":[]}', /invalid/], + ])("refuses to overwrite %s", (_, text, error) => { + expect(() => readPublishedManifest(text)).toThrow(error); + }); +}); + +describe("applySecurityUpdates", () => { + it("gives each package exactly the securityUpdates the file lists, and keeps releases", () => { + const published = stable(emptyUpdateManifest(), [ + { id: "@vitnode/blog", version: "2.1.0" }, + { id: "@vitnode/core", version: "2.1.0" }, + ]); + const withOld = applySecurityUpdates(published, { + securityUpdates: { "@vitnode/blog": [SECURITY_UPDATE] }, + schemaVersion: 1, + }); + + const next = applySecurityUpdates(withOld, { + securityUpdates: { "@vitnode/core": [SECURITY_UPDATE] }, + schemaVersion: 1, + }); + + expect(next.packages["@vitnode/blog"]).toMatchObject({ + securityUpdates: [], + channels: { stable: "2.1.0" }, + }); + expect(next.packages["@vitnode/core"]?.securityUpdates).toEqual([ + SECURITY_UPDATE, + ]); + }); +}); + +describe("parseSecurityUpdatesInput", () => { + it("reads the maintained security updates file", () => { + expect( + parseSecurityUpdatesInput( + JSON.stringify({ + securityUpdates: { "@vitnode/core": [SECURITY_UPDATE] }, + schemaVersion: 1, + }), + ), + ).toEqual({ + securityUpdates: { "@vitnode/core": [SECURITY_UPDATE] }, + schemaVersion: 1, + }); + }); + + it.each([ + ["text that is not JSON", "[", /not JSON/], + [ + "a security update linking over http", + JSON.stringify({ + securityUpdates: { + "@vitnode/core": [{ ...SECURITY_UPDATE, url: "http://x.io" }], + }, + schemaVersion: 1, + }), + /invalid: securityUpdates\.@vitnode\/core\.0\.url/, + ], + ])("rejects %s", (_, text, error) => { + expect(() => parseSecurityUpdatesInput(text)).toThrow(error); + }); +}); + +describe("requiresCoreFrom", () => { + it.each([ + [undefined, undefined], + ["workspace:*", "2.1.0"], + ["workspace:", "2.1.0"], + ["workspace:^", "^2.1.0"], + ["workspace:~", "~2.1.0"], + ["workspace:^2.0.0", "^2.0.0"], + [">=2.0.0", ">=2.0.0"], + ])("turns %s into %s", (spec, expected) => { + expect(requiresCoreFrom(spec, "2.1.0")).toBe(expected); + }); +}); + +describe("serializeUpdateManifest", () => { + it("writes the same bytes whatever order the keys were added in", () => { + const a = stable(emptyUpdateManifest(), [ + { id: "@vitnode/blog", version: "2.1.0" }, + { id: "@vitnode/core", version: "2.1.0" }, + ]); + const b = stable(emptyUpdateManifest(), [ + { id: "@vitnode/core", version: "2.1.0" }, + { id: "@vitnode/blog", version: "2.1.0" }, + ]); + + expect(serializeUpdateManifest(a)).toBe(serializeUpdateManifest(b)); + expect(readPublishedManifest(serializeUpdateManifest(a))).toEqual(a); + }); +}); diff --git a/packages/vitnode/scripts/update-manifest/merge.ts b/packages/vitnode/scripts/update-manifest/merge.ts new file mode 100644 index 0000000000..264aa09c9f --- /dev/null +++ b/packages/vitnode/scripts/update-manifest/merge.ts @@ -0,0 +1,259 @@ +import { gt } from "semver"; +import { z } from "zod"; + +import type { + UpdateChannel, + UpdateManifest, + UpdatePackage, + UpdateRelease, +} from "../../src/lib/updates/manifest"; + +import { + channelOfVersion, + isPackageId, + isStrictVersion, + parseUpdateManifest, + securityUpdateSchema, + UPDATE_MANIFEST_SCHEMA_VERSION, +} from "../../src/lib/updates/manifest"; + +/** + * The pure half of publishing VitNode's own update manifest: everything that + * decides what the JSON says, with no git, GitHub or npm in sight. The release + * workflow feeds it what it published; the security updates workflow feeds it the + * maintained security update list. + */ + +const ERROR_PREFIX = "[update manifest]"; + +export const RELEASE_CHANNELS: Record = { + canary: "canary", + "release-candidate": "rc", + stable: "stable", +}; + +export const channelForReleaseType = (releaseType: string): UpdateChannel => { + const channel = RELEASE_CHANNELS[releaseType]; + if (channel === undefined) { + throw new Error( + `${ERROR_PREFIX} Unknown release type ${JSON.stringify(releaseType)}. Expected one of: ${Object.keys(RELEASE_CHANNELS).join(", ")}.`, + ); + } + + return channel; +}; + +export const emptyUpdateManifest = (): UpdateManifest => ({ + packages: {}, + schemaVersion: UPDATE_MANIFEST_SCHEMA_VERSION, +}); + +/** Reads the published file, or starts a new one. Anything invalid stops the run: it is never overwritten blindly. */ +export const readPublishedManifest = (text: null | string): UpdateManifest => { + if (text === null || text.trim() === "") return emptyUpdateManifest(); + + let value: unknown; + try { + value = JSON.parse(text); + } catch { + throw new Error(`${ERROR_PREFIX} The published updates.json is not JSON.`); + } + + const parsed = parseUpdateManifest(value); + if (!parsed.ok) { + throw new Error( + `${ERROR_PREFIX} The published updates.json is invalid: ${parsed.error}`, + ); + } + + return parsed.manifest; +}; + +/** Stable key order, so a rerun that changes nothing writes the same bytes. */ +export const serializeUpdateManifest = (manifest: UpdateManifest): string => { + const sortKeys = (value: unknown): unknown => { + if (Array.isArray(value)) return value.map(sortKeys); + if (typeof value !== "object" || value === null) return value; + + return Object.fromEntries( + Object.keys(value) + .sort() + .map(key => [key, sortKeys((value as Record)[key])]), + ); + }; + + return `${JSON.stringify( + { + schemaVersion: manifest.schemaVersion, + packages: sortKeys(manifest.packages), + }, + null, + 2, + )}\n`; +}; + +const assertValid = (manifest: UpdateManifest): UpdateManifest => { + const parsed = parseUpdateManifest(JSON.parse(JSON.stringify(manifest))); + if (!parsed.ok) { + throw new Error( + `${ERROR_PREFIX} The merged manifest is invalid: ${parsed.error}`, + ); + } + + return parsed.manifest; +}; + +const emptyPackage = (): UpdatePackage => ({ + securityUpdates: [], + channels: {}, + releases: {}, +}); + +export interface PublishedPackage { + id: string; + /** `requiresCore` for this release, when the package declares one. */ + requiresCore?: string; + version: string; +} + +export interface GitHubReleaseMetadata { + publishedAt: string; + url: string; +} + +/** + * Adds one workflow run's packages to the manifest. + * + * - Only the run's channel moves, and never backwards: rerunning an older + * release leaves a newer channel target alone. + * - A release entry that already exists keeps its `publishedAt` and `url`. A + * rerun after the GitHub Release was created fills in what was missing, and + * changes nothing else. + * - Without a GitHub Release, `publishedAt` and `url` are left out - never + * invented. + * - Other packages, channels, releases and every security update are untouched. + */ +export const mergeRelease = ( + manifest: UpdateManifest, + { + channel, + githubRelease, + packages, + }: { + channel: UpdateChannel; + githubRelease: GitHubReleaseMetadata | null; + packages: readonly PublishedPackage[]; + }, +): UpdateManifest => { + const next: UpdateManifest = structuredClone(manifest); + + for (const published of packages) { + if (!isPackageId(published.id)) { + throw new Error( + `${ERROR_PREFIX} ${JSON.stringify(published.id)} is not a package name.`, + ); + } + if (!isStrictVersion(published.version)) { + throw new Error( + `${ERROR_PREFIX} ${published.id} has the version ${JSON.stringify(published.version)}, which is not SemVer.`, + ); + } + if (channelOfVersion(published.version) !== channel) { + throw new Error( + `${ERROR_PREFIX} ${published.id}@${published.version} does not belong on the ${channel} channel.`, + ); + } + + const entry = next.packages[published.id] ?? emptyPackage(); + const existing = entry.releases[published.version]; + const release: UpdateRelease = { ...existing }; + release.publishedAt ??= githubRelease?.publishedAt; + release.url ??= githubRelease?.url; + release.requiresCore ??= published.requiresCore; + entry.releases[published.version] = Object.fromEntries( + Object.entries(release).filter(([, value]) => value !== undefined), + ); + + const current = entry.channels[channel]; + if (current === undefined || gt(published.version, current)) { + entry.channels[channel] = published.version; + } + + next.packages[published.id] = entry; + } + + return assertValid(next); +}; + +export const securityUpdatesInputSchema = z.strictObject({ + schemaVersion: z.literal(UPDATE_MANIFEST_SCHEMA_VERSION), + securityUpdates: z.record( + z.string().refine(isPackageId, { message: "Expected an npm package name" }), + z.array(securityUpdateSchema), + ), +}); + +export type SecurityUpdatesInput = z.infer; + +export const parseSecurityUpdatesInput = ( + text: string, +): SecurityUpdatesInput => { + let value: unknown; + try { + value = JSON.parse(text); + } catch { + throw new Error(`${ERROR_PREFIX} The security updates file is not JSON.`); + } + + const parsed = securityUpdatesInputSchema.safeParse(value); + if (!parsed.success) { + throw new Error( + `${ERROR_PREFIX} The security updates file is invalid: ${parsed.error.issues + .slice(0, 5) + .map(issue => `${issue.path.join(".")}: ${issue.message}`) + .join("; ")}`, + ); + } + + return parsed.data; +}; + +/** + * The maintained security updates file is the whole truth about security updates: each + * package gets exactly the list it declares, and a package it does not list has + * none. Releases and channels are untouched. + */ +export const applySecurityUpdates = ( + manifest: UpdateManifest, + input: SecurityUpdatesInput, +): UpdateManifest => { + const next: UpdateManifest = structuredClone(manifest); + + for (const entry of Object.values(next.packages)) entry.securityUpdates = []; + for (const [id, securityUpdates] of Object.entries(input.securityUpdates)) { + const entry = next.packages[id] ?? emptyPackage(); + entry.securityUpdates = securityUpdates; + next.packages[id] = entry; + } + + return assertValid(next); +}; + +/** + * `workspace:*` becomes the exact version on publish, `workspace:^` a caret + * range and so on - the same rewrite pnpm does - so a plugin's declared + * dependency on core is what its release says it requires. + */ +export const requiresCoreFrom = ( + spec: string | undefined, + coreVersion: string, +): string | undefined => { + if (spec === undefined) return undefined; + if (!spec.startsWith("workspace:")) return spec; + + const rest = spec.slice("workspace:".length); + if (rest === "*" || rest === "") return coreVersion; + if (rest === "^" || rest === "~") return `${rest}${coreVersion}`; + + return rest; +}; diff --git a/packages/vitnode/scripts/write-plugin-manifest.test.ts b/packages/vitnode/scripts/write-plugin-manifest.test.ts index 82ae3d46f2..c0c63a85cc 100644 --- a/packages/vitnode/scripts/write-plugin-manifest.test.ts +++ b/packages/vitnode/scripts/write-plugin-manifest.test.ts @@ -40,7 +40,7 @@ const writeManifest = (root: string, manifest: Record) => { write(root, "package.json", JSON.stringify(manifest, null, 2)); }; -const writeInfo = (root: string, info: Record) => { +const writeInfo = (root: string, info: Record) => { write( root, PLUGIN_INFO_PATH, @@ -99,10 +99,22 @@ describe("writePluginManifest", () => { id: "@acme/blog", license: null, title, + updates: null, version: "1.0.0", }); }); + it("takes the update source from src/plugin-info.ts", async () => { + const root = pluginAt(BLOG); + writeInfo(root, { updates: { url: "https://example.com/updates.json" } }); + + writePluginManifest(root); + + expect(await generated(root)).toMatchObject({ + updates: { url: "https://example.com/updates.json" }, + }); + }); + it.each([ [ "a new dependency", diff --git a/packages/vitnode/src/api/config.ts b/packages/vitnode/src/api/config.ts index 6a4d84e023..b6f6907cf1 100644 --- a/packages/vitnode/src/api/config.ts +++ b/packages/vitnode/src/api/config.ts @@ -199,6 +199,7 @@ export function VitNodeAPI({ notifications: vitNodeApiConfig.notifications, search: vitNodeApiConfig.search, storage: vitNodeApiConfig.storage, + updates: vitNodeApiConfig.updates, users: vitNodeApiConfig.users, plugins, cacheClient: redisClient, diff --git a/packages/vitnode/src/api/lib/updates/check.test.ts b/packages/vitnode/src/api/lib/updates/check.test.ts new file mode 100644 index 0000000000..28e2bdedce --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/check.test.ts @@ -0,0 +1,379 @@ +import type { drizzle } from "drizzle-orm/postgres-js"; + +import { describe, expect, it, vi } from "vitest"; + +import type { UpdateManifest } from "@/lib/updates/manifest"; + +import { core_update_results, core_update_sources } from "@/database/updates"; +import { createMemoryDb } from "@/tests/memory-db"; + +import type { FetchUpdateSource } from "./check"; +import type { UpdateComponent } from "./components"; + +import { checkForUpdates, runLockedUpdateCheck } from "./check"; +import { UpdateSourceError } from "./fetch-source"; +import { acquireUpdateCheckLease, readUpdateCheckLock } from "./lock"; + +type Db = ReturnType; + +const ACME = "https://acme.example.com/updates.json"; +const CORE = "https://vitnode.example.com/updates.json"; + +const T1 = new Date("2026-10-14T03:00:00Z"); +const T2 = new Date("2026-10-15T03:00:00Z"); + +const component = ( + id: string, + sourceUrl: null | string, + versions: string[] = ["1.0.0"], +): UpdateComponent => ({ + hasUnknownVersion: false, + id, + kind: id === "@vitnode/core" ? "core" : "plugin", + sourceUrl, + versions, +}); + +const SECURITY_UPDATE = { + affected: "<1.1.0", + fixed: "1.1.0", + id: "ACME-1", + url: "https://acme.example.com/advisories/1", +}; + +const acmeManifest = ( + ids: string[] = ["@acme/blog", "@acme/forum"], +): UpdateManifest => ({ + packages: Object.fromEntries( + ids.map(id => [ + id, + { + securityUpdates: [SECURITY_UPDATE], + channels: { stable: "1.1.0" }, + releases: { "1.1.0": {} }, + }, + ]), + ), + schemaVersion: 1, +}); + +const coreManifest: UpdateManifest = { + packages: { + "@vitnode/core": { + securityUpdates: [], + channels: { stable: "2.0.0" }, + releases: { "2.0.0": {} }, + }, + }, + schemaVersion: 1, +}; + +const fetched = (body: unknown, etag: null | string = null) => ({ + body, + etag, + kind: "fetched" as const, + lastModified: null, +}); + +/** Answers per URL; a function answer can look at what was asked. */ +const sources = ( + answers: Record< + string, + | (( + options: Parameters[1], + ) => Awaited>) + | Awaited> + | Error + >, +) => + vi.fn(async (url, options) => { + const answer = answers[url]; + if (answer === undefined) throw new Error(`unexpected fetch of ${url}`); + if (answer instanceof Error) throw answer; + + return await Promise.resolve( + typeof answer === "function" ? answer(options) : answer, + ); + }); + +const setup = () => { + const memory = createMemoryDb(); + const db = memory.db as unknown as Db; + const check = async ( + components: UpdateComponent[], + fetchSource: FetchUpdateSource, + now = T1, + ) => + await checkForUpdates({ + channelOverride: null, + components, + coreVersion: "2.0.0", + db, + fetchSource, + now, + }); + const result = (id: string) => + memory.rows(core_update_results).find(row => row.componentId === id); + const source = (url: string) => + memory.rows(core_update_sources).find(row => row.url === url); + + return { check, db, memory, result, source }; +}; + +const BLOG = component("@acme/blog", ACME); +const FORUM = component("@acme/forum", ACME, ["1.1.0"]); + +describe("checkForUpdates", () => { + it("fetches a source shared by several plugins once, and evaluates each", async () => { + const { check, result } = setup(); + const fetchSource = sources({ [ACME]: fetched(acmeManifest()) }); + + const summary = await check([BLOG, FORUM], fetchSource); + + expect(fetchSource).toHaveBeenCalledTimes(1); + expect(summary).toEqual({ + securityUpdates: 1, + components: 2, + failedSources: 0, + sources: 1, + updates: 1, + }); + expect(result("@acme/blog")).toMatchObject({ + lastError: null, + lastSuccessAt: T1, + result: { + securityUpdates: [{ id: "ACME-1" }], + status: "update_available", + }, + sourceUrl: ACME, + versions: ["1.0.0"], + }); + expect(result("@acme/forum")).toMatchObject({ + result: { securityUpdates: [], status: "up_to_date" }, + }); + }); + + it("stores only the packages this install checks", async () => { + const { check, source } = setup(); + + await check([BLOG], sources({ [ACME]: fetched(acmeManifest(), '"v1"') })); + + expect(source(ACME)).toMatchObject({ etag: '"v1"', lastError: null }); + expect(source(ACME)?.document).toEqual({ + packages: { "@acme/blog": expect.any(Object) as unknown }, + schemaVersion: 1, + }); + }); + + it("skips a component without an update source", async () => { + const { check, result } = setup(); + const fetchSource = sources({}); + + await expect( + check([component("@acme/local", null)], fetchSource), + ).resolves.toMatchObject({ components: 0, sources: 0 }); + expect(fetchSource).not.toHaveBeenCalled(); + expect(result("@acme/local")).toBeUndefined(); + }); + + it("keeps one failing source from affecting the others", async () => { + const { check, result } = setup(); + const core = component("@vitnode/core", CORE, ["1.9.0"]); + + const summary = await check( + [core, BLOG], + sources({ + [ACME]: new UpdateSourceError("timeout"), + [CORE]: fetched(coreManifest), + }), + ); + + expect(summary).toMatchObject({ failedSources: 1, sources: 2, updates: 1 }); + expect(result("@vitnode/core")).toMatchObject({ + lastError: null, + result: { status: "update_available" }, + }); + expect(result("@acme/blog")).toMatchObject({ + lastError: { code: "timeout", detail: null }, + result: null, + }); + }); + + it.each([ + [ + "an unreachable source", + new UpdateSourceError("http_error", "503"), + { code: "http_error", detail: "503" }, + ], + [ + "a crash in the transport", + new Error("socket hang up"), + { code: "network", detail: null }, + ], + [ + "an invalid manifest", + fetched({ packages: [], schemaVersion: 1 }), + { code: "invalid_manifest" }, + ], + [ + "a manifest from a newer VitNode", + fetched({ schemaVersion: 2 }), + { code: "unsupported_schema" }, + ], + ])("keeps the previous findings through %s", async (_, answer, error) => { + const { check, result, source } = setup(); + await check([BLOG], sources({ [ACME]: fetched(acmeManifest()) }), T1); + + await check([BLOG], sources({ [ACME]: answer }), T2); + + expect(result("@acme/blog")).toMatchObject({ + lastAttemptAt: T2, + lastError: error, + lastSuccessAt: T1, + result: { + securityUpdates: [{ id: "ACME-1" }], + status: "update_available", + }, + }); + expect(source(ACME)).toMatchObject({ + document: { packages: { "@acme/blog": {} } }, + lastAttemptAt: T2, + lastError: error, + lastSuccessAt: T1, + }); + }); + + it("answers a 304 from the stored copy, evaluated for what runs now", async () => { + const { check, result } = setup(); + await check( + [BLOG], + sources({ [ACME]: fetched(acmeManifest(), '"v1"') }), + T1, + ); + const fetchSource = sources({ [ACME]: { kind: "not_modified" } }); + + await check([component("@acme/blog", ACME, ["1.1.0"])], fetchSource, T2); + + expect(fetchSource).toHaveBeenCalledWith(ACME, { + validators: { etag: '"v1"', lastModified: null }, + }); + expect(result("@acme/blog")).toMatchObject({ + lastError: null, + lastSuccessAt: T2, + result: { securityUpdates: [], status: "up_to_date" }, + versions: ["1.1.0"], + }); + }); + + it("asks for the full document when the stored copy does not cover a new plugin", async () => { + const { check, result } = setup(); + await check( + [BLOG], + sources({ [ACME]: fetched(acmeManifest(), '"v1"') }), + T1, + ); + const fetchSource = sources({ [ACME]: { kind: "not_modified" } }); + + await check([BLOG, FORUM], fetchSource, T2); + + expect(fetchSource).toHaveBeenCalledWith(ACME, { validators: null }); + expect(result("@acme/forum")).toMatchObject({ + lastError: { code: "http_error" }, + result: null, + }); + }); + + it("forgets removed plugins and sources nothing points to", async () => { + const { check, memory } = setup(); + const core = component("@vitnode/core", CORE, ["2.0.0"]); + await check( + [core, BLOG], + sources({ + [ACME]: fetched(acmeManifest()), + [CORE]: fetched(coreManifest), + }), + T1, + ); + + await check([core], sources({ [CORE]: fetched(coreManifest) }), T2); + + expect( + memory.rows(core_update_results).map(row => row.componentId), + ).toEqual(["@vitnode/core"]); + expect(memory.rows(core_update_sources).map(row => row.url)).toEqual([ + CORE, + ]); + }); + + it("forgets everything when nothing has an update source any more", async () => { + const { check, memory } = setup(); + await check([BLOG], sources({ [ACME]: fetched(acmeManifest()) }), T1); + + await check([component("@acme/blog", null)], sources({}), T2); + + expect(memory.rows(core_update_results)).toEqual([]); + expect(memory.rows(core_update_sources)).toEqual([]); + }); +}); + +describe("runLockedUpdateCheck", () => { + const options = (db: Db, fetchSource: FetchUpdateSource) => ({ + channelOverride: null, + components: [BLOG], + coreVersion: "2.0.0", + db, + fetchSource, + now: T1, + }); + + it("does not check while another instance holds the lease", async () => { + const { db } = setup(); + await acquireUpdateCheckLease(db, { + manual: false, + now: T1, + owner: "other", + }); + const fetchSource = sources({}); + + await expect( + runLockedUpdateCheck({ ...options(db, fetchSource), manual: true }), + ).resolves.toMatchObject({ reason: "running", status: "busy" }); + expect(fetchSource).not.toHaveBeenCalled(); + }); + + it("gives the lease back after the check, even when it throws", async () => { + const { db } = setup(); + const failingDb = new Proxy(db, { + get: (target, property, receiver) => + property === "delete" + ? () => { + throw new Error("database went away"); + } + : (Reflect.get(target, property, receiver) as unknown), + }); + + await expect( + runLockedUpdateCheck({ + ...options(failingDb, sources({ [ACME]: fetched(acmeManifest()) })), + manual: false, + }), + ).rejects.toThrow("database went away"); + await expect(readUpdateCheckLock(db, T1)).resolves.toMatchObject({ + running: false, + }); + }); + + it("returns the summary of a completed check", async () => { + const { db } = setup(); + + await expect( + runLockedUpdateCheck({ + ...options(db, sources({ [ACME]: fetched(acmeManifest()) })), + manual: false, + }), + ).resolves.toMatchObject({ + status: "completed", + summary: { sources: 1, updates: 1 }, + }); + }); +}); diff --git a/packages/vitnode/src/api/lib/updates/check.ts b/packages/vitnode/src/api/lib/updates/check.ts new file mode 100644 index 0000000000..7ae474cf2d --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/check.ts @@ -0,0 +1,350 @@ +import type { drizzle } from "drizzle-orm/postgres-js"; + +import { inArray, notInArray } from "drizzle-orm"; + +import type { UpdateCheckError } from "@/database/updates"; +import type { UpdateChannel, UpdateManifest } from "@/lib/updates/manifest"; + +import { core_update_results, core_update_sources } from "@/database/updates"; +import { parseUpdateManifest } from "@/lib/updates/manifest"; + +import type { UpdateComponent } from "./components"; +import type { + FetchUpdateSourceOptions, + UpdateSourceFetchResult, +} from "./fetch-source"; + +import { evaluateUpdate } from "./evaluate"; +import { fetchUpdateSource, UpdateSourceError } from "./fetch-source"; +import { + acquireUpdateCheckLease, + releaseUpdateCheckLease, + type UpdateCheckLease, +} from "./lock"; + +type Db = ReturnType; + +const SOURCE_CONCURRENCY = 4; + +const MAX_ERROR_DETAIL = 300; + +export type FetchUpdateSource = ( + url: string, + options: Pick, +) => Promise; + +export interface UpdateCheckSummary { + components: number; + failedSources: number; + securityUpdates: number; + sources: number; + updates: number; +} + +type SourceOutcome = + | { document: null | UpdateManifest; error: UpdateCheckError } + | { document: UpdateManifest; error: null }; + +const errorOf = (error: unknown): UpdateCheckError => { + if (error instanceof UpdateSourceError) { + return { code: error.reason, detail: error.detail }; + } + + return { code: "network", detail: null }; +}; + +/** Only the packages this install checks: the stored copy stays small. */ +const pick = ( + manifest: UpdateManifest, + ids: readonly string[], +): UpdateManifest => ({ + packages: Object.fromEntries( + ids.flatMap(id => { + const entry = manifest.packages[id]; + + return entry === undefined ? [] : [[id, entry] as const]; + }), + ), + schemaVersion: manifest.schemaVersion, +}); + +const mapLimited = async ( + items: readonly T[], + limit: number, + run: (item: T) => Promise, +): Promise => { + const results: R[] = Array.from({ length: items.length }); + let next = 0; + const worker = async () => { + while (next < items.length) { + const index = next++; + results[index] = await run(items[index]); + } + }; + await Promise.all( + Array.from({ length: Math.min(limit, items.length) }, worker), + ); + + return results; +}; + +/** + * The one check behind the daily cron job and the AdminCP button. Fetches every + * distinct update source once, validates it, evaluates each component against + * it and stores the outcome. A source that fails keeps its last valid copy - + * and every finding computed from it - and only records the failure, so one + * unreachable publisher neither blocks the others nor turns into "up to date". + */ +export const checkForUpdates = async ({ + channelOverride, + components, + coreVersion, + db, + fetchSource = async (url, options) => await fetchUpdateSource(url, options), + now = new Date(), +}: { + channelOverride: null | UpdateChannel; + components: readonly UpdateComponent[]; + coreVersion: null | string; + db: Db; + fetchSource?: FetchUpdateSource; + now?: Date; +}): Promise => { + const checked = components.filter( + (component): component is UpdateComponent & { sourceUrl: string } => + component.sourceUrl !== null, + ); + const bySource = new Map(); + for (const component of checked) { + bySource.set(component.sourceUrl, [ + ...(bySource.get(component.sourceUrl) ?? []), + component.id, + ]); + } + const urls = [...bySource.keys()]; + + const stored = new Map( + urls.length === 0 + ? [] + : ( + await db + .select() + .from(core_update_sources) + .where(inArray(core_update_sources.url, urls)) + ).map(row => [row.url, row]), + ); + + const outcomes = new Map(); + await mapLimited(urls, SOURCE_CONCURRENCY, async url => { + const previous = stored.get(url); + const ids = bySource.get(url) ?? []; + // A 304 can only stand in for a copy that is stored and still covers every + // package asked about now; otherwise ask for the full document. + const reusable = + previous?.document !== null && + previous?.document !== undefined && + ids.every(id => Object.hasOwn(previous.document?.packages ?? {}, id)) + ? previous.document + : null; + + let outcome: SourceOutcome; + let validators: null | { + etag: null | string; + lastModified: null | string; + } = null; + try { + const response = await fetchSource(url, { + validators: + reusable === null || (!previous?.etag && !previous?.lastModified) + ? null + : { + etag: previous?.etag ?? null, + lastModified: previous?.lastModified ?? null, + }, + }); + + if (response.kind === "not_modified") { + outcome = + reusable === null + ? { + document: previous?.document ?? null, + error: { code: "http_error", detail: "unexpected 304" }, + } + : { document: reusable, error: null }; + validators = { + etag: previous?.etag ?? null, + lastModified: previous?.lastModified ?? null, + }; + } else { + const parsed = parseUpdateManifest(response.body); + if (parsed.ok) { + outcome = { + document: pick(parsed.manifest, ids), + error: null, + }; + validators = { + etag: response.etag, + lastModified: response.lastModified, + }; + } else { + outcome = { + document: previous?.document ?? null, + error: { + code: + parsed.reason === "unsupported_schema" + ? "unsupported_schema" + : "invalid_manifest", + detail: parsed.error.slice(0, MAX_ERROR_DETAIL), + }, + }; + } + } + } catch (error) { + outcome = { document: previous?.document ?? null, error: errorOf(error) }; + } + + outcomes.set(url, outcome); + + if (outcome.error === null) { + const values = { + document: outcome.document, + etag: validators?.etag ?? null, + lastAttemptAt: now, + lastError: null, + lastModified: validators?.lastModified ?? null, + lastSuccessAt: now, + }; + await db + .insert(core_update_sources) + .values({ url, ...values }) + .onConflictDoUpdate({ set: values, target: core_update_sources.url }); + } else { + await db + .insert(core_update_sources) + .values({ lastAttemptAt: now, lastError: outcome.error, url }) + .onConflictDoUpdate({ + set: { lastAttemptAt: now, lastError: outcome.error }, + target: core_update_sources.url, + }); + } + }); + + const summary: UpdateCheckSummary = { + securityUpdates: 0, + components: checked.length, + failedSources: [...outcomes.values()].filter( + outcome => outcome.error !== null, + ).length, + sources: urls.length, + updates: 0, + }; + + for (const component of checked) { + const outcome = outcomes.get(component.sourceUrl); + if (outcome === undefined) continue; + + if (outcome.error !== null) { + // The failure is recorded; what was found before - and the versions it + // was found for - stays exactly as it was. + await db + .insert(core_update_results) + .values({ + channel: null, + componentId: component.id, + coreVersion, + hasUnknownVersion: component.hasUnknownVersion, + lastAttemptAt: now, + lastError: outcome.error, + result: null, + sourceUrl: component.sourceUrl, + versions: component.versions, + }) + .onConflictDoUpdate({ + set: { lastAttemptAt: now, lastError: outcome.error }, + target: core_update_results.componentId, + }); + continue; + } + + const result = evaluateUpdate({ + channelOverride, + coreVersion, + entry: outcome.document.packages[component.id], + installed: component, + isCore: component.kind === "core", + }); + summary.securityUpdates += result.securityUpdates.length; + if (result.status === "update_available") summary.updates += 1; + + const values = { + channel: result.channel, + coreVersion, + hasUnknownVersion: component.hasUnknownVersion, + lastAttemptAt: now, + lastError: null, + lastSuccessAt: now, + result, + sourceUrl: component.sourceUrl, + versions: component.versions, + }; + await db + .insert(core_update_results) + .values({ componentId: component.id, ...values }) + .onConflictDoUpdate({ + set: values, + target: core_update_results.componentId, + }); + } + + // Removed plugins, and plugins that dropped their update source, leave no + // result behind; neither does a source nothing points to any more. + const checkedIds = checked.map(component => component.id); + await db + .delete(core_update_results) + .where( + checkedIds.length === 0 + ? undefined + : notInArray(core_update_results.componentId, checkedIds), + ); + await db + .delete(core_update_sources) + .where( + urls.length === 0 ? undefined : notInArray(core_update_sources.url, urls), + ); + + return summary; +}; + +export type LockedUpdateCheck = + | { + reason: Extract["reason"]; + retryAfterSeconds: number; + status: "busy"; + } + | { status: "completed"; summary: UpdateCheckSummary }; + +/** Takes the shared lease, runs the check and always gives the lease back. */ +export const runLockedUpdateCheck = async ({ + manual, + ...options +}: Parameters[0] & { + manual: boolean; +}): Promise => { + const lease = await acquireUpdateCheckLease(options.db, { + manual, + now: options.now, + }); + if (!lease.acquired) { + return { + reason: lease.reason, + retryAfterSeconds: lease.retryAfterSeconds, + status: "busy", + }; + } + + try { + return { status: "completed", summary: await checkForUpdates(options) }; + } finally { + await releaseUpdateCheckLease(options.db, lease.owner); + } +}; diff --git a/packages/vitnode/src/api/lib/updates/components.test.ts b/packages/vitnode/src/api/lib/updates/components.test.ts new file mode 100644 index 0000000000..0ba155900a --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/components.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it } from "vitest"; + +import type { PluginManifest } from "@/lib/plugin-manifest"; + +import { buildInstalledPluginRegistry } from "../installed-plugins"; +import { collectUpdateComponents } from "./components"; + +const manifest = ( + id: string, + overrides: Partial = {}, +): PluginManifest => ({ + author: null, + description: null, + icon: null, + id, + license: null, + title: id, + version: "1.0.0", + ...overrides, +}); + +const CORE = { + id: "@vitnode/core", + updates: { url: "https://example.com/core.json" }, + version: "2.0.0", +}; + +const collect = ( + api: { manifest?: PluginManifest; pluginId: string }[], + frontend: { id: string; manifest: null | PluginManifest }[], +) => + collectUpdateComponents({ + api, + core: CORE, + frontend, + registry: buildInstalledPluginRegistry({ api, frontend }), + }); + +describe("collectUpdateComponents", () => { + it("lists core first, then each plugin with the versions both sides run", () => { + const blog = (version: string) => + manifest("@acme/blog", { + updates: { url: "https://example.com/acme.json" }, + version, + }); + + expect( + collect( + [{ manifest: blog("1.2.0"), pluginId: "@acme/blog" }], + [{ id: "@acme/blog", manifest: blog("1.1.0") }], + ), + ).toEqual([ + { + hasUnknownVersion: false, + id: "@vitnode/core", + kind: "core", + sourceUrl: "https://example.com/core.json", + versions: ["2.0.0"], + }, + { + hasUnknownVersion: false, + id: "@acme/blog", + kind: "plugin", + sourceUrl: "https://example.com/acme.json", + versions: ["1.2.0", "1.1.0"], + }, + ]); + }); + + it("takes the update source from the API manifest over the web app's", () => { + const [, plugin] = collect( + [ + { + manifest: manifest("@acme/blog", { + updates: { url: "https://api.example.com/updates.json" }, + }), + pluginId: "@acme/blog", + }, + ], + [ + { + id: "@acme/blog", + manifest: manifest("@acme/blog", { + updates: { url: "https://web.example.com/updates.json" }, + }), + }, + ], + ); + + expect(plugin?.sourceUrl).toBe("https://api.example.com/updates.json"); + }); + + it("falls back to the web app's update source", () => { + const [, plugin] = collect( + [{ manifest: manifest("@acme/blog"), pluginId: "@acme/blog" }], + [ + { + id: "@acme/blog", + manifest: manifest("@acme/blog", { + updates: { url: "https://web.example.com/updates.json" }, + }), + }, + ], + ); + + expect(plugin?.sourceUrl).toBe("https://web.example.com/updates.json"); + }); + + it("leaves a plugin without an update source uncheckable, and flags a side with no version", () => { + const [, plugin] = collect( + [{ pluginId: "@acme/blog" }], + [{ id: "@acme/blog", manifest: manifest("@acme/blog") }], + ); + + expect(plugin).toMatchObject({ + hasUnknownVersion: true, + sourceUrl: null, + versions: ["1.0.0"], + }); + }); +}); diff --git a/packages/vitnode/src/api/lib/updates/components.ts b/packages/vitnode/src/api/lib/updates/components.ts new file mode 100644 index 0000000000..81d718e5cd --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/components.ts @@ -0,0 +1,65 @@ +import type { + FrontendPluginInventory, + PluginManifest, +} from "@/lib/plugin-manifest"; + +import type { InstalledPluginRegistry } from "../installed-plugins"; +import type { UpdateComponentVersions } from "./evaluate"; + +import { normalizeInstalledVersions } from "./evaluate"; + +export interface UpdateComponent extends UpdateComponentVersions { + id: string; + kind: "core" | "plugin"; + /** `null` when the package declares no update source: checking is unavailable. */ + sourceUrl: null | string; +} + +/** + * Core first, then every plugin the catalog lists. A plugin's versions are the + * ones its registrations report - API and web app - so a stale side is checked + * too. Its update source comes from the API manifest, else the web app's. + */ +export const collectUpdateComponents = ({ + api, + core, + frontend, + registry, +}: { + api: readonly { manifest?: PluginManifest; pluginId: string }[]; + core: Pick; + frontend: FrontendPluginInventory | undefined; + registry: InstalledPluginRegistry; +}): UpdateComponent[] => { + const sources = new Map(); + for (const entry of frontend ?? []) { + const url = entry.manifest?.updates?.url; + if (url) sources.set(entry.id, url); + } + for (const plugin of api) { + const url = plugin.manifest?.updates?.url; + if (url) sources.set(plugin.pluginId, url); + } + + return [ + { + id: core.id, + kind: "core", + sourceUrl: core.updates?.url ?? null, + ...normalizeInstalledVersions([core.version]), + }, + ...registry.plugins.map(plugin => { + const { api: fromApi, frontend: fromFrontend } = plugin.registrations; + const reported = [fromApi, fromFrontend].flatMap(registration => + registration === null ? [] : [registration.version], + ); + + return { + id: plugin.id, + kind: "plugin" as const, + sourceUrl: sources.get(plugin.id) ?? null, + ...normalizeInstalledVersions(reported), + }; + }), + ]; +}; diff --git a/packages/vitnode/src/api/lib/updates/config.ts b/packages/vitnode/src/api/lib/updates/config.ts new file mode 100644 index 0000000000..1c06cd7d2a --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/config.ts @@ -0,0 +1,24 @@ +import type { VitNodeApiConfig } from "@/vitnode.config"; + +import { UPDATE_CHANNELS, type UpdateChannel } from "@/lib/updates/manifest"; + +export interface ResolvedUpdatesConfig { + channelOverride: null | UpdateChannel; + enabled: boolean; +} + +export const resolveUpdatesConfig = ( + updates: VitNodeApiConfig["updates"], +): ResolvedUpdatesConfig => { + const channel = updates?.channel; + if (channel !== undefined && !UPDATE_CHANNELS.includes(channel)) { + throw new Error( + `[VitNode updates] \`updates.channel\` is ${JSON.stringify(channel)}. Use one of: ${UPDATE_CHANNELS.join(", ")}.`, + ); + } + + return { + channelOverride: channel ?? null, + enabled: updates?.enabled ?? true, + }; +}; diff --git a/packages/vitnode/src/api/lib/updates/context.ts b/packages/vitnode/src/api/lib/updates/context.ts new file mode 100644 index 0000000000..b48dd82d9a --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/context.ts @@ -0,0 +1,21 @@ +import type { Context } from "hono"; + +import type { EnvVitNode } from "@/api/middlewares/global.middleware"; + +import { runLockedUpdateCheck } from "./check"; + +/** The check as the cron job and the AdminCP button both run it: same lease, same service. */ +export const runUpdateCheckFor = async ( + c: Context, + { manual }: { manual: boolean }, +) => { + const { channelOverride, components, coreVersion } = c.get("core").updates; + + return await runLockedUpdateCheck({ + channelOverride, + components, + coreVersion, + db: c.get("db"), + manual, + }); +}; diff --git a/packages/vitnode/src/api/lib/updates/evaluate.test.ts b/packages/vitnode/src/api/lib/updates/evaluate.test.ts new file mode 100644 index 0000000000..19162ffe3a --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/evaluate.test.ts @@ -0,0 +1,271 @@ +import { describe, expect, it } from "vitest"; + +import type { UpdateChannel, UpdatePackage } from "@/lib/updates/manifest"; + +import { evaluateUpdate, normalizeInstalledVersions } from "./evaluate"; + +const ENTRY: UpdatePackage = { + securityUpdates: [], + channels: { canary: "2.2.0-canary.3", rc: "2.2.0-rc.1", stable: "2.1.0" }, + releases: { + "2.0.1": { requiresCore: "^1.0.0" }, + "2.0.2": { requiresCore: "^1.0.0 || ^2.0.0" }, + "2.0.3": {}, + "2.1.0": { + publishedAt: "2026-10-01T10:00:00Z", + requiresCore: "^3.0.0", + url: "https://example.com/releases/2.1.0", + }, + "2.2.0-canary.3": {}, + "2.2.0-rc.1": {}, + }, +}; + +const SECURITY_UPDATE = { + affected: "<2.0.3", + fixed: "2.0.3", + id: "VN-2026-001", + url: "https://example.com/advisories/VN-2026-001", +}; + +const evaluate = ( + reported: (null | string)[], + options: { + channelOverride?: null | UpdateChannel; + coreVersion?: null | string; + entry?: undefined | UpdatePackage; + isCore?: boolean; + } = {}, +) => + evaluateUpdate({ + channelOverride: options.channelOverride ?? null, + coreVersion: + "coreVersion" in options ? (options.coreVersion ?? null) : "2.5.0", + entry: "entry" in options ? options.entry : ENTRY, + installed: normalizeInstalledVersions(reported), + isCore: options.isCore ?? false, + }); + +describe("normalizeInstalledVersions", () => { + it("keeps distinct valid versions, newest first", () => { + expect( + normalizeInstalledVersions(["1.0.0", "v1.2.0", "1.0.0", "1.10.0"]), + ).toEqual({ + hasUnknownVersion: false, + versions: ["1.10.0", "1.2.0", "1.0.0"], + }); + }); + + it.each([ + ["no registration", []], + ["a registration without a version", ["1.0.0", null]], + ["a version that is not SemVer", ["1.0.0", "latest"]], + ])("flags %s as an unknown version", (_, reported) => { + expect(normalizeInstalledVersions(reported).hasUnknownVersion).toBe(true); + }); +}); + +describe("evaluateUpdate", () => { + it.each([ + ["stable", "2.0.0", "2.1.0"], + ["rc", "2.2.0-rc.0", "2.2.0-rc.1"], + ["canary", "2.2.0-canary.1", "2.2.0-canary.3"], + ])( + "follows the %s channel of the installed version", + (channel, installed, target) => { + expect(evaluate([installed])).toMatchObject({ + channel, + latest: { version: target }, + status: "update_available", + }); + }, + ); + + it("follows the configured channel instead of the installed version's", () => { + expect(evaluate(["2.0.0"], { channelOverride: "canary" })).toMatchObject({ + channel: "canary", + latest: { version: "2.2.0-canary.3" }, + status: "update_available", + }); + }); + + it("never offers a downgrade when the installed version is newer than the target", () => { + expect( + evaluate(["2.2.0-canary.9"], { channelOverride: "stable" }), + ).toMatchObject({ + latest: { version: "2.1.0" }, + status: "up_to_date", + }); + }); + + it("is up to date on the channel target, with its release details", () => { + expect(evaluate(["2.1.0"])).toEqual({ + securityUpdates: [], + alternative: null, + channel: "stable", + compatibility: null, + latest: { + publishedAt: "2026-10-01T10:00:00Z", + requiresCore: "^3.0.0", + url: "https://example.com/releases/2.1.0", + version: "2.1.0", + }, + status: "up_to_date", + }); + }); + + it.each([ + [ + "the package is not in the manifest", + ["2.0.0"], + undefined, + "package_missing", + ], + [ + "the channel publishes nothing", + ["2.2.0-rc.0"], + { ...ENTRY, channels: { stable: "2.1.0" } }, + "channel_missing", + ], + [ + "the version is on no known channel", + ["2.2.0-beta.1"], + ENTRY, + "unknown_channel", + ], + ["no version is known", [null], ENTRY, "version_unknown"], + ["one side reports no version", ["2.1.0", null], ENTRY, "version_unknown"], + ])("when %s, says so", (_, reported, entry, status) => { + expect(evaluate(reported, { entry }).status).toBe(status); + }); + + it("reports an update when either side runs an older version", () => { + expect(evaluate(["2.1.0", "2.0.0"]).status).toBe("update_available"); + }); + + describe("securityUpdates", () => { + const withSecurityUpdates = ( + securityUpdates: UpdatePackage["securityUpdates"], + ): UpdatePackage => ({ ...ENTRY, securityUpdates }); + + it("reports a matching securityUpdate with the affected versions", () => { + expect( + evaluate(["2.0.1"], { entry: withSecurityUpdates([SECURITY_UPDATE]) }) + .securityUpdates, + ).toEqual([ + { + affected: "<2.0.3", + fixed: "2.0.3", + id: "VN-2026-001", + url: "https://example.com/advisories/VN-2026-001", + versions: ["2.0.1"], + }, + ]); + }); + + it("reports a vulnerable web app even when the API runs an unaffected version", () => { + const result = evaluate(["2.1.0", "2.0.1"], { + entry: withSecurityUpdates([SECURITY_UPDATE]), + }); + + expect(result.securityUpdates).toMatchObject([{ versions: ["2.0.1"] }]); + }); + + it("reports a vulnerable API even when the web app runs an unaffected version", () => { + const result = evaluate(["2.0.1", "2.0.3"], { + entry: withSecurityUpdates([SECURITY_UPDATE]), + }); + + expect(result.securityUpdates).toMatchObject([{ versions: ["2.0.1"] }]); + }); + + it("matches prereleases inside the affected range", () => { + expect( + evaluate(["2.0.3-canary.1"], { + entry: withSecurityUpdates([SECURITY_UPDATE]), + }).securityUpdates, + ).toHaveLength(1); + }); + + it("reports a security update with no fix yet", () => { + const { fixed: _, ...unfixed } = SECURITY_UPDATE; + + expect( + evaluate(["2.1.0"], { + entry: withSecurityUpdates([{ ...unfixed, affected: "<=2.1.0" }]), + }), + ).toMatchObject({ + securityUpdates: [{ fixed: null, versions: ["2.1.0"] }], + status: "up_to_date", + }); + }); + + it("leaves out a security update no installed version is affected by", () => { + expect( + evaluate(["2.0.3"], { entry: withSecurityUpdates([SECURITY_UPDATE]) }) + .securityUpdates, + ).toEqual([]); + }); + + it("reports securityUpdates whatever the channel says", () => { + const entry: UpdatePackage = { + securityUpdates: [{ ...SECURITY_UPDATE, affected: "<3.0.0" }], + channels: {}, + releases: {}, + }; + + expect(evaluate(["2.2.0-beta.1"], { entry })).toMatchObject({ + securityUpdates: [{ id: "VN-2026-001" }], + status: "unknown_channel", + }); + }); + }); + + describe("compatibility with the running core", () => { + const stableAt = (target: string, release: object): UpdatePackage => ({ + securityUpdates: [], + channels: { stable: target }, + releases: { [target]: release }, + }); + + it.each([ + ["compatible", "^2.0.0", "2.5.0"], + ["incompatible", "^3.0.0", "2.5.0"], + ["unknown", undefined, "2.5.0"], + ["unknown", "^2.0.0", null], + ])( + "is %s for requiresCore %s on core %s", + (compatibility, requiresCore, coreVersion) => { + expect( + evaluate(["1.0.0"], { + coreVersion, + entry: stableAt("1.1.0", requiresCore ? { requiresCore } : {}), + }), + ).toMatchObject({ alternative: null, compatibility }); + }, + ); + + it("offers the newest older release that declares it works with this core", () => { + expect(evaluate(["2.0.0"])).toMatchObject({ + alternative: { requiresCore: "^1.0.0 || ^2.0.0", version: "2.0.2" }, + compatibility: "incompatible", + latest: { version: "2.1.0" }, + }); + }); + + it("offers no alternative when no newer release fits this core", () => { + expect(evaluate(["2.0.2"])).toMatchObject({ + alternative: null, + compatibility: "incompatible", + }); + }); + + it("does not judge core's own updates against itself", () => { + expect(evaluate(["2.0.0"], { isCore: true })).toMatchObject({ + alternative: null, + compatibility: null, + status: "update_available", + }); + }); + }); +}); diff --git a/packages/vitnode/src/api/lib/updates/evaluate.ts b/packages/vitnode/src/api/lib/updates/evaluate.ts new file mode 100644 index 0000000000..0318d6fa7b --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/evaluate.ts @@ -0,0 +1,213 @@ +import { gt, lte, rcompare, satisfies, valid } from "semver"; + +import type { + UpdateChannel, + UpdatePackage, + UpdateRelease, +} from "@/lib/updates/manifest"; + +import { channelOfVersion, versionInRange } from "@/lib/updates/manifest"; + +export type UpdateStatus = + /** The package exists, but publishes nothing on the selected channel. */ + | "channel_missing" + /** The publisher has no entry for this package. */ + | "package_missing" + /** The installed version is on no known channel, and no override names one. */ + | "unknown_channel" + | "up_to_date" + | "update_available" + /** No newer release, but a registration reports no valid version, so "up to date" would be a guess. */ + | "version_unknown"; + +export type UpdateCompatibility = "compatible" | "incompatible" | "unknown"; + +export interface UpdateReleaseInfo { + publishedAt: null | string; + requiresCore: null | string; + url: null | string; + version: string; +} + +export interface SecurityUpdateFinding { + affected: string; + fixed: null | string; + id: string; + url: string; + /** The installed versions inside `affected` - the API's, the web app's, or both. */ + versions: string[]; +} + +export interface UpdateEvaluation { + /** The newest release on the channel that declares it works with the running core, when `latest` does not. */ + alternative: null | UpdateReleaseInfo; + channel: null | UpdateChannel; + /** Of `latest` with the running core. `null` for core itself and when there is no newer release. */ + compatibility: null | UpdateCompatibility; + latest: null | UpdateReleaseInfo; + securityUpdates: SecurityUpdateFinding[]; + status: UpdateStatus; +} + +export interface UpdateComponentVersions { + /** A registration reported no version, or one that is not SemVer. */ + hasUnknownVersion: boolean; + /** Distinct valid versions, newest first. */ + versions: string[]; +} + +/** Valid, distinct, newest first; anything else only flips `hasUnknownVersion`. */ +export const normalizeInstalledVersions = ( + reported: readonly (null | string)[], +): UpdateComponentVersions => { + const versions = new Set(); + let hasUnknownVersion = reported.length === 0; + + for (const value of reported) { + const normalized = value === null ? null : valid(value); + if (normalized === null) { + hasUnknownVersion = true; + } else { + versions.add(normalized); + } + } + + return { hasUnknownVersion, versions: [...versions].sort(rcompare) }; +}; + +const releaseInfo = ( + version: string, + release: undefined | UpdateRelease, +): UpdateReleaseInfo => ({ + publishedAt: release?.publishedAt ?? null, + requiresCore: release?.requiresCore ?? null, + url: release?.url ?? null, + version, +}); + +const compatibilityOf = ( + release: UpdateReleaseInfo, + coreVersion: null | string, +): UpdateCompatibility => { + if (release.requiresCore === null || coreVersion === null) return "unknown"; + + return satisfies(coreVersion, release.requiresCore, { + includePrerelease: true, + }) + ? "compatible" + : "incompatible"; +}; + +/** + * Only from releases the publisher listed, on the same channel, newer than what + * runs, no newer than the channel target, and explicitly declaring a + * `requiresCore` the running core satisfies. A release that says nothing about + * core is not offered as "compatible". + */ +const compatibleAlternative = ( + entry: UpdatePackage, + channel: UpdateChannel, + newestInstalled: string, + target: string, + coreVersion: null | string, +): null | UpdateReleaseInfo => { + if (coreVersion === null) return null; + + const candidates = Object.keys(entry.releases) + .filter( + version => + channelOfVersion(version) === channel && + gt(version, newestInstalled) && + lte(version, target), + ) + .sort(rcompare); + + for (const version of candidates) { + const info = releaseInfo(version, entry.releases[version]); + if (compatibilityOf(info, coreVersion) === "compatible") return info; + } + + return null; +}; + +const securityUpdateFindings = ( + entry: undefined | UpdatePackage, + versions: readonly string[], +): SecurityUpdateFinding[] => + (entry?.securityUpdates ?? []).flatMap(securityUpdate => { + const affected = versions.filter(version => + versionInRange(version, securityUpdate.affected), + ); + if (affected.length === 0) return []; + + return [ + { + affected: securityUpdate.affected, + fixed: securityUpdate.fixed ?? null, + id: securityUpdate.id, + url: securityUpdate.url, + versions: affected, + }, + ]; + }); + +export const evaluateUpdate = ({ + channelOverride, + coreVersion, + entry, + installed, + isCore, +}: { + channelOverride: null | UpdateChannel; + /** The running core version, for `requiresCore`. */ + coreVersion: null | string; + /** This package's entry in its publisher's manifest; `undefined` when absent. */ + entry: undefined | UpdatePackage; + installed: UpdateComponentVersions; + isCore: boolean; +}): UpdateEvaluation => { + const [newest] = [...installed.versions].sort(rcompare); + const channel = + channelOverride ?? (newest === undefined ? null : channelOfVersion(newest)); + // Security updates never depend on channels or releases: a vulnerable build is + // reported even when its publisher stopped shipping that channel. + const securityUpdates = securityUpdateFindings(entry, installed.versions); + const base = { + securityUpdates, + alternative: null, + channel, + compatibility: null, + latest: null, + }; + + if (entry === undefined) return { ...base, status: "package_missing" }; + if (newest === undefined) return { ...base, status: "version_unknown" }; + if (channel === null) return { ...base, status: "unknown_channel" }; + + const target = entry.channels[channel]; + if (target === undefined) return { ...base, status: "channel_missing" }; + + const latest = releaseInfo(target, entry.releases[target]); + const outdated = installed.versions.some(version => gt(target, version)); + + if (!outdated) { + return { + ...base, + latest, + status: installed.hasUnknownVersion ? "version_unknown" : "up_to_date", + }; + } + + const compatibility = isCore ? null : compatibilityOf(latest, coreVersion); + + return { + ...base, + alternative: + compatibility === "incompatible" + ? compatibleAlternative(entry, channel, newest, target, coreVersion) + : null, + compatibility, + latest, + status: "update_available", + }; +}; diff --git a/packages/vitnode/src/api/lib/updates/fetch-source.test.ts b/packages/vitnode/src/api/lib/updates/fetch-source.test.ts new file mode 100644 index 0000000000..d7d5758270 --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/fetch-source.test.ts @@ -0,0 +1,314 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { + FetchUpdateSourceOptions, + UpdateSourceTransport, + UpdateSourceTransportResponse, +} from "./fetch-source"; + +import { fetchUpdateSource } from "./fetch-source"; + +const MANIFEST = { packages: {}, schemaVersion: 1 }; + +const reply = ( + status: number, + { + body = new Uint8Array(), + contentLength = null, + destroy = () => undefined, + etag = null, + lastModified = null, + location = null, + }: Partial> & { + body?: Uint8Array; + } = {}, +): UpdateSourceTransportResponse => ({ + body: (async function* chunks() { + await Promise.resolve(); + yield body; + })(), + contentLength, + destroy, + etag, + lastModified, + location, + status, +}); + +const json = (value: unknown, extra: Parameters[1] = {}) => + reply(200, { + body: new TextEncoder().encode(JSON.stringify(value)), + ...extra, + }); + +const HOSTS: Record = { + "cdn.example.com": ["93.184.215.14"], + "mixed.example.com": ["93.184.215.14", "10.0.0.5"], + "private.example.com": ["192.168.1.10"], + "updates.example.com": ["93.184.215.14"], +}; + +const resolveHost = async (hostname: string) => + await Promise.resolve(HOSTS[hostname] ?? []); + +/** A transport that answers each request in turn, and records what was asked. */ +const scripted = (...responses: UpdateSourceTransportResponse[]) => { + const requests: { headers: Record; url: string }[] = []; + const transport: UpdateSourceTransport = async (url, { headers }) => { + requests.push({ headers, url: url.href }); + const next = responses.shift(); + if (!next) throw new Error("no more responses"); + + return await Promise.resolve(next); + }; + + return { requests, transport }; +}; + +const fetchWith = async ( + url: string, + transport: UpdateSourceTransport, + options: Omit = {}, +) => await fetchUpdateSource(url, { resolveHost, transport, ...options }); + +const URL_OK = "https://updates.example.com/updates.json"; + +describe("fetchUpdateSource", () => { + it("returns the JSON body and its validators, sending no cookie or authorization", async () => { + const { requests, transport } = scripted( + json(MANIFEST, { + etag: '"v1"', + lastModified: "Wed, 14 Oct 2026 10:00:00 GMT", + }), + ); + + await expect(fetchWith(URL_OK, transport)).resolves.toEqual({ + body: MANIFEST, + etag: '"v1"', + kind: "fetched", + lastModified: "Wed, 14 Oct 2026 10:00:00 GMT", + }); + expect(requests).toEqual([ + { + headers: { + accept: "application/json", + "user-agent": "VitNode update check", + }, + url: URL_OK, + }, + ]); + }); + + it("asks conditionally when validators are given, and reads a 304 as not modified", async () => { + const { requests, transport } = scripted(reply(304)); + + await expect( + fetchWith(URL_OK, transport, { + validators: { + etag: '"v1"', + lastModified: "Wed, 14 Oct 2026 10:00:00 GMT", + }, + }), + ).resolves.toEqual({ kind: "not_modified" }); + expect(requests[0]?.headers).toMatchObject({ + "if-modified-since": "Wed, 14 Oct 2026 10:00:00 GMT", + "if-none-match": '"v1"', + }); + }); + + it("refuses a 304 it did not ask for", async () => { + const { transport } = scripted(reply(304)); + + await expect(fetchWith(URL_OK, transport)).rejects.toMatchObject({ + reason: "http_error", + }); + }); + + it.each([ + ["http", "http://updates.example.com/updates.json", "invalid_url"], + [ + "a port other than 443", + "https://updates.example.com:8443/u.json", + "invalid_url", + ], + [ + "credentials", + "https://acme:secret@updates.example.com/u.json", + "invalid_url", + ], + ["not a URL", "updates.json", "invalid_url"], + ["loopback", "https://127.0.0.1/updates.json", "blocked_address"], + ["IPv6 loopback", "https://[::1]/updates.json", "blocked_address"], + ["a private network", "https://10.1.2.3/updates.json", "blocked_address"], + [ + "the cloud metadata endpoint", + "https://169.254.169.254/latest", + "blocked_address", + ], + ["a single-label host", "https://intranet/updates.json", "blocked_address"], + [ + "a .localhost host", + "https://app.localhost/updates.json", + "blocked_address", + ], + [ + "a host resolving to a private address", + "https://private.example.com/u.json", + "blocked_address", + ], + [ + "a host resolving to a private address among public ones", + "https://mixed.example.com/u.json", + "blocked_address", + ], + [ + "a host that does not resolve", + "https://nowhere.example.com/u.json", + "blocked_address", + ], + ])("refuses %s without connecting", async (_, url, reason) => { + const transport = vi.fn(); + + await expect(fetchWith(url, transport)).rejects.toMatchObject({ reason }); + expect(transport).not.toHaveBeenCalled(); + }); + + it("follows a relative redirect on the same host", async () => { + const { requests, transport } = scripted( + reply(302, { location: "/v2/updates.json" }), + json(MANIFEST), + ); + + await expect(fetchWith(URL_OK, transport)).resolves.toMatchObject({ + body: MANIFEST, + }); + expect(requests.map(request => request.url)).toEqual([ + URL_OK, + "https://updates.example.com/v2/updates.json", + ]); + }); + + it.each([ + ["a private host", "https://private.example.com/u.json", "blocked_address"], + [ + "the cloud metadata endpoint", + "https://169.254.169.254/latest", + "blocked_address", + ], + ["http", "http://cdn.example.com/u.json", "invalid_url"], + ])("refuses a redirect to %s", async (_, location, reason) => { + const { requests, transport } = scripted(reply(301, { location })); + + await expect(fetchWith(URL_OK, transport)).rejects.toMatchObject({ + reason, + }); + expect(requests).toHaveLength(1); + }); + + it("stops after the redirect limit", async () => { + const { requests, transport } = scripted( + reply(302, { location: "/1" }), + reply(302, { location: "/2" }), + reply(302, { location: "/3" }), + ); + + await expect( + fetchWith(URL_OK, transport, { maxRedirects: 2 }), + ).rejects.toMatchObject({ reason: "too_many_redirects" }); + expect(requests).toHaveLength(3); + }); + + it.each([ + ["a redirect without a location", reply(302), "302"], + ["a server error", reply(503), "503"], + ["a missing manifest", reply(404), "404"], + ])("reports %s as an HTTP error", async (_, response, detail) => { + const { transport } = scripted(response); + + await expect(fetchWith(URL_OK, transport)).rejects.toMatchObject({ + detail, + reason: "http_error", + }); + }); + + it("refuses a declared size over the limit before reading the body", async () => { + const destroy = vi.fn(); + const { transport } = scripted(reply(200, { contentLength: 11, destroy })); + + await expect( + fetchWith(URL_OK, transport, { maxBytes: 10 }), + ).rejects.toMatchObject({ reason: "too_large" }); + expect(destroy).toHaveBeenCalled(); + }); + + it("stops reading a body that grows past the limit without declaring its size", async () => { + const destroy = vi.fn(); + const { transport } = scripted( + reply(200, { body: new Uint8Array(11), destroy }), + ); + + await expect( + fetchWith(URL_OK, transport, { maxBytes: 10 }), + ).rejects.toMatchObject({ reason: "too_large" }); + expect(destroy).toHaveBeenCalled(); + }); + + it.each([ + ["text that is not JSON", new TextEncoder().encode("")], + ["bytes that are not UTF-8", new Uint8Array([0x7b, 0xff, 0x7d])], + ])("reports %s as invalid JSON", async (_, body) => { + const { transport } = scripted(reply(200, { body })); + + await expect(fetchWith(URL_OK, transport)).rejects.toMatchObject({ + reason: "invalid_json", + }); + }); + + it("times out a server that never answers", async () => { + const transport: UpdateSourceTransport = async (_, { signal }) => + await new Promise((_resolve, reject) => { + signal.addEventListener("abort", () => { + reject(signal.reason as Error); + }); + }); + + await expect( + fetchWith(URL_OK, transport, { timeoutMs: 5 }), + ).rejects.toMatchObject({ reason: "timeout" }); + }); + + it("times out a body that stalls", async () => { + const transport: UpdateSourceTransport = async (_, { signal }) => + await Promise.resolve({ + ...reply(200), + body: (async function* stalled() { + yield new TextEncoder().encode("{"); + await new Promise((_resolve, reject) => { + signal.addEventListener("abort", () => { + reject(signal.reason as Error); + }); + }); + })(), + }); + + await expect( + fetchWith(URL_OK, transport, { timeoutMs: 5 }), + ).rejects.toMatchObject({ reason: "timeout" }); + }); + + it.each([ + [ + "a connection the connect-time lookup blocked", + Object.assign(new Error("blocked"), { reason: "blocked_address" }), + "blocked_address", + ], + ["a refused connection", new Error("ECONNREFUSED"), "network"], + ])("reports %s", async (_, error, reason) => { + const transport: UpdateSourceTransport = async () => + await Promise.reject(error); + + await expect(fetchWith(URL_OK, transport)).rejects.toMatchObject({ + reason, + }); + }); +}); diff --git a/packages/vitnode/src/api/lib/updates/fetch-source.ts b/packages/vitnode/src/api/lib/updates/fetch-source.ts new file mode 100644 index 0000000000..ac0573aa01 --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/fetch-source.ts @@ -0,0 +1,315 @@ +import type { LookupFunction } from "node:net"; + +import { lookup as dnsLookup } from "node:dns"; +import { request as httpsRequest } from "node:https"; +import { isIP } from "node:net"; + +import { isPublicIpAddress, publicOnlyLookup } from "../remote-image"; + +export const UPDATE_SOURCE_MAX_BYTES = 1024 * 1024; + +const DEFAULT_TIMEOUT_MS = 10_000; + +const DEFAULT_MAX_REDIRECTS = 3; + +const MAX_URL_LENGTH = 2048; + +export type UpdateSourceFailure = + | "blocked_address" + | "http_error" + | "invalid_json" + | "invalid_url" + | "network" + | "timeout" + | "too_large" + | "too_many_redirects"; + +export class UpdateSourceError extends Error { + constructor(reason: UpdateSourceFailure, detail?: string) { + super(`Update source rejected: ${reason}${detail ? ` (${detail})` : ""}`); + this.name = "UpdateSourceError"; + this.reason = reason; + this.detail = detail ?? null; + } + + readonly detail: null | string; + readonly reason: UpdateSourceFailure; +} + +export interface UpdateSourceTransportResponse { + body: AsyncIterable; + contentLength: null | number; + destroy: () => void; + etag: null | string; + lastModified: null | string; + location: null | string; + status: number; +} + +/** + * The network seam. The default sends a plain GET over HTTPS with only the + * headers it is given - never a cookie or an authorization header, since + * nothing of the request that triggered the check is passed down here. + */ +export type UpdateSourceTransport = ( + url: URL, + options: { + headers: Record; + lookup: LookupFunction; + signal: AbortSignal; + }, +) => Promise; + +export type ResolveUpdateHost = (hostname: string) => Promise; + +const header = (value: string | string[] | undefined): null | string => + (Array.isArray(value) ? value[0] : value) ?? null; + +export const httpsUpdateSourceTransport: UpdateSourceTransport = async ( + url, + { headers, lookup, signal }, +) => + await new Promise((resolve, reject) => { + const request = httpsRequest( + url, + { agent: false, headers, lookup, method: "GET", signal }, + response => { + const length = Number(response.headers["content-length"]); + signal.addEventListener("abort", () => response.destroy(), { + once: true, + }); + + resolve({ + body: response, + contentLength: Number.isFinite(length) ? length : null, + destroy: () => response.destroy(), + etag: header(response.headers.etag), + lastModified: header(response.headers["last-modified"]), + location: header(response.headers.location), + status: response.statusCode ?? 0, + }); + }, + ); + + request.on("error", reject); + request.end(); + }); + +const resolveHostAddresses: ResolveUpdateHost = async hostname => + await new Promise((resolve, reject) => { + dnsLookup(hostname, { all: true }, (error, addresses) => { + if (error) { + reject(new UpdateSourceError("network", "DNS lookup failed")); + + return; + } + + resolve(addresses.map(entry => entry.address)); + }); + }); + +const parseUrl = (raw: string, base?: URL): URL => { + if (raw.length > MAX_URL_LENGTH) throw new UpdateSourceError("invalid_url"); + + try { + return new URL(raw, base); + } catch { + throw new UpdateSourceError("invalid_url"); + } +}; + +/** + * HTTPS on the default port, a public host name, and every address it resolves + * to public too. The same check runs again at connect time through + * `publicOnlyLookup`, so a name that changes its answer in between still cannot + * reach loopback, a private network or a cloud metadata endpoint. + */ +const assertFetchableUrl = async ( + url: URL, + resolveHost: ResolveUpdateHost, +): Promise => { + if (url.protocol !== "https:") { + throw new UpdateSourceError("invalid_url", "not https"); + } + if (url.username || url.password) throw new UpdateSourceError("invalid_url"); + if (url.port && url.port !== "443") { + throw new UpdateSourceError("invalid_url", "only port 443 is allowed"); + } + + const hostname = url.hostname.replace(/^\[|\]$/g, ""); + if (isIP(hostname) !== 0) { + if (!isPublicIpAddress(hostname)) { + throw new UpdateSourceError("blocked_address"); + } + + return; + } + if (!hostname.includes(".") || hostname.endsWith(".localhost")) { + throw new UpdateSourceError("blocked_address"); + } + + const addresses = await resolveHost(hostname); + if ( + addresses.length === 0 || + addresses.some(address => !isPublicIpAddress(address)) + ) { + throw new UpdateSourceError("blocked_address"); + } +}; + +const readCapped = async ( + body: AsyncIterable, + maxBytes: number, +): Promise => { + const chunks: Uint8Array[] = []; + let size = 0; + + for await (const chunk of body) { + size += chunk.byteLength; + if (size > maxBytes) throw new UpdateSourceError("too_large"); + chunks.push(chunk); + } + + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + + return bytes; +}; + +const transportFailure = ( + error: unknown, + signal: AbortSignal, +): UpdateSourceError => { + if (error instanceof UpdateSourceError) return error; + if (signal.aborted) return new UpdateSourceError("timeout"); + if ( + typeof error === "object" && + error !== null && + "reason" in error && + error.reason === "blocked_address" + ) { + return new UpdateSourceError("blocked_address"); + } + + return new UpdateSourceError("network"); +}; + +export interface UpdateSourceValidators { + etag: null | string; + lastModified: null | string; +} + +export type UpdateSourceFetchResult = + | (UpdateSourceValidators & { body: unknown; kind: "fetched" }) + | { kind: "not_modified" }; + +export interface FetchUpdateSourceOptions { + maxBytes?: number; + maxRedirects?: number; + resolveHost?: ResolveUpdateHost; + timeoutMs?: number; + transport?: UpdateSourceTransport; + /** Sent as `If-None-Match` / `If-Modified-Since`. Only pass them when a validated copy is stored. */ + validators?: null | UpdateSourceValidators; +} + +export const fetchUpdateSource = async ( + rawUrl: string, + { + maxBytes = UPDATE_SOURCE_MAX_BYTES, + maxRedirects = DEFAULT_MAX_REDIRECTS, + resolveHost = resolveHostAddresses, + timeoutMs = DEFAULT_TIMEOUT_MS, + transport = httpsUpdateSourceTransport, + validators = null, + }: FetchUpdateSourceOptions = {}, +): Promise => { + const signal = AbortSignal.timeout(timeoutMs); + const headers: Record = { + accept: "application/json", + "user-agent": "VitNode update check", + }; + if (validators?.etag) headers["if-none-match"] = validators.etag; + if (validators?.lastModified) { + headers["if-modified-since"] = validators.lastModified; + } + + let url = parseUrl(rawUrl); + + for (let hop = 0; hop <= maxRedirects; hop++) { + await assertFetchableUrl(url, resolveHost); + if (signal.aborted) throw new UpdateSourceError("timeout"); + + let response: UpdateSourceTransportResponse; + try { + response = await transport(url, { + headers, + lookup: publicOnlyLookup, + signal, + }); + } catch (error) { + throw transportFailure(error, signal); + } + + if (response.status === 304) { + response.destroy(); + if (!headers["if-none-match"] && !headers["if-modified-since"]) { + throw new UpdateSourceError( + "http_error", + "304 without a request for it", + ); + } + + return { kind: "not_modified" }; + } + + if (response.status >= 300 && response.status < 400) { + response.destroy(); + if (!response.location) { + throw new UpdateSourceError("http_error", String(response.status)); + } + url = parseUrl(response.location, url); + continue; + } + + if (response.status !== 200) { + response.destroy(); + throw new UpdateSourceError("http_error", String(response.status)); + } + + if (response.contentLength !== null && response.contentLength > maxBytes) { + response.destroy(); + throw new UpdateSourceError("too_large"); + } + + let bytes: Uint8Array; + try { + bytes = await readCapped(response.body, maxBytes); + } catch (error) { + response.destroy(); + throw transportFailure(error, signal); + } + + let body: unknown; + try { + body = JSON.parse( + new TextDecoder("utf-8", { fatal: true }).decode(bytes), + ); + } catch { + throw new UpdateSourceError("invalid_json"); + } + + return { + body, + etag: response.etag, + kind: "fetched", + lastModified: response.lastModified, + }; + } + + throw new UpdateSourceError("too_many_redirects"); +}; diff --git a/packages/vitnode/src/api/lib/updates/lock.test.ts b/packages/vitnode/src/api/lib/updates/lock.test.ts new file mode 100644 index 0000000000..6cc03d8cd8 --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/lock.test.ts @@ -0,0 +1,160 @@ +import type { drizzle } from "drizzle-orm/postgres-js"; + +import { describe, expect, it } from "vitest"; + +import { core_update_lock } from "@/database/updates"; +import { createMemoryDb } from "@/tests/memory-db"; + +import { + acquireUpdateCheckLease, + MANUAL_UPDATE_CHECK_COOLDOWN_MS, + readUpdateCheckLock, + releaseUpdateCheckLease, + UPDATE_CHECK_LEASE_MS, +} from "./lock"; + +type Db = ReturnType; + +const T0 = new Date("2026-10-15T12:00:00Z"); +const at = (ms: number) => new Date(T0.getTime() + ms); + +const memoryDb = () => { + const memory = createMemoryDb(); + + return { ...memory, db: memory.db as unknown as Db }; +}; + +describe("acquireUpdateCheckLease", () => { + it("lets only one of two racing checks take the lease", async () => { + const { db } = memoryDb(); + + const results = await Promise.all([ + acquireUpdateCheckLease(db, { manual: false, now: T0, owner: "a" }), + acquireUpdateCheckLease(db, { manual: false, now: T0, owner: "b" }), + ]); + + expect(results.filter(result => result.acquired)).toHaveLength(1); + expect(results.find(result => !result.acquired)).toEqual({ + acquired: false, + reason: "running", + retryAfterSeconds: UPDATE_CHECK_LEASE_MS / 1000, + }); + }); + + it("takes over a lease whose holder died and let it expire", async () => { + const { db } = memoryDb(); + await acquireUpdateCheckLease(db, { manual: false, now: T0, owner: "a" }); + + await expect( + acquireUpdateCheckLease(db, { + manual: false, + now: at(UPDATE_CHECK_LEASE_MS), + owner: "b", + }), + ).resolves.toEqual({ acquired: true, owner: "b" }); + }); + + it("is free again once the holder gives it back", async () => { + const { db } = memoryDb(); + await acquireUpdateCheckLease(db, { manual: false, now: T0, owner: "a" }); + await releaseUpdateCheckLease(db, "a"); + + await expect( + acquireUpdateCheckLease(db, { manual: false, now: at(1000), owner: "b" }), + ).resolves.toEqual({ acquired: true, owner: "b" }); + }); + + it("keeps manual checks apart by the cooldown, but not the daily one", async () => { + const { db } = memoryDb(); + await acquireUpdateCheckLease(db, { manual: true, now: T0, owner: "a" }); + await releaseUpdateCheckLease(db, "a"); + + await expect( + acquireUpdateCheckLease(db, { + manual: true, + now: at(30_000), + owner: "b", + }), + ).resolves.toEqual({ + acquired: false, + reason: "cooldown", + retryAfterSeconds: MANUAL_UPDATE_CHECK_COOLDOWN_MS / 1000 - 30, + }); + await expect( + acquireUpdateCheckLease(db, { + manual: false, + now: at(30_000), + owner: "c", + }), + ).resolves.toEqual({ acquired: true, owner: "c" }); + }); + + it("allows a manual check again once the cooldown is over", async () => { + const { db } = memoryDb(); + await acquireUpdateCheckLease(db, { manual: true, now: T0, owner: "a" }); + await releaseUpdateCheckLease(db, "a"); + + await expect( + acquireUpdateCheckLease(db, { + manual: true, + now: at(MANUAL_UPDATE_CHECK_COOLDOWN_MS), + owner: "b", + }), + ).resolves.toEqual({ acquired: true, owner: "b" }); + }); + + it("does not let the daily check reset the manual cooldown", async () => { + const { db } = memoryDb(); + await acquireUpdateCheckLease(db, { manual: true, now: T0, owner: "a" }); + await releaseUpdateCheckLease(db, "a"); + await acquireUpdateCheckLease(db, { + manual: false, + now: at(10_000), + owner: "b", + }); + await releaseUpdateCheckLease(db, "b"); + + await expect(readUpdateCheckLock(db, at(20_000))).resolves.toEqual({ + lastManualAt: T0, + running: false, + }); + }); + + it("reports a running check to a manual request still in cooldown", async () => { + const { db } = memoryDb(); + await acquireUpdateCheckLease(db, { manual: true, now: T0, owner: "a" }); + + await expect( + acquireUpdateCheckLease(db, { manual: true, now: at(1000), owner: "b" }), + ).resolves.toMatchObject({ acquired: false, reason: "running" }); + }); +}); + +describe("releaseUpdateCheckLease", () => { + it("only lets the holder give the lease back", async () => { + const { db } = memoryDb(); + await acquireUpdateCheckLease(db, { manual: false, now: T0, owner: "a" }); + + await expect(releaseUpdateCheckLease(db, "b")).resolves.toBe(false); + await expect(readUpdateCheckLock(db, at(1000))).resolves.toMatchObject({ + running: true, + }); + await expect(releaseUpdateCheckLease(db, "a")).resolves.toBe(true); + await expect(readUpdateCheckLock(db, at(1000))).resolves.toMatchObject({ + running: false, + }); + }); + + it("cannot give back a lease another instance took over after expiry", async () => { + const { db, rows } = memoryDb(); + await acquireUpdateCheckLease(db, { manual: false, now: T0, owner: "a" }); + await acquireUpdateCheckLease(db, { + manual: false, + now: at(UPDATE_CHECK_LEASE_MS + 1), + owner: "b", + }); + + await expect(releaseUpdateCheckLease(db, "a")).resolves.toBe(false); + expect(rows(core_update_lock)).toMatchObject([{ owner: "b" }]); + }); +}); diff --git a/packages/vitnode/src/api/lib/updates/lock.ts b/packages/vitnode/src/api/lib/updates/lock.ts new file mode 100644 index 0000000000..83fe836965 --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/lock.ts @@ -0,0 +1,152 @@ +import type { drizzle } from "drizzle-orm/postgres-js"; + +import { and, eq, isNull, lte, or, sql } from "drizzle-orm"; +import { randomUUID } from "node:crypto"; + +import { core_update_lock } from "@/database/updates"; + +type Db = ReturnType; + +const LOCK_NAME = "check"; + +/** Longer than a check can take: every source has a 10 s timeout and they run four at a time. */ +export const UPDATE_CHECK_LEASE_MS = 5 * 60_000; + +export const MANUAL_UPDATE_CHECK_COOLDOWN_MS = 2 * 60_000; + +export type UpdateCheckLease = + | { acquired: false; reason: "cooldown"; retryAfterSeconds: number } + | { acquired: false; reason: "running"; retryAfterSeconds: number } + | { acquired: true; owner: string }; + +const secondsUntil = (date: Date | null, now: Date): number => + date === null + ? 1 + : Math.max(1, Math.ceil((date.getTime() - now.getTime()) / 1000)); + +/** + * One statement, so two instances racing for the lease cannot both win: the + * insert creates the row, and the conflict branch only takes it over when no + * one holds it or the holder's lease ran out. A manual check also has to be + * past the cooldown, and stamps it in the same statement. + */ +export const acquireUpdateCheckLease = async ( + db: Db, + { + cooldownMs = MANUAL_UPDATE_CHECK_COOLDOWN_MS, + leaseMs = UPDATE_CHECK_LEASE_MS, + manual, + now = new Date(), + owner = randomUUID(), + }: { + cooldownMs?: number; + leaseMs?: number; + manual: boolean; + now?: Date; + owner?: string; + }, +): Promise => { + const expiresAt = new Date(now.getTime() + leaseMs); + const free = or( + isNull(core_update_lock.owner), + isNull(core_update_lock.expiresAt), + lte(core_update_lock.expiresAt, now), + ); + const cooledDown = or( + isNull(core_update_lock.lastManualAt), + lte(core_update_lock.lastManualAt, new Date(now.getTime() - cooldownMs)), + ); + + const [taken] = await db + .insert(core_update_lock) + .values({ + expiresAt, + lastManualAt: manual ? now : null, + name: LOCK_NAME, + owner, + }) + .onConflictDoUpdate({ + set: { + expiresAt, + lastManualAt: manual ? now : sql`${core_update_lock.lastManualAt}`, + owner, + }, + setWhere: manual ? and(free, cooledDown) : free, + target: core_update_lock.name, + }) + .returning({ owner: core_update_lock.owner }); + + if (taken?.owner === owner) return { acquired: true, owner }; + + const [row] = await db + .select() + .from(core_update_lock) + .where(eq(core_update_lock.name, LOCK_NAME)); + + if ( + row?.owner !== null && + row?.owner !== undefined && + row.expiresAt !== null && + row.expiresAt > now + ) { + return { + acquired: false, + reason: "running", + retryAfterSeconds: secondsUntil(row.expiresAt, now), + }; + } + + return { + acquired: false, + reason: "cooldown", + retryAfterSeconds: secondsUntil( + row?.lastManualAt + ? new Date(row.lastManualAt.getTime() + cooldownMs) + : null, + now, + ), + }; +}; + +/** Only the instance that holds the lease can give it back. */ +export const releaseUpdateCheckLease = async ( + db: Db, + owner: string, +): Promise => { + const released = await db + .update(core_update_lock) + .set({ expiresAt: null, owner: null }) + .where( + and( + eq(core_update_lock.name, LOCK_NAME), + eq(core_update_lock.owner, owner), + ), + ) + .returning({ name: core_update_lock.name }); + + return released.length > 0; +}; + +export interface UpdateCheckLockState { + lastManualAt: Date | null; + running: boolean; +} + +export const readUpdateCheckLock = async ( + db: Db, + now: Date = new Date(), +): Promise => { + const [row] = await db + .select() + .from(core_update_lock) + .where(eq(core_update_lock.name, LOCK_NAME)); + + return { + lastManualAt: row?.lastManualAt ?? null, + running: + row?.owner !== null && + row?.owner !== undefined && + row.expiresAt !== null && + row.expiresAt > now, + }; +}; diff --git a/packages/vitnode/src/api/lib/updates/status.test.ts b/packages/vitnode/src/api/lib/updates/status.test.ts new file mode 100644 index 0000000000..c99c25dccf --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/status.test.ts @@ -0,0 +1,224 @@ +import type { drizzle } from "drizzle-orm/postgres-js"; + +import { describe, expect, it } from "vitest"; + +import type { UpdateChannel, UpdateManifest } from "@/lib/updates/manifest"; + +import { createMemoryDb } from "@/tests/memory-db"; + +import type { UpdateComponent } from "./components"; + +import { checkForUpdates } from "./check"; +import { UpdateSourceError } from "./fetch-source"; +import { acquireUpdateCheckLease } from "./lock"; +import { loadUpdatesOverview } from "./status"; + +type Db = ReturnType; + +const ACME = "https://acme.example.com/updates.json"; +const T1 = new Date("2026-10-14T03:00:00Z"); +const T2 = new Date("2026-10-15T03:00:00Z"); + +const MANIFEST: UpdateManifest = { + packages: { + "@acme/blog": { + securityUpdates: [ + { + affected: "<1.1.0", + fixed: "1.1.0", + id: "ACME-1", + url: "https://acme.example.com/advisories/1", + }, + ], + channels: { canary: "1.2.0-canary.1", stable: "1.1.0" }, + releases: { "1.1.0": {}, "1.2.0-canary.1": {} }, + }, + }, + schemaVersion: 1, +}; + +const blog = (overrides: Partial = {}): UpdateComponent => ({ + hasUnknownVersion: false, + id: "@acme/blog", + kind: "plugin", + sourceUrl: ACME, + versions: ["1.0.0"], + ...overrides, +}); + +/** A database holding one successful check of `blog()` on core 2.0.0. */ +const checked = async () => { + const db = createMemoryDb().db as unknown as Db; + await checkForUpdates({ + channelOverride: null, + components: [blog()], + coreVersion: "2.0.0", + db, + fetchSource: async () => + await Promise.resolve({ + body: MANIFEST, + etag: null, + kind: "fetched" as const, + lastModified: null, + }), + now: T1, + }); + + return db; +}; + +const overview = async ( + db: Db, + { + channelOverride = null, + components = [blog()], + coreVersion = "2.0.0", + enabled = true, + }: { + channelOverride?: null | UpdateChannel; + components?: UpdateComponent[]; + coreVersion?: string; + enabled?: boolean; + } = {}, +) => + await loadUpdatesOverview({ + channelOverride, + components, + coreVersion, + db, + enabled, + now: T2, + }); + +describe("loadUpdatesOverview", () => { + it("shows the stored result while nothing changed", async () => { + const db = await checked(); + + const { components } = await overview(db); + + expect(components).toEqual([ + { + id: "@acme/blog", + lastAttemptAt: T1, + lastError: null, + lastSuccessAt: T1, + result: expect.objectContaining({ + securityUpdates: [expect.objectContaining({ id: "ACME-1" })], + status: "update_available", + }) as unknown, + sourceUrl: ACME, + stale: false, + state: "checked", + }, + ]); + }); + + it("re-evaluates a redeployed plugin from the stored manifest and marks it stale", async () => { + const db = await checked(); + + const [state] = ( + await overview(db, { components: [blog({ versions: ["1.1.0"] })] }) + ).components; + + expect(state).toMatchObject({ + lastSuccessAt: T1, + result: { securityUpdates: [], status: "up_to_date" }, + stale: true, + state: "checked", + }); + }); + + it.each([ + ["the channel changed", { channelOverride: "canary" as const }], + ["core was upgraded", { coreVersion: "2.1.0" }], + [ + "a side lost its version", + { components: [blog({ hasUnknownVersion: true })] }, + ], + ])("marks the result stale when %s", async (_, change) => { + const db = await checked(); + + const [state] = (await overview(db, change)).components; + + expect(state?.stale).toBe(true); + }); + + it("does not reuse a result checked against another update source", async () => { + const db = await checked(); + + const [state] = ( + await overview(db, { + components: [ + blog({ sourceUrl: "https://elsewhere.example.com/u.json" }), + ], + }) + ).components; + + expect(state).toMatchObject({ + result: null, + stale: false, + state: "not_checked", + }); + }); + + it("keeps the previous result next to a failed check", async () => { + const db = await checked(); + await checkForUpdates({ + channelOverride: null, + components: [blog()], + coreVersion: "2.0.0", + db, + fetchSource: async () => + await Promise.reject(new UpdateSourceError("timeout")), + now: T2, + }); + + const [state] = (await overview(db)).components; + + expect(state).toMatchObject({ + lastAttemptAt: T2, + lastError: { code: "timeout" }, + lastSuccessAt: T1, + result: { securityUpdates: [{ id: "ACME-1" }] }, + stale: false, + state: "checked", + }); + }); + + it.each([ + [ + "a component without an update source", + blog({ sourceUrl: null }), + "unavailable", + ], + ["a component never checked", blog({ id: "@acme/forum" }), "not_checked"], + ])("shows %s as such", async (_, component, state) => { + const db = await checked(); + + const [entry] = (await overview(db, { components: [component] })) + .components; + + expect(entry).toMatchObject({ result: null, state }); + }); + + it("shows nothing as checkable while checks are turned off", async () => { + const db = await checked(); + + const result = await overview(db, { enabled: false }); + + expect(result).toMatchObject({ + components: [{ result: null, state: "unavailable" }], + enabled: false, + }); + }); + + it("reports a running check and the last manual one", async () => { + const db = await checked(); + await acquireUpdateCheckLease(db, { manual: true, now: T2, owner: "a" }); + + await expect(overview(db)).resolves.toMatchObject({ + lastManualAt: T2, + running: true, + }); + }); +}); diff --git a/packages/vitnode/src/api/lib/updates/status.ts b/packages/vitnode/src/api/lib/updates/status.ts new file mode 100644 index 0000000000..c8b1ac1d5a --- /dev/null +++ b/packages/vitnode/src/api/lib/updates/status.ts @@ -0,0 +1,151 @@ +import type { drizzle } from "drizzle-orm/postgres-js"; + +import type { UpdateCheckError } from "@/database/updates"; +import type { UpdateChannel } from "@/lib/updates/manifest"; + +import { core_update_results, core_update_sources } from "@/database/updates"; +import { channelOfVersion } from "@/lib/updates/manifest"; + +import type { UpdateComponent } from "./components"; +import type { UpdateEvaluation } from "./evaluate"; + +import { evaluateUpdate } from "./evaluate"; +import { readUpdateCheckLock } from "./lock"; + +type Db = ReturnType; + +export interface ComponentUpdateState { + id: string; + lastAttemptAt: Date | null; + /** The last attempt for this component failed. Whatever `result` holds is from before it. */ + lastError: null | UpdateCheckError; + lastSuccessAt: Date | null; + result: null | UpdateEvaluation; + sourceUrl: null | string; + /** + * `true` when the result was not checked for what runs now - a new version, + * channel or core. It is recomputed from the publisher data stored by the + * last successful check, so a security warning still shows, but no fresh + * check has confirmed it yet. + */ + stale: boolean; + state: "checked" | "not_checked" | "unavailable"; +} + +export interface UpdatesOverview { + channelOverride: null | UpdateChannel; + components: ComponentUpdateState[]; + enabled: boolean; + lastManualAt: Date | null; + running: boolean; +} + +const sameVersions = (a: readonly string[], b: readonly string[]): boolean => + a.length === b.length && a.every((version, index) => version === b[index]); + +/** + * What the catalog shows. Reads stored results only - it never reaches out to + * a publisher, so opening the page costs two small queries. + */ +export const loadUpdatesOverview = async ({ + channelOverride, + components, + coreVersion, + db, + enabled, + now = new Date(), +}: { + channelOverride: null | UpdateChannel; + components: readonly UpdateComponent[]; + coreVersion: null | string; + db: Db; + enabled: boolean; + now?: Date; +}): Promise => { + const lock = await readUpdateCheckLock(db, now); + const unavailable = (component: UpdateComponent): ComponentUpdateState => ({ + id: component.id, + lastAttemptAt: null, + lastError: null, + lastSuccessAt: null, + result: null, + sourceUrl: component.sourceUrl, + stale: false, + state: "unavailable", + }); + + if (!enabled) { + return { + channelOverride, + components: components.map(unavailable), + enabled, + lastManualAt: lock.lastManualAt, + running: lock.running, + }; + } + + const [results, sources] = await Promise.all([ + db.select().from(core_update_results), + db.select().from(core_update_sources), + ]); + const resultById = new Map(results.map(row => [row.componentId, row])); + const sourceByUrl = new Map(sources.map(row => [row.url, row])); + + return { + channelOverride, + components: components.map((component): ComponentUpdateState => { + if (component.sourceUrl === null) return unavailable(component); + + const row = resultById.get(component.id); + const notChecked: ComponentUpdateState = { + id: component.id, + lastAttemptAt: row?.lastAttemptAt ?? null, + lastError: row?.lastError ?? null, + lastSuccessAt: null, + result: null, + sourceUrl: component.sourceUrl, + stale: false, + state: "not_checked", + }; + // A result for another source answers a different question. + if (row?.sourceUrl !== component.sourceUrl || row.result === null) { + return notChecked; + } + + const [newest] = component.versions; + const channel = + channelOverride ?? + (newest === undefined ? null : channelOfVersion(newest)); + const current = + sameVersions(row.versions, component.versions) && + row.hasUnknownVersion === component.hasUnknownVersion && + row.coreVersion === coreVersion && + row.channel === channel; + const checked = { + ...notChecked, + lastSuccessAt: row.lastSuccessAt, + state: "checked" as const, + }; + + if (current) return { ...checked, result: row.result }; + + const document = sourceByUrl.get(component.sourceUrl)?.document; + if (!document) return notChecked; + + return { + ...checked, + result: evaluateUpdate({ + channelOverride, + coreVersion, + entry: document.packages[component.id], + installed: component, + isCore: component.kind === "core", + }), + stale: true, + }; + }), + enabled, + lastManualAt: lock.lastManualAt, + running: lock.running, + }; +}; diff --git a/packages/vitnode/src/api/middlewares/global.middleware.ts b/packages/vitnode/src/api/middlewares/global.middleware.ts index 6bed7b1f9f..4104ad083a 100644 --- a/packages/vitnode/src/api/middlewares/global.middleware.ts +++ b/packages/vitnode/src/api/middlewares/global.middleware.ts @@ -8,6 +8,7 @@ import type { RegisteredContentModel } from "@/content/server/model"; import type { LocaleRouting } from "@/lib/i18n/locale-routing"; import type { LocaleConfig, MessagesSource } from "@/lib/i18n/types"; import type { NavigationPreset } from "@/lib/navigation"; +import type { UpdateChannel } from "@/lib/updates/manifest"; import type { PersonalInformationFields } from "@/lib/user-personal-information"; import type { VitNodeApiConfig, VitNodeConfig } from "@/vitnode.config"; import type { VitNodeRealtime } from "@/ws/registry"; @@ -44,6 +45,7 @@ import { collectLocaleCodes } from "@/lib/i18n/load-messages"; import { localeRoutingFromConfig } from "@/lib/i18n/locale-routing"; import { buildApiMessagesSources } from "@/lib/i18n/sources"; import { resolvePersonalInformationFields } from "@/lib/user-personal-information"; +import { manifest as coreManifest } from "@/manifest"; import { realtime } from "@/ws/registry"; import type { AiActionRegistry } from "../lib/ai/registry"; @@ -57,6 +59,7 @@ import type { NotificationRegistry } from "../lib/notifications/registry"; import type { ResolvedPasskeysConfig } from "../lib/passkey-config"; import type { PermissionStaffCatalogEntry } from "../lib/permission-staff"; import type { BuildQueueTaskReturn } from "../lib/queue"; +import type { UpdateComponent } from "../lib/updates/components"; import type { WebSocketConfig } from "../lib/websocket"; import type { EventsApiPlugin } from "../models/events"; import type { @@ -85,6 +88,8 @@ import { resolveNotificationWorkerSettings } from "../lib/notifications/preferen import { createNotificationRegistry } from "../lib/notifications/registry"; import { resolvePasskeysConfig } from "../lib/passkey-config"; import { normalizePermissionStaffModules } from "../lib/permission-staff"; +import { collectUpdateComponents } from "../lib/updates/components"; +import { resolveUpdatesConfig } from "../lib/updates/config"; declare module "hono" { // oxlint-disable-next-line typescript/no-empty-object-type @@ -182,6 +187,13 @@ export interface EnvVariablesVitNode { search: { adapter: SearchProviderApiPlugin }; searchIndexers: SearchIndexerConfig[]; storage?: VitNodeApiConfig["storage"]; + /** Core and every catalog plugin, with the update source and versions to check. */ + updates: { + channelOverride: null | UpdateChannel; + components: UpdateComponent[]; + coreVersion: null | string; + enabled: boolean; + }; webSockets: WebSocketConfig[]; }; db: Pick["dbProvider"]; @@ -237,6 +249,7 @@ export const globalMiddleware = ({ notifications, search, storage, + updates, users, cacheClient, }: Pick< @@ -255,6 +268,7 @@ export const globalMiddleware = ({ | "plugins" | "search" | "storage" + | "updates" | "users" > & Pick & { @@ -268,6 +282,17 @@ export const globalMiddleware = ({ api: plugins, frontend: frontendPlugins, }); + const updatesConfig = resolveUpdatesConfig(updates); + const updatesMetadata = { + ...updatesConfig, + components: collectUpdateComponents({ + api: plugins, + core: coreManifest, + frontend: frontendPlugins, + registry: installedPlugins, + }), + coreVersion: coreManifest.version, + }; // Resolved once at boot: the packages installed here can't change per // request. With no `i18n` block the locale list is whatever the installed @@ -533,6 +558,7 @@ export const globalMiddleware = ({ hasCronAdapter: !!cron, plugins: pluginsMetadata, installedPlugins, + updates: updatesMetadata, cron: cronMetadata, editablePages: editablePagesMetadata, queue: queueMetadata, diff --git a/packages/vitnode/src/api/modules/admin/plugins/plugins.admin.module.ts b/packages/vitnode/src/api/modules/admin/plugins/plugins.admin.module.ts index c31eadd91b..837591c3d7 100644 --- a/packages/vitnode/src/api/modules/admin/plugins/plugins.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/plugins/plugins.admin.module.ts @@ -1,8 +1,14 @@ import { buildModule } from "@/api/builders"; +import { checkPluginUpdatesAdminRoute } from "./routes/check-updates.route"; import { listPluginsAdminRoute } from "./routes/list.route"; +import { pluginUpdatesAdminRoute } from "./routes/updates.route"; export const pluginsAdminModule = buildModule({ name: "plugins", - routes: [listPluginsAdminRoute], + routes: [ + listPluginsAdminRoute, + pluginUpdatesAdminRoute, + checkPluginUpdatesAdminRoute, + ], }); diff --git a/packages/vitnode/src/api/modules/admin/plugins/routes/check-updates.route.test.ts b/packages/vitnode/src/api/modules/admin/plugins/routes/check-updates.route.test.ts new file mode 100644 index 0000000000..12f95b6e95 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/plugins/routes/check-updates.route.test.ts @@ -0,0 +1,136 @@ +import type { drizzle } from "drizzle-orm/postgres-js"; +import type { Context } from "hono"; + +import { OpenAPIHono } from "@hono/zod-openapi"; +import { describe, expect, it } from "vitest"; + +import type { PermissionsStaffArgs } from "@/api/lib/permission-staff"; + +import { + acquireUpdateCheckLease, + releaseUpdateCheckLease, +} from "@/api/lib/updates/lock"; +import { createTestCache } from "@/tests/cache"; +import { createMemoryDb } from "@/tests/memory-db"; +import { grantStaffPermissions } from "@/tests/staff-permissions"; + +import { checkPluginUpdatesAdminRoute } from "./check-updates.route"; + +type Db = ReturnType; + +const ADMIN = { id: 1, roleId: 1 }; + +const CAN_CHECK: PermissionsStaffArgs[] = [ + { module: "plugins", permission: "can_view", plugin: "@vitnode/core" }, + { + module: "plugins", + permission: "can_check_updates", + plugin: "@vitnode/core", + }, +]; + +const checkAs = async ( + permissions: PermissionsStaffArgs[], + { + enabled = true, + prepare, + }: { enabled?: boolean; prepare?: (db: Db) => Promise } = {}, +) => { + const cache = createTestCache(); + await grantStaffPermissions(cache, { permissions, userId: ADMIN.id }); + const db = createMemoryDb().db as unknown as Db; + await prepare?.(db); + + const app = new OpenAPIHono(); + app.use("*", async (c, next) => { + c.set("admin", { user: ADMIN } as unknown as Context["var"]["admin"]); + c.set("cache", cache); + c.set("db", db); + c.set("core", { + updates: { + channelOverride: null, + // No update sources: the check runs end to end without the network. + components: [ + { + hasUnknownVersion: false, + id: "@vitnode/core", + kind: "core", + sourceUrl: null, + versions: ["2.0.0"], + }, + ], + coreVersion: "2.0.0", + enabled, + }, + } as unknown as Context["var"]["core"]); + await next(); + }); + app.openapi( + checkPluginUpdatesAdminRoute.route, + checkPluginUpdatesAdminRoute.handler, + ); + + return await app.request("/updates/check", { method: "POST" }); +}; + +describe("POST /admin/plugins/updates/check", () => { + it("runs the check for an administrator allowed to", async () => { + const response = await checkAs(CAN_CHECK); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + securityUpdates: 0, + components: 0, + failedSources: 0, + sources: 0, + status: "completed", + updates: 0, + }); + }); + + it("refuses an administrator who can only view plugins", async () => { + const response = await checkAs(CAN_CHECK.slice(0, 1)); + + expect(response.status).toBe(403); + }); + + it.each([ + [ + "a check is already running", + { + prepare: async (db: Db) => { + await acquireUpdateCheckLease(db, { manual: false, owner: "cron" }); + }, + }, + 409, + "running", + ], + [ + "a manual check ran moments ago", + { + prepare: async (db: Db) => { + await acquireUpdateCheckLease(db, { manual: true, owner: "earlier" }); + await releaseUpdateCheckLease(db, "earlier"); + }, + }, + 429, + "cooldown", + ], + ])("answers %s with a Retry-After", async (_, options, status, body) => { + const response = await checkAs(CAN_CHECK, options); + + expect(response.status).toBe(status); + expect(await response.json()).toMatchObject({ status: body }); + expect(Number(response.headers.get("Retry-After"))).toBeGreaterThan(0); + }); + + it("answers 409 while update checks are turned off", async () => { + const response = await checkAs(CAN_CHECK, { enabled: false }); + + expect(response.status).toBe(409); + expect(await response.json()).toEqual({ + retryAfterSeconds: null, + status: "disabled", + }); + }); +}); diff --git a/packages/vitnode/src/api/modules/admin/plugins/routes/check-updates.route.ts b/packages/vitnode/src/api/modules/admin/plugins/routes/check-updates.route.ts new file mode 100644 index 0000000000..0acc48b643 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/plugins/routes/check-updates.route.ts @@ -0,0 +1,80 @@ +import { z } from "zod"; + +import { buildRoute } from "@/api/builders"; +import { runUpdateCheckFor } from "@/api/lib/updates/context"; + +const busySchema = z.object({ + status: z.enum(["cooldown", "disabled", "running"]), + retryAfterSeconds: z.number().nullable(), +}); + +export const checkPluginUpdatesAdminRoute = buildRoute({ + adminStaffPermission: { + module: "plugins", + permission: "can_check_updates", + }, + route: { + method: "post", + description: + "Check core and every installed plugin for updates and security updates now. Shares its lease with the daily cron job.", + path: "/updates/check", + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + status: z.literal("completed"), + sources: z.number(), + failedSources: z.number(), + components: z.number(), + updates: z.number(), + securityUpdates: z.number(), + }), + }, + }, + description: "The check finished", + }, + 403: { description: "Access Denied" }, + 409: { + content: { "application/json": { schema: busySchema } }, + description: "A check is already running, or checks are turned off", + }, + 429: { + content: { "application/json": { schema: busySchema } }, + description: "A manual check ran moments ago", + }, + }, + }, + handler: async c => { + if (!c.get("core").updates.enabled) { + return c.json( + { retryAfterSeconds: null, status: "disabled" as const }, + 409, + ); + } + + const outcome = await runUpdateCheckFor(c, { manual: true }); + + if (outcome.status === "completed") { + return c.json({ ...outcome.summary, status: "completed" as const }, 200); + } + + c.header("Retry-After", String(outcome.retryAfterSeconds)); + + return outcome.reason === "running" + ? c.json( + { + retryAfterSeconds: outcome.retryAfterSeconds, + status: "running" as const, + }, + 409, + ) + : c.json( + { + retryAfterSeconds: outcome.retryAfterSeconds, + status: "cooldown" as const, + }, + 429, + ); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/plugins/routes/updates.route.test.ts b/packages/vitnode/src/api/modules/admin/plugins/routes/updates.route.test.ts new file mode 100644 index 0000000000..536cf7354f --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/plugins/routes/updates.route.test.ts @@ -0,0 +1,114 @@ +import type { drizzle } from "drizzle-orm/postgres-js"; +import type { Context } from "hono"; + +import { OpenAPIHono } from "@hono/zod-openapi"; +import { describe, expect, it } from "vitest"; + +import type { PermissionsStaffArgs } from "@/api/lib/permission-staff"; +import type { UpdateComponent } from "@/api/lib/updates/components"; + +import { checkForUpdates } from "@/api/lib/updates/check"; +import { createTestCache } from "@/tests/cache"; +import { createMemoryDb } from "@/tests/memory-db"; +import { grantStaffPermissions } from "@/tests/staff-permissions"; + +import { pluginUpdatesAdminRoute } from "./updates.route"; + +type Db = ReturnType; + +const ADMIN = { id: 1, roleId: 1 }; +const SOURCE = "https://vitnode.example.com/updates.json"; +const CHECKED_AT = new Date("2026-10-15T03:00:00Z"); + +const COMPONENTS: UpdateComponent[] = [ + { + hasUnknownVersion: false, + id: "@vitnode/core", + kind: "core", + sourceUrl: SOURCE, + versions: ["2.0.0"], + }, + { + hasUnknownVersion: false, + id: "@acme/blog", + kind: "plugin", + sourceUrl: null, + versions: ["1.0.0"], + }, +]; + +const requestAs = async (permissions: PermissionsStaffArgs[]) => { + const cache = createTestCache(); + await grantStaffPermissions(cache, { permissions, userId: ADMIN.id }); + const db = createMemoryDb().db as unknown as Db; + await checkForUpdates({ + channelOverride: null, + components: COMPONENTS, + coreVersion: "2.0.0", + db, + fetchSource: async () => + await Promise.resolve({ + body: { + packages: { + "@vitnode/core": { + channels: { stable: "2.1.0" }, + releases: { "2.1.0": {} }, + }, + }, + schemaVersion: 1, + }, + etag: null, + kind: "fetched" as const, + lastModified: null, + }), + now: CHECKED_AT, + }); + + const app = new OpenAPIHono(); + app.use("*", async (c, next) => { + c.set("admin", { user: ADMIN } as unknown as Context["var"]["admin"]); + c.set("cache", cache); + c.set("db", db); + c.set("core", { + updates: { + channelOverride: null, + components: COMPONENTS, + coreVersion: "2.0.0", + enabled: true, + }, + } as unknown as Context["var"]["core"]); + await next(); + }); + app.openapi(pluginUpdatesAdminRoute.route, pluginUpdatesAdminRoute.handler); + + return await app.request("/updates"); +}; + +describe("GET /admin/plugins/updates", () => { + it("returns core and the plugins from the stored results", async () => { + const response = await requestAs([ + { module: "plugins", permission: "can_view", plugin: "@vitnode/core" }, + ]); + + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + core: { + id: "@vitnode/core", + lastSuccessAt: CHECKED_AT.toISOString(), + result: { latest: { version: "2.1.0" }, status: "update_available" }, + state: "checked", + }, + enabled: true, + plugins: [{ id: "@acme/blog", state: "unavailable" }], + running: false, + }); + }); + + it("refuses an administrator without the plugins permission", async () => { + const response = await requestAs([ + { module: "system", permission: "can_view", plugin: "@vitnode/core" }, + ]); + + expect(response.status).toBe(403); + }); +}); diff --git a/packages/vitnode/src/api/modules/admin/plugins/routes/updates.route.ts b/packages/vitnode/src/api/modules/admin/plugins/routes/updates.route.ts new file mode 100644 index 0000000000..c026b8eb01 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/plugins/routes/updates.route.ts @@ -0,0 +1,53 @@ +import { buildRoute } from "@/api/builders"; +import { MANUAL_UPDATE_CHECK_COOLDOWN_MS } from "@/api/lib/updates/lock"; +import { loadUpdatesOverview } from "@/api/lib/updates/status"; + +import { + serializeComponentUpdate, + updatesOverviewSchema, +} from "./updates.schema"; + +export const pluginUpdatesAdminRoute = buildRoute({ + adminStaffPermission: { module: "plugins", permission: "can_view" }, + route: { + method: "get", + description: + "Stored update and security-update results for core and every installed plugin. Never contacts a publisher.", + path: "/updates", + responses: { + 200: { + content: { "application/json": { schema: updatesOverviewSchema } }, + description: "Update status", + }, + 403: { description: "Access Denied" }, + }, + }, + handler: async c => { + const { channelOverride, components, coreVersion, enabled } = + c.get("core").updates; + const overview = await loadUpdatesOverview({ + channelOverride, + components, + coreVersion, + db: c.get("db"), + enabled, + }); + const [core, ...plugins] = overview.components.map( + serializeComponentUpdate, + ); + if (core === undefined) throw new Error("The core component is missing"); + + return c.json( + { + channelOverride: overview.channelOverride, + core, + enabled: overview.enabled, + lastManualAt: overview.lastManualAt?.toISOString() ?? null, + manualCooldownSeconds: MANUAL_UPDATE_CHECK_COOLDOWN_MS / 1000, + plugins, + running: overview.running, + }, + 200, + ); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/plugins/routes/updates.schema.ts b/packages/vitnode/src/api/modules/admin/plugins/routes/updates.schema.ts new file mode 100644 index 0000000000..d92223c4f3 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/plugins/routes/updates.schema.ts @@ -0,0 +1,71 @@ +import { z } from "zod"; + +import type { ComponentUpdateState } from "@/api/lib/updates/status"; + +import { UPDATE_CHANNELS } from "@/lib/updates/manifest"; + +const releaseSchema = z.object({ + version: z.string(), + publishedAt: z.string().nullable(), + requiresCore: z.string().nullable(), + url: z.string().nullable(), +}); + +export const componentUpdateSchema = z.object({ + id: z.string(), + sourceUrl: z.string().nullable(), + state: z.enum(["checked", "not_checked", "unavailable"]), + stale: z.boolean(), + lastAttemptAt: z.string().nullable(), + lastSuccessAt: z.string().nullable(), + lastError: z + .object({ code: z.string(), detail: z.string().nullable() }) + .nullable(), + result: z + .object({ + status: z.enum([ + "package_missing", + "channel_missing", + "unknown_channel", + "up_to_date", + "update_available", + "version_unknown", + ]), + channel: z.enum(UPDATE_CHANNELS).nullable(), + latest: releaseSchema.nullable(), + compatibility: z + .enum(["compatible", "incompatible", "unknown"]) + .nullable(), + alternative: releaseSchema.nullable(), + securityUpdates: z.array( + z.object({ + id: z.string(), + affected: z.string(), + fixed: z.string().nullable(), + url: z.string(), + versions: z.array(z.string()), + }), + ), + }) + .nullable(), +}); + +export const updatesOverviewSchema = z.object({ + enabled: z.boolean(), + channelOverride: z.enum(UPDATE_CHANNELS).nullable(), + running: z.boolean(), + lastManualAt: z.string().nullable(), + manualCooldownSeconds: z.number(), + core: componentUpdateSchema, + plugins: z.array(componentUpdateSchema), +}); + +const iso = (date: Date | null): null | string => date?.toISOString() ?? null; + +export const serializeComponentUpdate = ( + state: ComponentUpdateState, +): z.infer => ({ + ...state, + lastAttemptAt: iso(state.lastAttemptAt), + lastSuccessAt: iso(state.lastSuccessAt), +}); diff --git a/packages/vitnode/src/api/modules/cron/cron.module.ts b/packages/vitnode/src/api/modules/cron/cron.module.ts index f07ca418e7..cc3dda7b72 100644 --- a/packages/vitnode/src/api/modules/cron/cron.module.ts +++ b/packages/vitnode/src/api/modules/cron/cron.module.ts @@ -1,10 +1,11 @@ import { buildModule } from "@/api/builders"; +import { checkUpdatesCron } from "./cron/check-updates.cron"; import { cleanCron } from "./cron/clean.cron"; import { runCronRoute } from "./routes/cron.route"; export const cronModule = buildModule({ name: "cron", routes: [runCronRoute], - cronJobs: [cleanCron], + cronJobs: [cleanCron, checkUpdatesCron], }); diff --git a/packages/vitnode/src/api/modules/cron/cron/check-updates.cron.ts b/packages/vitnode/src/api/modules/cron/cron/check-updates.cron.ts new file mode 100644 index 0000000000..96792fa90f --- /dev/null +++ b/packages/vitnode/src/api/modules/cron/cron/check-updates.cron.ts @@ -0,0 +1,29 @@ +import { buildCron } from "@/api/lib/cron"; +import { runUpdateCheckFor } from "@/api/lib/updates/context"; + +export const checkUpdatesCron = buildCron({ + name: "check_updates", + description: + "Check core and plugins for updates and security updates in their public update manifests", + // Daily at 03:17 in the API server's time zone. A new job has never run, so + // the first scheduler tick after it is registered runs the first check. + schedule: "17 3 * * *", + handler: async c => { + if (!c.get("core").updates.enabled) return; + + const outcome = await runUpdateCheckFor(c, { manual: false }); + + if (outcome.status !== "completed") { + // Someone pressed "Check for updates" a moment ago; that run covers this one. + return; + } + + if (outcome.summary.failedSources > 0) { + await c + .get("log") + .warn( + `Update check: ${String(outcome.summary.failedSources)} of ${String(outcome.summary.sources)} update source(s) could not be read. The last valid results are kept.`, + ); + } + }, +}); diff --git a/packages/vitnode/src/api/plugin.ts b/packages/vitnode/src/api/plugin.ts index 1f51e43193..0293399a1a 100644 --- a/packages/vitnode/src/api/plugin.ts +++ b/packages/vitnode/src/api/plugin.ts @@ -100,7 +100,10 @@ export const newBuildPluginApiCore = buildApiPlugin({ { permission: "can_delete", dependsOn: ["can_view"] }, ], queue: ["can_view"], - plugins: ["can_view"], + plugins: [ + "can_view", + { permission: "can_check_updates", dependsOn: ["can_view"] }, + ], notifications: [ "can_view", { permission: "can_edit", dependsOn: ["can_view"] }, diff --git a/packages/vitnode/src/components/side-nav.tsx b/packages/vitnode/src/components/side-nav.tsx index b06d4c696e..35780dc2cd 100644 --- a/packages/vitnode/src/components/side-nav.tsx +++ b/packages/vitnode/src/components/side-nav.tsx @@ -1,17 +1,58 @@ import { Link } from "@tanstack/react-router"; import { cn } from "cn"; +import React from "react"; -export const SideNavCard = ({ +import { glideSidebarIndicators } from "./ui/sidebar-glide"; + +export const SideNav = ({ + children, className, ...props -}: React.ComponentProps<"nav">) => ( - - + {MODCP_NAV_ITEMS.map(item => ( { /> ))} - + ); }; diff --git a/packages/vitnode/src/vitnode.config.ts b/packages/vitnode/src/vitnode.config.ts index ac38276b8b..a8054ecfb1 100644 --- a/packages/vitnode/src/vitnode.config.ts +++ b/packages/vitnode/src/vitnode.config.ts @@ -26,6 +26,7 @@ import type { import type { VitNodeMetadata } from "./lib/metadata"; import type { BuildPluginReturn } from "./lib/plugin"; import type { FrontendPluginInventory } from "./lib/plugin-manifest"; +import type { UpdateChannel } from "./lib/updates/manifest"; import type { PersonalInformationFieldsConfig } from "./lib/user-personal-information"; import type { PageCacheConfig } from "./page-cache/config"; @@ -158,6 +159,21 @@ export interface VitNodeApiConfig { webp?: boolean; }; }; + /** + * Update and security-update checks for core and plugins, shown in + * AdminCP → System → Plugins. They read public JSON manifests once a day and + * never install anything. + */ + updates?: { + /** + * The release channel to compare against. Defaults to the channel of the + * installed version: `2.1.0` follows stable, `-rc.N` rc, `-canary.N` + * canary. Setting it is how an installation moves channels on purpose. + */ + channel?: UpdateChannel; + /** `false` turns checking off - for an install without outbound network access. Defaults to `true`. */ + enabled?: boolean; + }; users?: { /** * Which personal-information fields this install offers. Every field is on diff --git a/plugins/blog/src/manifest.gen.ts b/plugins/blog/src/manifest.gen.ts index f4b123381d..ce45426a01 100644 --- a/plugins/blog/src/manifest.gen.ts +++ b/plugins/blog/src/manifest.gen.ts @@ -3,9 +3,10 @@ // This file is generated by VitNode. Do not edit it, and do not format it. // // It is rewritten by `vitnode build` and `vitnode dev` from this package's -// package.json - `name`, `version`, `description`, `author` and `license` - and the title and -// icon in `src/plugin-info.ts`. Pass `manifest` to `buildPlugin`, -// `buildApiPlugin` and `bindApiPlugin` - it is where the plugin id lives. +// package.json - `name`, `version`, `description`, `author` and `license` - +// and the title, icon and update source in `src/plugin-info.ts`. Pass +// `manifest` to `buildPlugin`, `buildApiPlugin` and `bindApiPlugin` - it is +// where the plugin id lives. import { pluginInfo } from "./plugin-info" @@ -21,4 +22,5 @@ export const manifest = { }, license: "MIT", version: "2.0.0-canary.13", + updates: pluginInfo.updates ?? null, } as const diff --git a/plugins/blog/src/plugin-info.ts b/plugins/blog/src/plugin-info.ts index 293aaa6fb3..0116c1e1eb 100644 --- a/plugins/blog/src/plugin-info.ts +++ b/plugins/blog/src/plugin-info.ts @@ -3,4 +3,7 @@ import type { PluginInfo } from "@vitnode/core/lib/plugin-manifest"; export const pluginInfo: PluginInfo = { title: "Blog", icon: "newspaper", + updates: { + url: "https://raw.githubusercontent.com/aXenDeveloper/vitnode/update-manifest/updates.json", + }, }; diff --git a/plugins/example/src/manifest.gen.ts b/plugins/example/src/manifest.gen.ts index cff9eb31d5..9c388e423e 100644 --- a/plugins/example/src/manifest.gen.ts +++ b/plugins/example/src/manifest.gen.ts @@ -3,9 +3,10 @@ // This file is generated by VitNode. Do not edit it, and do not format it. // // It is rewritten by `vitnode build` and `vitnode dev` from this package's -// package.json - `name`, `version`, `description`, `author` and `license` - and the title and -// icon in `src/plugin-info.ts`. Pass `manifest` to `buildPlugin`, -// `buildApiPlugin` and `bindApiPlugin` - it is where the plugin id lives. +// package.json - `name`, `version`, `description`, `author` and `license` - +// and the title, icon and update source in `src/plugin-info.ts`. Pass +// `manifest` to `buildPlugin`, `buildApiPlugin` and `bindApiPlugin` - it is +// where the plugin id lives. import { pluginInfo } from "./plugin-info" @@ -21,4 +22,5 @@ export const manifest = { }, license: "MIT", version: "2.0.0-canary.13", + updates: pluginInfo.updates ?? null, } as const diff --git a/plugins/forum/src/manifest.gen.ts b/plugins/forum/src/manifest.gen.ts index 7c648daf61..b99de8a6b9 100644 --- a/plugins/forum/src/manifest.gen.ts +++ b/plugins/forum/src/manifest.gen.ts @@ -3,9 +3,10 @@ // This file is generated by VitNode. Do not edit it, and do not format it. // // It is rewritten by `vitnode build` and `vitnode dev` from this package's -// package.json - `name`, `version`, `description`, `author` and `license` - and the title and -// icon in `src/plugin-info.ts`. Pass `manifest` to `buildPlugin`, -// `buildApiPlugin` and `bindApiPlugin` - it is where the plugin id lives. +// package.json - `name`, `version`, `description`, `author` and `license` - +// and the title, icon and update source in `src/plugin-info.ts`. Pass +// `manifest` to `buildPlugin`, `buildApiPlugin` and `bindApiPlugin` - it is +// where the plugin id lives. import { pluginInfo } from "./plugin-info" @@ -21,4 +22,5 @@ export const manifest = { }, license: "MIT", version: "2.0.0-canary.13", + updates: pluginInfo.updates ?? null, } as const diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45b178f23b..fb06dc9161 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -687,6 +687,9 @@ importers: sanitize-html: specifier: ^2.18.0 version: 2.18.0 + semver: + specifier: ^7.8.5 + version: 7.8.5 shadcn: specifier: ^4.21.4 version: 4.21.4 @@ -748,6 +751,9 @@ importers: '@types/sanitize-html': specifier: ^2.16.2 version: 2.16.2 + '@types/semver': + specifier: ^7.8.0 + version: 7.8.0 '@vitejs/plugin-react': specifier: ^6.1.2 version: 6.1.2(babel-plugin-react-compiler@1.0.0)(vite@8.3.4(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) @@ -4938,6 +4944,9 @@ packages: '@types/sanitize-html@2.16.2': resolution: {integrity: sha512-kuAbgDCFv418flrp3z4o/4ufLA/5ovmXRH0L419iZCPGO7w00znjeDI5Wy/KZWNIBpDQc7hucCK+J5almB6FyA==} + '@types/semver@7.8.0': + resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -12860,6 +12869,8 @@ snapshots: dependencies: htmlparser2: 12.0.0 + '@types/semver@7.8.0': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} diff --git a/scripts/bump-version/version-manager.ts b/scripts/bump-version/version-manager.ts index 95a7984db2..d709634c76 100644 --- a/scripts/bump-version/version-manager.ts +++ b/scripts/bump-version/version-manager.ts @@ -26,6 +26,11 @@ export class VersionManager { async init(): Promise { if (this.env.GITHUB_OPTION_MODE === "publish") { console.log("Skipping version bump in publish mode"); + // The release steps and the update manifest still need the tag of what + // is about to be published. + this.exposeNewVersion( + `${CONFIG.TAG_PREFIX}${this.getCurrentVersion()}${CONFIG.TAG_SUFFIX}`, + ); return; } diff --git a/scripts/update-manifest/publish-update-manifest.ts b/scripts/update-manifest/publish-update-manifest.ts new file mode 100644 index 0000000000..ff4cd95f92 --- /dev/null +++ b/scripts/update-manifest/publish-update-manifest.ts @@ -0,0 +1,353 @@ +import { execFileSync } from "node:child_process"; +import { + existsSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +import type { UpdateManifest } from "../../packages/vitnode/src/lib/updates/manifest.ts"; +import type { + GitHubReleaseMetadata, + PublishedPackage, +} from "../../packages/vitnode/scripts/update-manifest/merge.ts"; + +import { + applySecurityUpdates, + channelForReleaseType, + mergeRelease, + parseSecurityUpdatesInput, + readPublishedManifest, + requiresCoreFrom, + serializeUpdateManifest, +} from "../../packages/vitnode/scripts/update-manifest/merge.ts"; + +/** + * Publishes VitNode's official update manifest to the `update-manifest` branch, + * served at https://raw.githubusercontent.com//update-manifest/updates.json. + * + * release after a successful npm publish: adds the published versions + * security-updates replaces the security updates with `security/security-updates.json` + * check validates `security/security-updates.json` and exits + * + * Every write fetches the branch, merges, and pushes; a push that loses a race + * is retried from a fresh fetch, so concurrent runs cannot overwrite each other. + */ + +const BRANCH = process.env.UPDATE_MANIFEST_BRANCH ?? "update-manifest"; +const FILE = "updates.json"; +const SECURITY_UPDATES_FILE = join("security", "security-updates.json"); +const MAX_PUSH_ATTEMPTS = 5; +const CORE_ID = "@vitnode/core"; + +const workspace = process.env.GITHUB_WORKSPACE ?? process.cwd(); + +const requireEnv = (name: string): string => { + const value = process.env[name]; + if (!value) throw new Error(`Missing required environment variable: ${name}`); + + return value; +}; + +const run = ( + command: string, + args: string[], + options: { cwd?: string; allowFailure?: boolean } = {}, +): null | string => { + try { + return execFileSync(command, args, { + cwd: options.cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + } catch (error) { + if (options.allowFailure) return null; + // The remote URL carries the token; never echo the arguments. + const stderr = + typeof error === "object" && error !== null && "stderr" in error + ? String(error.stderr).replaceAll(/x-access-token:[^@]+@/g, "***@") + : ""; + throw new Error(`\`${command} ${args[0] ?? ""}\` failed. ${stderr}`.trim()); + } +}; + +interface WorkspacePackage { + dependencies: Record; + dir: string; + name: string; + version: null | string; +} + +const MANIFEST_FILES = ["src/manifest.gen.ts", "src/manifest.ts"]; + +const updatesUrlOf = async (dir: string): Promise => { + const file = MANIFEST_FILES.map(name => join(dir, name)).find(existsSync); + if (file === undefined) return null; + + const { manifest } = (await import(pathToFileURL(file).href)) as { + manifest?: { updates?: null | { url?: string } }; + }; + + return manifest?.updates?.url ?? null; +}; + +const readWorkspacePackages = (): Map => { + const packages = new Map(); + + for (const group of ["packages", "plugins", "apps"]) { + const root = join(workspace, group); + if (!existsSync(root)) continue; + + for (const dir of readdirSync(root)) { + const file = join(root, dir, "package.json"); + if (!existsSync(file)) continue; + + const json = JSON.parse(readFileSync(file, "utf8")) as { + dependencies?: Record; + name?: string; + peerDependencies?: Record; + version?: string; + }; + if (!json.name) continue; + + packages.set(json.name, { + dependencies: { ...json.peerDependencies, ...json.dependencies }, + dir: join(root, dir), + name: json.name, + version: json.version ?? null, + }); + } + } + + return packages; +}; + +const githubReleaseFor = ( + repository: string, + tag: string, +): GitHubReleaseMetadata | null => { + const output = run( + "gh", + [ + "release", + "view", + tag, + "--repo", + repository, + "--json", + "publishedAt,url,isDraft", + ], + { allowFailure: true }, + ); + if (output === null) return null; + + const release = JSON.parse(output) as { + isDraft?: boolean; + publishedAt?: null | string; + url?: string; + }; + if (release.isDraft || !release.publishedAt || !release.url) return null; + + return { publishedAt: release.publishedAt, url: release.url }; +}; + +/** + * The packages this run published that point at this feed. A package that is + * published but declares another update source - or none - is not ours to + * advertise here. + */ +const publishedPackages = async ( + feedUrl: string, +): Promise => { + const names = requireEnv("PUBLISHED_PACKAGES").split(/\s+/).filter(Boolean); + const workspacePackages = readWorkspacePackages(); + const coreVersion = workspacePackages.get(CORE_ID)?.version ?? null; + + const packages: PublishedPackage[] = []; + for (const name of names) { + const pkg = workspacePackages.get(name); + if (!pkg) throw new Error(`${name} is not a package in this workspace.`); + + const updatesUrl = await updatesUrlOf(pkg.dir); + if (updatesUrl?.toLowerCase() !== feedUrl.toLowerCase()) continue; + if (!pkg.version) throw new Error(`${name} has no version.`); + + const requiresCore = + name === CORE_ID || coreVersion === null + ? undefined + : requiresCoreFrom(pkg.dependencies[CORE_ID], coreVersion); + + packages.push({ id: name, requiresCore, version: pkg.version }); + } + + return packages; +}; + +const publishToBranch = ({ + message, + repository, + token, + update, +}: { + message: string; + repository: string; + token: string; + update: (manifest: UpdateManifest) => UpdateManifest; +}): void => { + const remote = + process.env.UPDATE_MANIFEST_REMOTE ?? + `https://x-access-token:${token}@github.com/${repository}.git`; + + for (let attempt = 1; attempt <= MAX_PUSH_ATTEMPTS; attempt++) { + const dir = mkdtempSync(join(tmpdir(), "vitnode-update-manifest-")); + try { + run("git", ["init", "--quiet"], { cwd: dir }); + run("git", ["remote", "add", "origin", remote], { cwd: dir }); + run("git", ["config", "user.name", "github-actions[bot]"], { cwd: dir }); + run( + "git", + [ + "config", + "user.email", + "41898282+github-actions[bot]@users.noreply.github.com", + ], + { cwd: dir }, + ); + + const fetched = + run("git", ["fetch", "--quiet", "--depth", "1", "origin", BRANCH], { + allowFailure: true, + cwd: dir, + }) !== null; + if (fetched) { + run("git", ["checkout", "--quiet", "-B", BRANCH, "FETCH_HEAD"], { + cwd: dir, + }); + } else { + // First publication: a branch with nothing but the manifest on it. + run("git", ["checkout", "--quiet", "--orphan", BRANCH], { cwd: dir }); + } + + const path = join(dir, FILE); + const before = existsSync(path) ? readFileSync(path, "utf8") : null; + const after = serializeUpdateManifest( + update(readPublishedManifest(before)), + ); + + if (before === after) { + console.log("updates.json is already up to date - nothing to push."); + + return; + } + + writeFileSync(path, after); + run("git", ["add", FILE], { cwd: dir }); + run("git", ["commit", "--quiet", "-m", message], { cwd: dir }); + + const pushed = + run("git", ["push", "--quiet", "origin", `HEAD:${BRANCH}`], { + allowFailure: true, + cwd: dir, + }) !== null; + if (pushed) { + console.log(`Published ${FILE} to ${BRANCH}.`); + + return; + } + + console.warn( + `Push attempt ${String(attempt)} was rejected - another run published first. Merging again.`, + ); + } finally { + rmSync(dir, { force: true, recursive: true }); + } + } + + throw new Error( + `Could not publish ${FILE} after ${String(MAX_PUSH_ATTEMPTS)} attempts.`, + ); +}; + +const readSecurityUpdates = () => + parseSecurityUpdatesInput( + readFileSync(join(workspace, SECURITY_UPDATES_FILE), "utf8"), + ); + +const main = async () => { + const mode = process.argv[2]; + + if (mode === "check") { + const input = readSecurityUpdates(); + const count = Object.values(input.securityUpdates).flat().length; + console.log( + `${SECURITY_UPDATES_FILE} is valid (${String(count)} security updates).`, + ); + + return; + } + + const repository = requireEnv("GITHUB_REPOSITORY"); + const token = requireEnv("GITHUB_TOKEN"); + const feedUrl = `https://raw.githubusercontent.com/${repository}/${BRANCH}/${FILE}`; + + if (mode === "security-updates") { + const input = readSecurityUpdates(); + publishToBranch({ + message: "chore: update security updates", + repository, + token, + update: manifest => applySecurityUpdates(manifest, input), + }); + + return; + } + + if (mode === "release") { + const channel = channelForReleaseType(requireEnv("RELEASE_TYPE")); + const packages = await publishedPackages(feedUrl); + if (packages.length === 0) { + console.log("No published package points at this update manifest."); + + return; + } + + const coreVersion = + packages.find(pkg => pkg.id === CORE_ID)?.version ?? packages[0]?.version; + // Releases are tagged v by the bump script; a run that skipped the + // GitHub Release simply finds none, and the entry goes out without a date. + const githubRelease = coreVersion + ? githubReleaseFor(repository, `v${coreVersion}`) + : null; + if (githubRelease === null) { + console.log( + "No published GitHub Release for this version - publishedAt and the release URL are left out.", + ); + } + + publishToBranch({ + message: `chore: publish ${packages + .map(pkg => `${pkg.id}@${pkg.version}`) + .join(", ")} to ${channel}`, + repository, + token, + update: manifest => + mergeRelease(manifest, { channel, githubRelease, packages }), + }); + + return; + } + + throw new Error( + "Usage: publish-update-manifest.ts ", + ); +}; + +main().catch((error: unknown) => { + console.error(`❌ ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); +}); diff --git a/security/README.md b/security/README.md new file mode 100644 index 0000000000..99f527ff5c --- /dev/null +++ b/security/README.md @@ -0,0 +1,7 @@ +# Security updates + +`security-updates.json` is the maintained list of security updates for the packages VitNode publishes. The **Publish security updates** workflow validates it on every pull request and, once it lands on `canary`, copies it into the public update manifest on the `update-manifest` branch - the one every VitNode installation reads once a day. + +How to write an entry, how ranges match prereleases, and how to announce a vulnerability without a fix: https://vitnode.com/docs/dev/updates/security-updates + +Only real security updates belong here. Examples live in the documentation. diff --git a/security/security-updates.json b/security/security-updates.json new file mode 100644 index 0000000000..44fa4f2281 --- /dev/null +++ b/security/security-updates.json @@ -0,0 +1,4 @@ +{ + "schemaVersion": 1, + "securityUpdates": {} +}