@agent-native/core 0.80.4 → 0.80.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +28 -2
- package/corpus/core/docs/content/locales/ar-SA/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/de-DE/real-time-collaboration.mdx +3 -3
- package/corpus/core/docs/content/locales/de-DE/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/es-ES/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/es-ES/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/fr-FR/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/ja-JP/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/ko-KR/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/pt-BR/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-CN/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-TW/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +1 -1
- package/corpus/core/docs/content/real-time-collaboration.mdx +2 -2
- package/corpus/core/docs/content/template-design.mdx +1 -1
- package/corpus/core/package.json +3 -7
- package/corpus/core/src/action.ts +21 -0
- package/corpus/core/src/agent/durable-background.ts +32 -11
- package/corpus/core/src/agent/production-agent.ts +11 -1
- package/corpus/core/src/agent/run-loop-with-resume.ts +3 -1
- package/corpus/core/src/brand-kit/fig/index.ts +17 -37
- package/corpus/core/src/cli/create.ts +1 -0
- package/corpus/core/src/cli/index.ts +1 -1
- package/corpus/core/src/cli/mcp-config-writers.ts +187 -51
- package/corpus/core/src/cli/skills.ts +18 -3
- package/corpus/core/src/client/AssistantChat.tsx +11 -1
- package/corpus/core/src/client/agent-chat.ts +12 -1
- package/corpus/core/src/client/composer/PromptComposer.tsx +100 -41
- package/corpus/core/src/client/org/OrgSwitcher.tsx +1 -1
- package/corpus/core/src/client/sharing/ShareButton.tsx +3 -27
- package/corpus/core/src/collab/presence.ts +1 -1
- package/corpus/core/src/mcp/connect-route.ts +34 -5
- package/corpus/core/src/server/action-discovery.ts +4 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +20 -0
- package/corpus/core/src/sharing/registry.ts +17 -0
- package/corpus/core/src/templates/default/pnpm-workspace.yaml +1 -0
- package/corpus/core/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -0
- package/corpus/templates/analytics/actions/update-dashboard.ts +8 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +117 -33
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -0
- package/corpus/templates/analytics/changelog/2026-06-27-dashboards-can-now-nest-under-a-parent-in-the-sidebar-via-a-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-analytics-pages-now-sit-closer-under-the-header-with-tighter.md +6 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +3 -0
- package/corpus/templates/assets/actions/generate-image.ts +3 -0
- package/corpus/templates/assets/changelog/2026-06-29-longer-agent-generation-window.md +6 -0
- package/corpus/templates/assets/netlify.toml +3 -3
- package/corpus/templates/assets/server/plugins/agent-chat.ts +4 -1
- package/corpus/templates/clips/app/components/capture-install-options.tsx +7 -8
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +1 -1
- package/corpus/templates/content/AGENTS.md +57 -44
- package/corpus/templates/content/README.md +16 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +125 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +28 -6
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +238 -69
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +125 -43
- package/corpus/templates/content/actions/_database-source-utils.ts +746 -97
- package/corpus/templates/content/actions/_database-utils.ts +17 -14
- package/corpus/templates/content/actions/_federation-join.ts +14 -2
- package/corpus/templates/content/actions/_join-suggestion.ts +89 -14
- package/corpus/templates/content/actions/_local-file-documents.ts +2 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +124 -5
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +256 -0
- package/corpus/templates/content/actions/change-content-database-source-role.ts +420 -0
- package/corpus/templates/content/actions/create-document.ts +8 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +282 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +220 -8
- package/corpus/templates/content/actions/get-content-database.ts +10 -2
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +19 -2
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +47 -15
- package/corpus/templates/content/actions/refresh-content-database-source.ts +11 -4
- package/corpus/templates/content/actions/review-content-database-source-change-set.ts +6 -2
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +32 -12
- package/corpus/templates/content/actions/stage-builder-revision.ts +14 -10
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +25 -3
- package/corpus/templates/content/app/blocks/BuilderDocsBlocks.tsx +10 -0
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +1 -0
- package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +185 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1520 -553
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +150 -1
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +19 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +647 -66
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +543 -204
- package/corpus/templates/content/app/components/editor/extensions/ContentReferenceNode.tsx +82 -0
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +62 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +76 -0
- package/corpus/templates/content/app/i18n-data.ts +924 -0
- package/corpus/templates/content/changelog/2026-06-29-builder-review-checks-now-preserve-publish-and-unpublish-cho.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-database-sources-can-now-be-added-as-more-rows-or-matched.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-fixed-loading-the-final-rows-in-larger-databases-without-bre.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-reusable-mdx-references-now-preview-linked-local-documents-i.md +6 -0
- package/corpus/templates/content/shared/api.ts +91 -1
- package/corpus/templates/content/shared/builder-docs-blocks.ts +8 -0
- package/corpus/templates/content/shared/builder-mdx.ts +141 -4
- package/corpus/templates/content/shared/nfm.ts +62 -0
- package/corpus/templates/content/vite.config.ts +14 -7
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +10 -11
- package/corpus/templates/design/AGENTS.md +5 -0
- package/corpus/templates/design/_gitignore +7 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +22 -12
- package/corpus/templates/design/actions/create-design-system.ts +15 -4
- package/corpus/templates/design/actions/create-file.ts +20 -1
- package/corpus/templates/design/actions/delete-design-system.ts +2 -2
- package/corpus/templates/design/actions/duplicate-design.ts +35 -9
- package/corpus/templates/design/actions/generate-design.ts +146 -69
- package/corpus/templates/design/actions/generate-screens.ts +51 -19
- package/corpus/templates/design/actions/get-design-snapshot.ts +5 -1
- package/corpus/templates/design/actions/get-design.ts +2 -0
- package/corpus/templates/design/actions/import-design-project.ts +28 -9
- package/corpus/templates/design/actions/import-document.ts +22 -8
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +6 -153
- package/corpus/templates/design/actions/navigate.ts +105 -54
- package/corpus/templates/design/actions/set-default-design-system.ts +17 -3
- package/corpus/templates/design/actions/show-design-questions.ts +2 -0
- package/corpus/templates/design/actions/update-design.ts +2 -1
- package/corpus/templates/design/actions/view-screen.ts +12 -6
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +4 -4
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +159 -41
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +8 -4
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +37 -42
- package/corpus/templates/design/app/components/design/EditPanel.tsx +336 -183
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +153 -34
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +135 -33
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +6 -0
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +2 -1
- package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +4 -10
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +84 -43
- package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +4 -4
- package/corpus/templates/design/app/components/design/inspector/{FigmaColorPicker.tsx → DesignColorPicker.tsx} +89 -62
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +14 -26
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +40 -12
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -4
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +13 -13
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +13 -9
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +13 -4
- package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +6 -6
- package/corpus/templates/design/app/components/design/inspector/{figma-icons.tsx → design-icons.tsx} +2 -2
- package/corpus/templates/design/app/components/design/inspector/index.ts +12 -12
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +5 -2
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +3 -3
- package/corpus/templates/design/app/components/layout/Layout.tsx +5 -2
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +167 -91
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +16 -3
- package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +15 -0
- package/corpus/templates/design/app/global.css +4 -0
- package/corpus/templates/design/app/hooks/use-design-system.ts +4 -1
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +38 -3
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +9 -2
- package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
- package/corpus/templates/design/app/i18n-data.ts +527 -0
- package/corpus/templates/design/app/lib/design-systems.ts +6 -4
- package/corpus/templates/design/app/lib/design-templates.ts +15 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +641 -158
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +42 -34
- package/corpus/templates/design/app/pages/VisualEdit.tsx +100 -0
- package/corpus/templates/design/app/root.tsx +42 -9
- package/corpus/templates/design/app/routes/visual-edit.tsx +7 -0
- package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +1 -1
- package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +1 -1
- package/corpus/templates/design/changelog/2026-06-28-visual-code-design-mode.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-generation-prompts-now-show-connect-ai-before-sending.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +1 -1
- package/corpus/templates/design/changelog/2026-06-29-fixed-design-editor-exports-and-gradient-edits-so-multi-scre.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-png-export-handles-modern-css-color-functions-more-reliably.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-png-export-now-ignores-duplicate-clicks-while-a-download-is-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-public-visual-edit-and-shared-design-links-now-invite-visito.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-visual-edit-actions-now-reject-missing-design-targets-before.md +6 -0
- package/corpus/templates/design/e2e/README.md +71 -0
- package/corpus/templates/design/e2e/global-setup.ts +157 -0
- package/corpus/templates/design/e2e/helpers.ts +148 -0
- package/corpus/templates/design/netlify.toml +3 -4
- package/corpus/templates/design/package.json +5 -1
- package/corpus/templates/design/playwright.config.ts +51 -0
- package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +121 -0
- package/corpus/templates/design/server/handlers/uploads.ts +89 -38
- package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +181 -0
- package/corpus/templates/design/server/lib/coding-handoff.ts +33 -4
- package/corpus/templates/design/server/lib/design-export.ts +35 -8
- package/corpus/templates/design/server/plugins/agent-chat.ts +4 -0
- package/corpus/templates/design/server/plugins/auth.ts +10 -1
- package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +1 -0
- package/corpus/templates/design/shared/api.ts +2 -0
- package/corpus/templates/design/shared/apply-edits.ts +12 -8
- package/corpus/templates/design/shared/canvas-math.ts +19 -1
- package/corpus/templates/design/shared/code-layer.ts +87 -28
- package/corpus/templates/design/shared/generation-session.ts +1 -0
- package/corpus/templates/design/shared/resolve-tweaks.ts +4 -3
- package/corpus/templates/design/shared/shader-presets.ts +10 -0
- package/corpus/templates/design/shared/source-mode.ts +9 -0
- package/corpus/templates/design/vitest.config.ts +2 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +1 -1
- package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +1 -1
- package/corpus/templates/plan/AGENTS.md +1 -1
- package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
- package/corpus/templates/plan/actions/validate-local-plan-source.ts +2 -10
- package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -0
- package/corpus/templates/plan/docs/commenting-ux-plan.md +2 -2
- package/corpus/templates/plan/server/plan-content.ts +1 -1
- package/corpus/templates/plan/server/ui-plan-html.ts +2 -2
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +6 -153
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +53 -38
- package/corpus/templates/slides/app/components/design-system/{fig-import-response.ts → builder-index-response.ts} +9 -6
- package/corpus/templates/slides/app/components/visual-editor/MultiSelectChip.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-06-29-design-system-setup-now-indexes-uploaded-design-files-throug.md +6 -0
- package/corpus/templates/slides/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
- package/corpus/templates/slides/netlify.toml +3 -6
- package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +116 -0
- package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +179 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +4 -1
- package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +1 -0
- package/dist/action.d.ts +15 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +6 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/durable-background.d.ts +9 -7
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +27 -11
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/production-agent.d.ts +7 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +4 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +2 -1
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +2 -2
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/brand-kit/fig/index.d.ts +4 -4
- package/dist/brand-kit/fig/index.d.ts.map +1 -1
- package/dist/brand-kit/fig/index.js +14 -27
- package/dist/brand-kit/fig/index.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +1 -0
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-config-writers.d.ts +15 -7
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +198 -45
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +18 -3
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +11 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +15 -2
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +35 -1
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/org/OrgSwitcher.js +1 -1
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/sharing/ShareButton.d.ts +1 -1
- package/dist/client/sharing/ShareButton.d.ts.map +1 -1
- package/dist/client/sharing/ShareButton.js +2 -15
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/collab/presence.d.ts +1 -1
- package/dist/collab/presence.js +1 -1
- package/dist/collab/presence.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/mcp/connect-route.d.ts.map +1 -1
- package/dist/mcp/connect-route.js +34 -5
- package/dist/mcp/connect-route.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +6 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +12 -3
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js +11 -0
- package/dist/sharing/registry.js.map +1 -1
- package/dist/templates/default/pnpm-workspace.yaml +1 -0
- package/dist/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/docs/content/locales/ar-SA/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/ar-SA/template-design.mdx +1 -1
- package/docs/content/locales/de-DE/real-time-collaboration.mdx +3 -3
- package/docs/content/locales/de-DE/template-design.mdx +1 -1
- package/docs/content/locales/es-ES/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/es-ES/template-design.mdx +1 -1
- package/docs/content/locales/fr-FR/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/fr-FR/template-design.mdx +1 -1
- package/docs/content/locales/hi-IN/template-design.mdx +1 -1
- package/docs/content/locales/ja-JP/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/ja-JP/template-design.mdx +1 -1
- package/docs/content/locales/ko-KR/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/ko-KR/template-design.mdx +1 -1
- package/docs/content/locales/pt-BR/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/pt-BR/template-design.mdx +1 -1
- package/docs/content/locales/zh-CN/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/zh-CN/template-design.mdx +1 -1
- package/docs/content/locales/zh-TW/real-time-collaboration.mdx +2 -2
- package/docs/content/locales/zh-TW/template-design.mdx +1 -1
- package/docs/content/real-time-collaboration.mdx +2 -2
- package/docs/content/template-design.mdx +1 -1
- package/package.json +3 -7
- package/src/templates/default/pnpm-workspace.yaml +1 -0
- package/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/corpus/core/src/brand-kit/fig/decode.ts +0 -410
- package/corpus/core/src/brand-kit/fig/extract-design-system.ts +0 -947
- package/corpus/core/src/brand-kit/fig/fig-to-html.ts +0 -1906
- package/corpus/templates/design/actions/import-figma.ts +0 -55
- package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +0 -6
- package/corpus/templates/design/server/handlers/import-figma-system.ts +0 -86
- package/corpus/templates/design/server/routes/api/import-figma-system.post.ts +0 -1
- package/corpus/templates/slides/server/handlers/import-figma-system.ts +0 -101
- package/corpus/templates/slides/server/routes/api/import-figma-system.post.ts +0 -1
- package/dist/brand-kit/fig/decode.d.ts +0 -33
- package/dist/brand-kit/fig/decode.d.ts.map +0 -1
- package/dist/brand-kit/fig/decode.js +0 -358
- package/dist/brand-kit/fig/decode.js.map +0 -1
- package/dist/brand-kit/fig/extract-design-system.d.ts +0 -44
- package/dist/brand-kit/fig/extract-design-system.d.ts.map +0 -1
- package/dist/brand-kit/fig/extract-design-system.js +0 -752
- package/dist/brand-kit/fig/extract-design-system.js.map +0 -1
- package/dist/brand-kit/fig/fig-to-html.d.ts +0 -246
- package/dist/brand-kit/fig/fig-to-html.d.ts.map +0 -1
- package/dist/brand-kit/fig/fig-to-html.js +0 -1506
- package/dist/brand-kit/fig/fig-to-html.js.map +0 -1
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
serializeDatabase,
|
|
20
20
|
} from "./_property-utils.js";
|
|
21
21
|
|
|
22
|
+
export const CONTENT_DATABASE_MAX_READ_LIMIT = 5_000;
|
|
23
|
+
|
|
22
24
|
function canManageRole(role: string) {
|
|
23
25
|
return role === "owner" || role === "admin";
|
|
24
26
|
}
|
|
@@ -78,7 +80,10 @@ export function normalizeContentDatabasePageOptions(options: {
|
|
|
78
80
|
}) {
|
|
79
81
|
const limit =
|
|
80
82
|
typeof options.limit === "number" && Number.isFinite(options.limit)
|
|
81
|
-
? Math.max(
|
|
83
|
+
? Math.max(
|
|
84
|
+
1,
|
|
85
|
+
Math.min(Math.floor(options.limit), CONTENT_DATABASE_MAX_READ_LIMIT),
|
|
86
|
+
)
|
|
82
87
|
: null;
|
|
83
88
|
const offset =
|
|
84
89
|
typeof options.offset === "number" && Number.isFinite(options.offset)
|
|
@@ -181,21 +186,19 @@ export async function getContentDatabaseResponse(
|
|
|
181
186
|
const serializedDocumentIds = new Set(
|
|
182
187
|
serializedItems.map((item) => item.document.id),
|
|
183
188
|
);
|
|
184
|
-
// When paginating,
|
|
185
|
-
//
|
|
186
|
-
// document)
|
|
189
|
+
// When paginating, scope every DOCUMENT-BACKED source's rows to the visible
|
|
190
|
+
// page — that's the primary AND any row-union secondary (each row maps to a
|
|
191
|
+
// real document). Federated join rows carry no document (empty documentId),
|
|
192
|
+
// so they're kept intact — only matched ones overlay anyway.
|
|
187
193
|
const pagedSources =
|
|
188
194
|
limit !== null
|
|
189
|
-
? sources.map((source
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
: source,
|
|
198
|
-
)
|
|
195
|
+
? sources.map((source) => ({
|
|
196
|
+
...source,
|
|
197
|
+
rows: source.rows.filter(
|
|
198
|
+
(row) =>
|
|
199
|
+
!row.documentId || serializedDocumentIds.has(row.documentId),
|
|
200
|
+
),
|
|
201
|
+
}))
|
|
199
202
|
: sources;
|
|
200
203
|
const pagedPrimary = pagedSources[0] ?? null;
|
|
201
204
|
|
|
@@ -53,6 +53,13 @@ export function federateSources(args: {
|
|
|
53
53
|
const primaryRowByDocumentId = new Map(
|
|
54
54
|
(primary?.rows ?? []).map((row) => [row.documentId, row]),
|
|
55
55
|
);
|
|
56
|
+
const rowByDocumentId = new Map<string, ContentDatabaseSourceRow>();
|
|
57
|
+
for (const source of sources) {
|
|
58
|
+
for (const row of source.rows) {
|
|
59
|
+
if (!row.documentId || rowByDocumentId.has(row.documentId)) continue;
|
|
60
|
+
rowByDocumentId.set(row.documentId, row);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
56
63
|
|
|
57
64
|
// Secondary sources carrying an identity-join config.
|
|
58
65
|
const secondaries = sources
|
|
@@ -82,7 +89,9 @@ export function federateSources(args: {
|
|
|
82
89
|
return items.map((item) => ({
|
|
83
90
|
...item,
|
|
84
91
|
sourceRecord:
|
|
85
|
-
primaryRowByDocumentId.get(item.document.id) ??
|
|
92
|
+
primaryRowByDocumentId.get(item.document.id) ??
|
|
93
|
+
rowByDocumentId.get(item.document.id) ??
|
|
94
|
+
item.sourceRecord,
|
|
86
95
|
}));
|
|
87
96
|
}
|
|
88
97
|
|
|
@@ -112,7 +121,10 @@ export function federateSources(args: {
|
|
|
112
121
|
|
|
113
122
|
return {
|
|
114
123
|
...item,
|
|
115
|
-
sourceRecord:
|
|
124
|
+
sourceRecord:
|
|
125
|
+
primaryRow ??
|
|
126
|
+
rowByDocumentId.get(item.document.id) ??
|
|
127
|
+
item.sourceRecord,
|
|
116
128
|
canonicalKey,
|
|
117
129
|
sourceOverlays: overlays.length > 0 ? overlays : undefined,
|
|
118
130
|
};
|
|
@@ -36,7 +36,16 @@ export interface JoinSuggestion {
|
|
|
36
36
|
|
|
37
37
|
type ValueRow = Record<string, DocumentPropertyValue>;
|
|
38
38
|
|
|
39
|
-
const KEY_NAME_HINTS = [
|
|
39
|
+
const KEY_NAME_HINTS = [
|
|
40
|
+
"url",
|
|
41
|
+
"slug",
|
|
42
|
+
"handle",
|
|
43
|
+
"id",
|
|
44
|
+
"key",
|
|
45
|
+
"path",
|
|
46
|
+
"ref",
|
|
47
|
+
"author",
|
|
48
|
+
];
|
|
40
49
|
|
|
41
50
|
function stringValue(value: DocumentPropertyValue): string | null {
|
|
42
51
|
if (typeof value === "string") return value.trim() ? value : null;
|
|
@@ -59,6 +68,18 @@ function proposeFormula(field: string, urlLike: boolean): string {
|
|
|
59
68
|
: `lower(trim({${field}}))`;
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
function looksLikeBuilderReference(values: string[]): boolean {
|
|
72
|
+
return values.some((value) => /^[a-z0-9_-]+:[a-z0-9_-]+$/i.test(value));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function proposeFormulas(field: string, values: string[], urlLike: boolean) {
|
|
76
|
+
const formulas = [proposeFormula(field, urlLike)];
|
|
77
|
+
if (looksLikeBuilderReference(values)) {
|
|
78
|
+
formulas.push(`lower(trim(regexreplace({${field}}, "^[^:]+:", "")))`);
|
|
79
|
+
}
|
|
80
|
+
return formulas;
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
function fieldLabel(field: string): string {
|
|
63
84
|
return (
|
|
64
85
|
field
|
|
@@ -98,12 +119,11 @@ function rawStringValuesForField(field: string, rows: ValueRow[]) {
|
|
|
98
119
|
return raws;
|
|
99
120
|
}
|
|
100
121
|
|
|
101
|
-
function
|
|
122
|
+
function profileFieldWithFormula(
|
|
102
123
|
field: string,
|
|
103
124
|
raws: string[],
|
|
104
|
-
|
|
125
|
+
formula: string,
|
|
105
126
|
): FieldProfile {
|
|
106
|
-
const formula = proposeFormula(field, urlLike);
|
|
107
127
|
const normalizedByRaw = new Map<string, string>();
|
|
108
128
|
const normalizedSet = new Set<string>();
|
|
109
129
|
for (const raw of raws) {
|
|
@@ -123,6 +143,19 @@ function jaccard(a: Set<string>, b: Set<string>): number {
|
|
|
123
143
|
return union === 0 ? 0 : intersection / union;
|
|
124
144
|
}
|
|
125
145
|
|
|
146
|
+
function intersectionSize(a: Set<string>, b: Set<string>): number {
|
|
147
|
+
let intersection = 0;
|
|
148
|
+
for (const value of a) if (b.has(value)) intersection += 1;
|
|
149
|
+
return intersection;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function lowInformationField(field: string): boolean {
|
|
153
|
+
const normalized = field.toLowerCase();
|
|
154
|
+
return /(^|[._-])(published|state|status|sync_state|created|updated|lastupdated|last_updated)([._-]|$)/.test(
|
|
155
|
+
normalized,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
126
159
|
function nameNudge(primaryField: string, secondaryField: string): number {
|
|
127
160
|
const hinted = (field: string) =>
|
|
128
161
|
KEY_NAME_HINTS.some((hint) => field.toLowerCase().includes(hint));
|
|
@@ -164,17 +197,59 @@ export function suggestJoinKey(args: {
|
|
|
164
197
|
for (const [secondaryField, secondaryRaws] of secondaryRawByField) {
|
|
165
198
|
const pairUrlLike =
|
|
166
199
|
looksLikeUrl(primaryRaws) || looksLikeUrl(secondaryRaws);
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
secondaryRaws,
|
|
200
|
+
for (const primaryFormula of proposeFormulas(
|
|
201
|
+
primaryField,
|
|
202
|
+
primaryRaws,
|
|
171
203
|
pairUrlLike,
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
204
|
+
)) {
|
|
205
|
+
for (const secondaryFormula of proposeFormulas(
|
|
206
|
+
secondaryField,
|
|
207
|
+
secondaryRaws,
|
|
208
|
+
pairUrlLike,
|
|
209
|
+
)) {
|
|
210
|
+
const primary = profileFieldWithFormula(
|
|
211
|
+
primaryField,
|
|
212
|
+
primaryRaws,
|
|
213
|
+
primaryFormula,
|
|
214
|
+
);
|
|
215
|
+
const secondary = profileFieldWithFormula(
|
|
216
|
+
secondaryField,
|
|
217
|
+
secondaryRaws,
|
|
218
|
+
secondaryFormula,
|
|
219
|
+
);
|
|
220
|
+
const overlap = jaccard(
|
|
221
|
+
primary.normalizedSet,
|
|
222
|
+
secondary.normalizedSet,
|
|
223
|
+
);
|
|
224
|
+
if (overlap <= 0) continue;
|
|
225
|
+
const sharedCount = intersectionSize(
|
|
226
|
+
primary.normalizedSet,
|
|
227
|
+
secondary.normalizedSet,
|
|
228
|
+
);
|
|
229
|
+
if (
|
|
230
|
+
sharedCount < 2 &&
|
|
231
|
+
(primary.normalizedByRaw.size > 2 ||
|
|
232
|
+
secondary.normalizedByRaw.size > 2)
|
|
233
|
+
) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const lowInfoPenalty =
|
|
237
|
+
lowInformationField(primary.field) ||
|
|
238
|
+
lowInformationField(secondary.field)
|
|
239
|
+
? 0.5
|
|
240
|
+
: 0;
|
|
241
|
+
const cardinalityNudge =
|
|
242
|
+
Math.min(primary.normalizedSet.size, secondary.normalizedSet.size) *
|
|
243
|
+
0.001;
|
|
244
|
+
const score =
|
|
245
|
+
overlap +
|
|
246
|
+
nameNudge(primary.field, secondary.field) +
|
|
247
|
+
cardinalityNudge -
|
|
248
|
+
lowInfoPenalty;
|
|
249
|
+
if (!best || score > best.score) {
|
|
250
|
+
best = { primary, secondary, score, overlap };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
178
253
|
}
|
|
179
254
|
}
|
|
180
255
|
}
|
|
@@ -309,7 +309,8 @@ function upsertFrontmatter(
|
|
|
309
309
|
return body;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
|
|
312
|
+
const frontmatterBlock = `---\n${nextLines.filter(Boolean).join("\n")}\n---\n`;
|
|
313
|
+
return body ? `${frontmatterBlock}\n${body}` : frontmatterBlock;
|
|
313
314
|
}
|
|
314
315
|
|
|
315
316
|
function stripDuplicateTitleHeading(content: string, title: string) {
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
} from "./_builder-cms-read-client.js";
|
|
16
16
|
import type { BuilderCmsSourceEntry } from "./_builder-cms-source-adapter.js";
|
|
17
17
|
import {
|
|
18
|
+
databaseSourceExistsForTable,
|
|
19
|
+
ensureDatabaseSourceProperty,
|
|
18
20
|
getExistingSource,
|
|
19
21
|
getSourceRows,
|
|
20
22
|
importBuilderCmsEntriesAsDatabaseItems,
|
|
@@ -129,10 +131,22 @@ export default defineAction({
|
|
|
129
131
|
.string()
|
|
130
132
|
.optional()
|
|
131
133
|
.describe("Source table/model name, for example content_items."),
|
|
134
|
+
relationshipMode: z
|
|
135
|
+
.enum(["items", "details"])
|
|
136
|
+
.optional()
|
|
137
|
+
.describe(
|
|
138
|
+
"How to attach a second source: items adds more rows; details joins fields onto existing rows.",
|
|
139
|
+
),
|
|
132
140
|
join: joinSchema
|
|
133
141
|
.optional()
|
|
134
142
|
.describe(
|
|
135
|
-
"When
|
|
143
|
+
"When relationshipMode is details, the canonical-key join that adds fields onto the primary rows.",
|
|
144
|
+
),
|
|
145
|
+
mode: z
|
|
146
|
+
.enum(["replace", "add"])
|
|
147
|
+
.optional()
|
|
148
|
+
.describe(
|
|
149
|
+
"Backward-compatible alias: add means relationshipMode items; replace (default) re-links the primary source.",
|
|
136
150
|
),
|
|
137
151
|
limit: z.coerce.number().int().min(1).max(500).default(100),
|
|
138
152
|
offset: z.coerce.number().int().min(0).default(0),
|
|
@@ -160,10 +174,16 @@ export default defineAction({
|
|
|
160
174
|
await resolveReadableLocalTableSource(sourceTable);
|
|
161
175
|
}
|
|
162
176
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
const relationshipMode =
|
|
178
|
+
args.relationshipMode ?? (args.mode === "add" ? "items" : undefined);
|
|
179
|
+
|
|
180
|
+
// Adding a SECOND source as details: relate it onto the primary on the
|
|
181
|
+
// canonical key. Read-only overlay — the secondary's entries are NOT
|
|
182
|
+
// imported as local documents/items.
|
|
183
|
+
if ((relationshipMode === "details" || args.join) && existingSource) {
|
|
184
|
+
if (!args.join) {
|
|
185
|
+
throw new Error("Choose a match key before adding source details.");
|
|
186
|
+
}
|
|
167
187
|
let entries: BuilderCmsSourceEntry[];
|
|
168
188
|
let modelFields: BuilderCmsModelFieldSummary[];
|
|
169
189
|
if (sourceType === "builder-cms") {
|
|
@@ -229,6 +249,105 @@ export default defineAction({
|
|
|
229
249
|
});
|
|
230
250
|
}
|
|
231
251
|
|
|
252
|
+
// Adding an ADDITIONAL writable Builder source (row-union): insert a new
|
|
253
|
+
// source and import its entries as their OWN rows, instead of replacing the
|
|
254
|
+
// primary. No canonical-key join — each row belongs to exactly one source.
|
|
255
|
+
if (
|
|
256
|
+
relationshipMode === "items" &&
|
|
257
|
+
existingSource &&
|
|
258
|
+
sourceType === "builder-cms"
|
|
259
|
+
) {
|
|
260
|
+
// Don't add the same collection twice — each "add" starts a fresh source
|
|
261
|
+
// with no prior rows, so a duplicate attach would re-import duplicate rows.
|
|
262
|
+
if (await databaseSourceExistsForTable(database.id, sourceTable)) {
|
|
263
|
+
throw new Error(`"${sourceTable}" is already attached as a source.`);
|
|
264
|
+
}
|
|
265
|
+
const additionalRead = await readBuilderCmsContentEntries({
|
|
266
|
+
model: sourceTable,
|
|
267
|
+
});
|
|
268
|
+
const additionalModelFields = await readBuilderCmsModelFields({
|
|
269
|
+
model: sourceTable,
|
|
270
|
+
});
|
|
271
|
+
const additionalSourceId = await insertSecondarySource({
|
|
272
|
+
database,
|
|
273
|
+
sourceType,
|
|
274
|
+
sourceName,
|
|
275
|
+
sourceTable,
|
|
276
|
+
now,
|
|
277
|
+
});
|
|
278
|
+
// Snapshot existing items BEFORE importing so we can bind the new source
|
|
279
|
+
// to ONLY the rows it imports — never the primary's existing rows.
|
|
280
|
+
const beforeSetup = await sourceSetupPayload(database.id);
|
|
281
|
+
const priorDocumentIds = new Set(
|
|
282
|
+
beforeSetup.response.items.map((item) => item.document.id),
|
|
283
|
+
);
|
|
284
|
+
if (additionalRead.state === "live") {
|
|
285
|
+
await importBuilderCmsEntriesAsDatabaseItems({
|
|
286
|
+
database,
|
|
287
|
+
entries: additionalRead.entries,
|
|
288
|
+
now,
|
|
289
|
+
sourceTable,
|
|
290
|
+
existingSourceRows: [],
|
|
291
|
+
skipTitleDedup: true,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
const additionalSetup = await sourceSetupPayload(database.id);
|
|
295
|
+
// Only the items this collection just created — exclude the primary's.
|
|
296
|
+
const importedItems = additionalSetup.response.items.filter(
|
|
297
|
+
(item) => !priorDocumentIds.has(item.document.id),
|
|
298
|
+
);
|
|
299
|
+
const additionalEntriesByDocumentId =
|
|
300
|
+
additionalRead.state === "live"
|
|
301
|
+
? mapBuilderCmsEntriesToLocalItems({
|
|
302
|
+
entries: additionalRead.entries,
|
|
303
|
+
items: importedItems,
|
|
304
|
+
sourceTable,
|
|
305
|
+
now,
|
|
306
|
+
existingRows: [],
|
|
307
|
+
})
|
|
308
|
+
: undefined;
|
|
309
|
+
await seedMockSourceFields({
|
|
310
|
+
sourceId: additionalSourceId,
|
|
311
|
+
ownerEmail: database.ownerEmail,
|
|
312
|
+
sourceType,
|
|
313
|
+
properties: additionalSetup.properties,
|
|
314
|
+
builderModelFields: additionalModelFields,
|
|
315
|
+
builderSampleEntries:
|
|
316
|
+
additionalRead.state === "live" ? additionalRead.entries : [],
|
|
317
|
+
now,
|
|
318
|
+
});
|
|
319
|
+
await seedMockSourceRows({
|
|
320
|
+
sourceId: additionalSourceId,
|
|
321
|
+
ownerEmail: database.ownerEmail,
|
|
322
|
+
sourceType,
|
|
323
|
+
sourceTable,
|
|
324
|
+
items: importedItems,
|
|
325
|
+
now,
|
|
326
|
+
builderEntriesByDocumentId: additionalEntriesByDocumentId,
|
|
327
|
+
});
|
|
328
|
+
await updateBuilderCmsSourceReadMetadata({
|
|
329
|
+
sourceId: additionalSourceId,
|
|
330
|
+
sourceTable,
|
|
331
|
+
readState: additionalRead.state,
|
|
332
|
+
entryCount: additionalRead.entries.length,
|
|
333
|
+
matchedRowCount: additionalEntriesByDocumentId?.size ?? 0,
|
|
334
|
+
fetchedAt: additionalRead.fetchedAt,
|
|
335
|
+
now,
|
|
336
|
+
message: additionalRead.message,
|
|
337
|
+
syncState: "linked",
|
|
338
|
+
});
|
|
339
|
+
await ensureDatabaseSourceProperty({ database, now });
|
|
340
|
+
|
|
341
|
+
return getContentDatabaseResponse(database.id, {
|
|
342
|
+
limit: args.limit,
|
|
343
|
+
offset: args.offset,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (relationshipMode === "items" && existingSource) {
|
|
348
|
+
throw new Error("Only Builder sources can add more items right now.");
|
|
349
|
+
}
|
|
350
|
+
|
|
232
351
|
const existingSourceRows = existingSource
|
|
233
352
|
? await getSourceRows(existingSource.id)
|
|
234
353
|
: [];
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq, inArray, ne } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import type {
|
|
8
|
+
BindContentDatabaseSourceFieldRequest,
|
|
9
|
+
ContentDatabaseResponse,
|
|
10
|
+
} from "../shared/api.js";
|
|
11
|
+
import { serializePropertyValue } from "../shared/properties.js";
|
|
12
|
+
import { resolveDatabaseForSourceMutation } from "./_database-source-utils.js";
|
|
13
|
+
import { getContentDatabaseResponse } from "./_database-utils.js";
|
|
14
|
+
import { nanoid } from "./_property-utils.js";
|
|
15
|
+
import {
|
|
16
|
+
propertyTypeForSourceField,
|
|
17
|
+
sourceFieldPropertyValuesFromRows,
|
|
18
|
+
} from "./add-content-database-source-field-property.js";
|
|
19
|
+
|
|
20
|
+
const SOURCE_TAG_PROPERTY_NAME = "Source";
|
|
21
|
+
|
|
22
|
+
export default defineAction({
|
|
23
|
+
description:
|
|
24
|
+
"Bind a source field to an existing database column (row-union per-source field binding), or unbind it. Binding routes the source's per-row values into the shared column; types must be compatible. Pass propertyId: null to unbind.",
|
|
25
|
+
schema: z.object({
|
|
26
|
+
databaseId: z.string().optional().describe("Database ID"),
|
|
27
|
+
documentId: z.string().optional().describe("Database document/page ID"),
|
|
28
|
+
sourceFieldId: z.string().describe("Source field mapping ID"),
|
|
29
|
+
propertyId: z
|
|
30
|
+
.string()
|
|
31
|
+
.nullable()
|
|
32
|
+
.describe(
|
|
33
|
+
"Target column property to bind the field to, or null to unbind.",
|
|
34
|
+
),
|
|
35
|
+
}),
|
|
36
|
+
run: async (
|
|
37
|
+
args: BindContentDatabaseSourceFieldRequest,
|
|
38
|
+
): Promise<ContentDatabaseResponse> => {
|
|
39
|
+
const database = await resolveDatabaseForSourceMutation(args);
|
|
40
|
+
if (!database) throw new Error("Database not found.");
|
|
41
|
+
await assertAccess("document", database.documentId, "editor");
|
|
42
|
+
|
|
43
|
+
const db = getDb();
|
|
44
|
+
const [field] = await db
|
|
45
|
+
.select()
|
|
46
|
+
.from(schema.contentDatabaseSourceFields)
|
|
47
|
+
.where(eq(schema.contentDatabaseSourceFields.id, args.sourceFieldId));
|
|
48
|
+
if (!field) throw new Error("Source field not found.");
|
|
49
|
+
|
|
50
|
+
const [source] = await db
|
|
51
|
+
.select()
|
|
52
|
+
.from(schema.contentDatabaseSources)
|
|
53
|
+
.where(
|
|
54
|
+
and(
|
|
55
|
+
eq(schema.contentDatabaseSources.id, field.sourceId),
|
|
56
|
+
eq(schema.contentDatabaseSources.databaseId, database.id),
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
if (!source) {
|
|
60
|
+
throw new Error("Source field does not belong to this database.");
|
|
61
|
+
}
|
|
62
|
+
if (field.mappingType === "title") {
|
|
63
|
+
throw new Error("The title field is bound to Name and can't be rebound.");
|
|
64
|
+
}
|
|
65
|
+
if (field.mappingType === "system" || field.writeOwner === "derived") {
|
|
66
|
+
throw new Error("Integration-managed fields can't be bound to a column.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
|
|
71
|
+
// ── Unbind ────────────────────────────────────────────────────────────
|
|
72
|
+
if (args.propertyId === null) {
|
|
73
|
+
if (field.propertyId) {
|
|
74
|
+
const sourceRows = await db
|
|
75
|
+
.select({ documentId: schema.contentDatabaseSourceRows.documentId })
|
|
76
|
+
.from(schema.contentDatabaseSourceRows)
|
|
77
|
+
.where(eq(schema.contentDatabaseSourceRows.sourceId, source.id));
|
|
78
|
+
const sourceDocumentIds = sourceRows
|
|
79
|
+
.map((row) => row.documentId)
|
|
80
|
+
.filter((id): id is string => Boolean(id));
|
|
81
|
+
if (sourceDocumentIds.length > 0) {
|
|
82
|
+
await db
|
|
83
|
+
.delete(schema.documentPropertyValues)
|
|
84
|
+
.where(
|
|
85
|
+
and(
|
|
86
|
+
eq(schema.documentPropertyValues.propertyId, field.propertyId),
|
|
87
|
+
inArray(
|
|
88
|
+
schema.documentPropertyValues.documentId,
|
|
89
|
+
sourceDocumentIds,
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
await db
|
|
96
|
+
.update(schema.contentDatabaseSourceFields)
|
|
97
|
+
.set({
|
|
98
|
+
propertyId: null,
|
|
99
|
+
localFieldKey: field.sourceFieldKey,
|
|
100
|
+
updatedAt: now,
|
|
101
|
+
})
|
|
102
|
+
.where(eq(schema.contentDatabaseSourceFields.id, field.id));
|
|
103
|
+
await db
|
|
104
|
+
.update(schema.contentDatabaseSources)
|
|
105
|
+
.set({ updatedAt: now })
|
|
106
|
+
.where(eq(schema.contentDatabaseSources.id, source.id));
|
|
107
|
+
return getContentDatabaseResponse(database.id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Bind to an existing column ─────────────────────────────────────────
|
|
111
|
+
const [property] = await db
|
|
112
|
+
.select()
|
|
113
|
+
.from(schema.documentPropertyDefinitions)
|
|
114
|
+
.where(
|
|
115
|
+
and(
|
|
116
|
+
eq(schema.documentPropertyDefinitions.id, args.propertyId),
|
|
117
|
+
eq(schema.documentPropertyDefinitions.databaseId, database.id),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
if (!property) {
|
|
121
|
+
throw new Error("Target column does not belong to this database.");
|
|
122
|
+
}
|
|
123
|
+
// The auto-created "Source" tag is internal row-tagging, never a writable
|
|
124
|
+
// bind target.
|
|
125
|
+
if (
|
|
126
|
+
property.name === SOURCE_TAG_PROPERTY_NAME &&
|
|
127
|
+
property.type === "select"
|
|
128
|
+
) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"The Source tag column can't be bound to a source field.",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
// Don't silently repoint a field that's already feeding another column —
|
|
134
|
+
// that would orphan the old column's materialized values. Require an
|
|
135
|
+
// explicit unbind first. (Re-binding to the SAME column is an idempotent
|
|
136
|
+
// refresh and is allowed.)
|
|
137
|
+
if (field.propertyId && field.propertyId !== property.id) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"This source field is already bound to another column. Unbind it first.",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
// At most one field per source per column: a column reads one value per row,
|
|
143
|
+
// and a row belongs to one source. Enforce server-side, not just in the UI.
|
|
144
|
+
const [conflictingField] = await db
|
|
145
|
+
.select({ id: schema.contentDatabaseSourceFields.id })
|
|
146
|
+
.from(schema.contentDatabaseSourceFields)
|
|
147
|
+
.where(
|
|
148
|
+
and(
|
|
149
|
+
eq(schema.contentDatabaseSourceFields.sourceId, source.id),
|
|
150
|
+
eq(schema.contentDatabaseSourceFields.propertyId, property.id),
|
|
151
|
+
ne(schema.contentDatabaseSourceFields.id, field.id),
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
if (conflictingField) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
"This source already feeds this column from another field. Unbind it first.",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
// Only type-compatible fields can share a column. A `text` column is a
|
|
160
|
+
// permissive target for SCALAR fields; a multi-value (list) field would be
|
|
161
|
+
// lossily stringified, so it needs a matching list/multi-select column.
|
|
162
|
+
// Otherwise the field's derived type must equal the column's type.
|
|
163
|
+
const fieldType = propertyTypeForSourceField(field.sourceFieldType);
|
|
164
|
+
const fieldIsMultiValue = [
|
|
165
|
+
"list",
|
|
166
|
+
"array",
|
|
167
|
+
"tags",
|
|
168
|
+
"multi_select",
|
|
169
|
+
].includes(field.sourceFieldType.trim().toLowerCase());
|
|
170
|
+
if (property.type === "text") {
|
|
171
|
+
if (fieldIsMultiValue) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"A multi-value source field can't be bound to a text column.",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
} else if (property.type !== fieldType) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Field type "${fieldType}" is not compatible with the "${property.type}" column.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await db
|
|
183
|
+
.update(schema.contentDatabaseSourceFields)
|
|
184
|
+
.set({
|
|
185
|
+
propertyId: property.id,
|
|
186
|
+
localFieldKey: property.id,
|
|
187
|
+
mappingType: "property",
|
|
188
|
+
updatedAt: now,
|
|
189
|
+
})
|
|
190
|
+
.where(eq(schema.contentDatabaseSourceFields.id, field.id));
|
|
191
|
+
await db
|
|
192
|
+
.update(schema.contentDatabaseSources)
|
|
193
|
+
.set({ updatedAt: now })
|
|
194
|
+
.where(eq(schema.contentDatabaseSources.id, source.id));
|
|
195
|
+
|
|
196
|
+
// Backfill the column with this source's per-row values. A federated
|
|
197
|
+
// secondary's rows carry no local document (the read path overlays them),
|
|
198
|
+
// so only materialize for document-backed sources.
|
|
199
|
+
let federationRole: string | null = null;
|
|
200
|
+
try {
|
|
201
|
+
const parsed = JSON.parse(source.metadataJson ?? "{}") as {
|
|
202
|
+
federation?: { role?: string };
|
|
203
|
+
};
|
|
204
|
+
federationRole = parsed.federation?.role ?? null;
|
|
205
|
+
} catch {
|
|
206
|
+
federationRole = null;
|
|
207
|
+
}
|
|
208
|
+
if (federationRole !== "secondary") {
|
|
209
|
+
const sourceRows = await db
|
|
210
|
+
.select()
|
|
211
|
+
.from(schema.contentDatabaseSourceRows)
|
|
212
|
+
.where(eq(schema.contentDatabaseSourceRows.sourceId, source.id));
|
|
213
|
+
const itemValues = sourceFieldPropertyValuesFromRows(
|
|
214
|
+
sourceRows,
|
|
215
|
+
field.sourceFieldKey,
|
|
216
|
+
property.type,
|
|
217
|
+
);
|
|
218
|
+
// Clear this column's values for ALL of this source's rows first — not
|
|
219
|
+
// just the rows that now have a value — so a row whose new bound field is
|
|
220
|
+
// empty doesn't keep showing a stale/previous value. Then write the
|
|
221
|
+
// non-empty ones. (This source owns these documents' values for the row-
|
|
222
|
+
// union, so clearing them is safe.)
|
|
223
|
+
const sourceDocumentIds = sourceRows
|
|
224
|
+
.map((row) => row.documentId)
|
|
225
|
+
.filter((id): id is string => Boolean(id));
|
|
226
|
+
if (sourceDocumentIds.length > 0) {
|
|
227
|
+
await db
|
|
228
|
+
.delete(schema.documentPropertyValues)
|
|
229
|
+
.where(
|
|
230
|
+
and(
|
|
231
|
+
eq(schema.documentPropertyValues.propertyId, property.id),
|
|
232
|
+
inArray(
|
|
233
|
+
schema.documentPropertyValues.documentId,
|
|
234
|
+
sourceDocumentIds,
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (itemValues.length > 0) {
|
|
240
|
+
await db.insert(schema.documentPropertyValues).values(
|
|
241
|
+
itemValues.map((row) => ({
|
|
242
|
+
id: nanoid(),
|
|
243
|
+
ownerEmail: database.ownerEmail,
|
|
244
|
+
documentId: row.documentId,
|
|
245
|
+
propertyId: property.id,
|
|
246
|
+
valueJson: serializePropertyValue(row.value),
|
|
247
|
+
createdAt: now,
|
|
248
|
+
updatedAt: now,
|
|
249
|
+
})),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return getContentDatabaseResponse(database.id);
|
|
255
|
+
},
|
|
256
|
+
});
|