@agent-native/core 0.123.0 → 0.123.2
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 +1 -1
- package/corpus/core/CHANGELOG.md +24 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/templates-meta.ts +12 -0
- package/corpus/core/src/deploy/build.ts +8 -0
- package/corpus/core/src/server/builder-browser.ts +76 -0
- package/corpus/core/src/server/core-routes-plugin.ts +15 -5
- package/corpus/templates/assets/app/global.css +0 -4
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +195 -167
- package/corpus/templates/assets/changelog/2026-07-23-drafts-preview-panel-now-fills-the-full-width-with-a-large-p.md +6 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +55 -9
- package/corpus/templates/content/actions/_database-source-utils.ts +756 -182
- package/corpus/templates/content/actions/change-content-database-source-role.ts +21 -20
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +49 -8
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +42 -9
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +88 -90
- package/corpus/templates/content/actions/preview-builder-source-review.ts +4 -3
- package/corpus/templates/content/actions/refresh-content-database-source.ts +36 -9
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +28 -24
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +62 -12
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +91 -16
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +15 -1
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +234 -44
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +194 -16
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +95 -42
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +4 -2
- package/corpus/templates/content/app/components/editor/extensions/CodeBlockNode.tsx +10 -166
- package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +32 -8
- package/corpus/templates/content/app/components/editor/extensions/VideoNode.ts +14 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +5 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +59 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +18 -26
- package/corpus/templates/content/app/root.tsx +1 -6
- package/corpus/templates/content/changelog/2026-07-24-builder-can-now-publish-approved-updates-to-entries-that-are.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-collection-loading-no-longer-restarts-from-the-begin.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-connections-now-complete-securely-from-netlify-deplo.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-draft-creation-now-scopes-execution-to-the-selected-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-review-now-distinguishes-preparing-a-safe-dry-run-from.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-imports-now-recover-from-a-transient-paginati.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-refresh-now-preserves-the-provider-bound-fiel.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-update-reviews-now-open-reliably-on-large-collection.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-write-policy-controls-now-reflect-successful-changes.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-date-properties-now-save-reliably-when-selected-with-the-nat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-editor-slash-search-now-keeps-multi-word-block-names-open.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-exact-editor-slash-commands-now-execute-on-enter-even-if-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-collaborative-pages-now-save-their-first-edits.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-local-builder-rows-no-longer-remain-locked-behind-a-none.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-opening-or-closing-a-collaborative-document-no-longer-lets-u.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-selected-text-now-opens-the-link-editor-with-command-k-or-co.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-slash-menu-block-choices-no-longer-stall-pointer-interaction.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-uploaded-and-embedded-media-now-stay-intact-while-collaborative-document-drafts-save.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-video-link-insertion-now-keeps-its-source-panel-open-while-t.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-publishing-now-shows-safe-validation-details-instead.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-reviews-stay-responsive-on-large-collections-and-pre.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-collaborative-documents-no-longer-flash-duplicated-recent-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-corrected-builder-updates-are-now-reviewed-separately-from-f.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-documents-now-remain-read-only-until-their-live-collaborativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-slash-command-menus-now-stay-reachable-when-editing-near-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-the-slash-menu-now-shows-one-clear-callout-choice-instead-of.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/content/shared/builder-mdx.ts +73 -16
- package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
- package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
- package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
- package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
- package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
- package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
- package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
- package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
- package/corpus/templates/crm/.env.example +5 -0
- package/corpus/templates/crm/AGENTS.md +82 -0
- package/corpus/templates/crm/CHANGELOG.md +25 -0
- package/corpus/templates/crm/DEVELOPING.md +66 -0
- package/corpus/templates/crm/README.md +122 -0
- package/corpus/templates/crm/_gitignore +12 -0
- package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
- package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
- package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
- package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
- package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
- package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
- package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
- package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
- package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
- package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
- package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
- package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
- package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
- package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
- package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
- package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
- package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
- package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
- package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
- package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
- package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
- package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
- package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
- package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
- package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
- package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
- package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
- package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
- package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
- package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
- package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
- package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
- package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
- package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
- package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
- package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
- package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
- package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
- package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
- package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
- package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
- package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
- package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
- package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
- package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
- package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
- package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
- package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
- package/corpus/templates/crm/actions/navigate.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
- package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
- package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
- package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
- package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
- package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
- package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
- package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
- package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
- package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
- package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
- package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
- package/corpus/templates/crm/actions/run.ts +7 -0
- package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
- package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
- package/corpus/templates/crm/actions/sync-crm.ts +146 -0
- package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
- package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
- package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
- package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
- package/corpus/templates/crm/actions/view-screen.ts +324 -0
- package/corpus/templates/crm/agent-native.app-skill.json +69 -0
- package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
- package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
- package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
- package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
- package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
- package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
- package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
- package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
- package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
- package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
- package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
- package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
- package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
- package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
- package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
- package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
- package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
- package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
- package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
- package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
- package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
- package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
- package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
- package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
- package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
- package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
- package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
- package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
- package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
- package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
- package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
- package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
- package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
- package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
- package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
- package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
- package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
- package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
- package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
- package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
- package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
- package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
- package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
- package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/crm/app/design-system.ts +3 -0
- package/corpus/templates/crm/app/entry.client.tsx +19 -0
- package/corpus/templates/crm/app/entry.server.tsx +10 -0
- package/corpus/templates/crm/app/global.css +167 -0
- package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
- package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
- package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
- package/corpus/templates/crm/app/i18n/index.ts +34 -0
- package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
- package/corpus/templates/crm/app/lib/navigation.ts +17 -0
- package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
- package/corpus/templates/crm/app/lib/types.ts +172 -0
- package/corpus/templates/crm/app/lib/utils.ts +1 -0
- package/corpus/templates/crm/app/root.tsx +93 -0
- package/corpus/templates/crm/app/routes/_index.tsx +26 -0
- package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
- package/corpus/templates/crm/app/routes/agent.tsx +5 -0
- package/corpus/templates/crm/app/routes/ask.tsx +53 -0
- package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
- package/corpus/templates/crm/app/routes/lists.tsx +228 -0
- package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
- package/corpus/templates/crm/app/routes/people.tsx +41 -0
- package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
- package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
- package/corpus/templates/crm/app/routes/records.tsx +64 -0
- package/corpus/templates/crm/app/routes/settings.tsx +118 -0
- package/corpus/templates/crm/app/routes/setup.tsx +308 -0
- package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
- package/corpus/templates/crm/app/routes/views.tsx +683 -0
- package/corpus/templates/crm/app/routes.ts +19 -0
- package/corpus/templates/crm/app/vite-env.d.ts +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-any-list-or-view-can-be-shown-as-a-board-grouped-by-stage-so.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-press-cmd-k-to-jump-to-any-record-list-view-or-setting-and-u.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
- package/corpus/templates/crm/components.json +20 -0
- package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
- package/corpus/templates/crm/docs/features/README.md +55 -0
- package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
- package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
- package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
- package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
- package/corpus/templates/crm/learnings.defaults.md +5 -0
- package/corpus/templates/crm/netlify.toml +11 -0
- package/corpus/templates/crm/package.json +61 -0
- package/corpus/templates/crm/public/favicon.svg +1 -0
- package/corpus/templates/crm/public/icon-180.svg +1 -0
- package/corpus/templates/crm/public/manifest.json +11 -0
- package/corpus/templates/crm/react-router.config.ts +7 -0
- package/corpus/templates/crm/server/crm/adapter.ts +32 -0
- package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
- package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
- package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
- package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
- package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
- package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
- package/corpus/templates/crm/server/crm/read-through.ts +207 -0
- package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
- package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
- package/corpus/templates/crm/server/db/index.ts +191 -0
- package/corpus/templates/crm/server/db/schema.ts +647 -0
- package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
- package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
- package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
- package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
- package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
- package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
- package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
- package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
- package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
- package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
- package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
- package/corpus/templates/crm/server/middleware/auth.ts +4 -0
- package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
- package/corpus/templates/crm/server/plugins/auth.ts +14 -0
- package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
- package/corpus/templates/crm/server/plugins/db.ts +738 -0
- package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
- package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
- package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
- package/corpus/templates/crm/shared/crm-contract.ts +417 -0
- package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
- package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
- package/corpus/templates/crm/ssr-entry.ts +11 -0
- package/corpus/templates/crm/tsconfig.json +24 -0
- package/corpus/templates/crm/vite.config.ts +15 -0
- package/corpus/templates/crm/vitest.config.ts +5 -0
- package/corpus/templates/design/app/i18n-data.ts +12 -12
- package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/editor/useCollabReconcile.ts +14 -1
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +12 -0
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/deploy/build.js +6 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/builder-browser.d.ts +13 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +59 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +10 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
- package/src/cli/templates-meta.ts +12 -0
- package/src/deploy/build.ts +8 -0
- package/src/server/builder-browser.ts +76 -0
- package/src/server/core-routes-plugin.ts +15 -5
|
@@ -162,6 +162,9 @@ type SourceMetadataRecord = {
|
|
|
162
162
|
lastReadSuspiciousEmpty?: boolean;
|
|
163
163
|
activeReadSourceRowIds?: string[];
|
|
164
164
|
sourceFetchState?: "idle" | "fetching" | "error" | string;
|
|
165
|
+
builderContinuationClaimId?: string;
|
|
166
|
+
builderContinuationClaimOffset?: number;
|
|
167
|
+
builderContinuationClaimedAt?: string;
|
|
165
168
|
allowDraftWrites?: boolean;
|
|
166
169
|
allowPublishWrites?: boolean;
|
|
167
170
|
allowedWriteModes?: ContentDatabaseSourcePushMode[];
|
|
@@ -183,6 +186,229 @@ function parseObject<T extends object>(
|
|
|
183
186
|
}
|
|
184
187
|
}
|
|
185
188
|
|
|
189
|
+
type ContentDatabaseSourceMetadataMutationRow = Pick<
|
|
190
|
+
ContentDatabaseSourceRowDb,
|
|
191
|
+
| "capabilitiesJson"
|
|
192
|
+
| "metadataJson"
|
|
193
|
+
| "sourceTable"
|
|
194
|
+
| "sourceType"
|
|
195
|
+
| "lastSourceUpdatedAt"
|
|
196
|
+
>;
|
|
197
|
+
|
|
198
|
+
type ContentDatabaseSourceMetadataMutationPatch = Pick<
|
|
199
|
+
ContentDatabaseSourceRowDb,
|
|
200
|
+
"metadataJson"
|
|
201
|
+
> &
|
|
202
|
+
Partial<
|
|
203
|
+
Pick<
|
|
204
|
+
ContentDatabaseSourceRowDb,
|
|
205
|
+
| "capabilitiesJson"
|
|
206
|
+
| "syncState"
|
|
207
|
+
| "freshness"
|
|
208
|
+
| "lastRefreshedAt"
|
|
209
|
+
| "lastSourceUpdatedAt"
|
|
210
|
+
| "lastError"
|
|
211
|
+
>
|
|
212
|
+
>;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Merge source metadata against the latest persisted row and compare-and-swap
|
|
216
|
+
* both JSON envelopes. Refresh ownership lives in metadataJson, so every
|
|
217
|
+
* unrelated writer must retry from the winning value instead of restoring a
|
|
218
|
+
* stale claim snapshot.
|
|
219
|
+
*/
|
|
220
|
+
export async function mutateContentDatabaseSourceMetadata(args: {
|
|
221
|
+
sourceId: string;
|
|
222
|
+
now: string;
|
|
223
|
+
buildPatch: (
|
|
224
|
+
current: ContentDatabaseSourceMetadataMutationRow,
|
|
225
|
+
) =>
|
|
226
|
+
| ContentDatabaseSourceMetadataMutationPatch
|
|
227
|
+
| Promise<ContentDatabaseSourceMetadataMutationPatch>;
|
|
228
|
+
failureMessage?: string;
|
|
229
|
+
}) {
|
|
230
|
+
const db = getDb();
|
|
231
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
232
|
+
const [current] = await db
|
|
233
|
+
.select({
|
|
234
|
+
capabilitiesJson: schema.contentDatabaseSources.capabilitiesJson,
|
|
235
|
+
metadataJson: schema.contentDatabaseSources.metadataJson,
|
|
236
|
+
sourceTable: schema.contentDatabaseSources.sourceTable,
|
|
237
|
+
sourceType: schema.contentDatabaseSources.sourceType,
|
|
238
|
+
lastSourceUpdatedAt: schema.contentDatabaseSources.lastSourceUpdatedAt,
|
|
239
|
+
})
|
|
240
|
+
.from(schema.contentDatabaseSources)
|
|
241
|
+
.where(eq(schema.contentDatabaseSources.id, args.sourceId))
|
|
242
|
+
.limit(1);
|
|
243
|
+
if (!current) throw new Error("Content database source not found.");
|
|
244
|
+
|
|
245
|
+
const patch = await args.buildPatch(current);
|
|
246
|
+
const updated = await db
|
|
247
|
+
.update(schema.contentDatabaseSources)
|
|
248
|
+
.set({ ...patch, updatedAt: args.now })
|
|
249
|
+
.where(
|
|
250
|
+
and(
|
|
251
|
+
eq(schema.contentDatabaseSources.id, args.sourceId),
|
|
252
|
+
eq(
|
|
253
|
+
schema.contentDatabaseSources.capabilitiesJson,
|
|
254
|
+
current.capabilitiesJson,
|
|
255
|
+
),
|
|
256
|
+
current.metadataJson === null
|
|
257
|
+
? isNull(schema.contentDatabaseSources.metadataJson)
|
|
258
|
+
: eq(
|
|
259
|
+
schema.contentDatabaseSources.metadataJson,
|
|
260
|
+
current.metadataJson,
|
|
261
|
+
),
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
.returning({ id: schema.contentDatabaseSources.id });
|
|
265
|
+
if (updated.length > 0) return;
|
|
266
|
+
}
|
|
267
|
+
throw new Error(
|
|
268
|
+
args.failureMessage ??
|
|
269
|
+
"Content database source metadata changed repeatedly; retry the action.",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function builderCmsSourceContinuationIsCurrent(
|
|
274
|
+
metadataJson: string | null | undefined,
|
|
275
|
+
expectedOffset: number,
|
|
276
|
+
) {
|
|
277
|
+
const metadata = parseObject<SourceMetadataRecord>(metadataJson);
|
|
278
|
+
return (
|
|
279
|
+
metadata?.sourceFetchState === "fetching" &&
|
|
280
|
+
metadata.lastReadHasMore === true &&
|
|
281
|
+
expectedOffset > 0 &&
|
|
282
|
+
metadata.lastReadNextOffset === expectedOffset &&
|
|
283
|
+
Array.isArray(metadata.activeReadSourceRowIds) &&
|
|
284
|
+
metadata.activeReadSourceRowIds.length > 0 &&
|
|
285
|
+
metadata.activeReadSourceRowIds.every((id) => typeof id === "string")
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Content's hosted request wall is 75 seconds. Keep the orphan-recovery lease
|
|
290
|
+
// well beyond that so an expired owner cannot still be executing mutations.
|
|
291
|
+
export const BUILDER_CMS_REFRESH_CLAIM_LEASE_MS = 30 * 60 * 1000;
|
|
292
|
+
|
|
293
|
+
export async function claimBuilderCmsSourceRefresh(args: {
|
|
294
|
+
source: ContentDatabaseSourceRowDb;
|
|
295
|
+
expectedOffset?: number;
|
|
296
|
+
now?: string;
|
|
297
|
+
}) {
|
|
298
|
+
const metadata =
|
|
299
|
+
parseObject<SourceMetadataRecord>(args.source.metadataJson) ?? {};
|
|
300
|
+
const now = args.now ?? new Date().toISOString();
|
|
301
|
+
const claimedAt = metadata.builderContinuationClaimedAt
|
|
302
|
+
? Date.parse(metadata.builderContinuationClaimedAt)
|
|
303
|
+
: Number.NaN;
|
|
304
|
+
const activeClaim =
|
|
305
|
+
!!metadata.builderContinuationClaimId &&
|
|
306
|
+
Number.isFinite(claimedAt) &&
|
|
307
|
+
Date.parse(now) - claimedAt < BUILDER_CMS_REFRESH_CLAIM_LEASE_MS;
|
|
308
|
+
if (
|
|
309
|
+
activeClaim ||
|
|
310
|
+
(args.expectedOffset !== undefined &&
|
|
311
|
+
!builderCmsSourceContinuationIsCurrent(
|
|
312
|
+
args.source.metadataJson,
|
|
313
|
+
args.expectedOffset,
|
|
314
|
+
))
|
|
315
|
+
) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const claimId = nanoid();
|
|
320
|
+
const claimedMetadataJson = JSON.stringify({
|
|
321
|
+
...metadata,
|
|
322
|
+
builderContinuationClaimId: claimId,
|
|
323
|
+
builderContinuationClaimOffset: args.expectedOffset,
|
|
324
|
+
builderContinuationClaimedAt: now,
|
|
325
|
+
});
|
|
326
|
+
const [claimed] = await getDb()
|
|
327
|
+
.update(schema.contentDatabaseSources)
|
|
328
|
+
.set({ metadataJson: claimedMetadataJson })
|
|
329
|
+
.where(
|
|
330
|
+
and(
|
|
331
|
+
eq(schema.contentDatabaseSources.id, args.source.id),
|
|
332
|
+
args.source.metadataJson === null
|
|
333
|
+
? isNull(schema.contentDatabaseSources.metadataJson)
|
|
334
|
+
: eq(
|
|
335
|
+
schema.contentDatabaseSources.metadataJson,
|
|
336
|
+
args.source.metadataJson,
|
|
337
|
+
),
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
.returning();
|
|
341
|
+
return claimed ? { source: claimed, claimId } : null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function releaseBuilderCmsSourceRefreshClaim(args: {
|
|
345
|
+
sourceId: string;
|
|
346
|
+
claimId: string;
|
|
347
|
+
}) {
|
|
348
|
+
const db = getDb();
|
|
349
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
350
|
+
const [current] = await db
|
|
351
|
+
.select({ metadataJson: schema.contentDatabaseSources.metadataJson })
|
|
352
|
+
.from(schema.contentDatabaseSources)
|
|
353
|
+
.where(eq(schema.contentDatabaseSources.id, args.sourceId))
|
|
354
|
+
.limit(1);
|
|
355
|
+
const metadata = parseObject<SourceMetadataRecord>(current?.metadataJson);
|
|
356
|
+
if (!metadata || metadata.builderContinuationClaimId !== args.claimId) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
delete metadata.builderContinuationClaimId;
|
|
360
|
+
delete metadata.builderContinuationClaimOffset;
|
|
361
|
+
delete metadata.builderContinuationClaimedAt;
|
|
362
|
+
const nextMetadataJson = JSON.stringify(metadata);
|
|
363
|
+
const released = await db
|
|
364
|
+
.update(schema.contentDatabaseSources)
|
|
365
|
+
.set({ metadataJson: nextMetadataJson })
|
|
366
|
+
.where(
|
|
367
|
+
and(
|
|
368
|
+
eq(schema.contentDatabaseSources.id, args.sourceId),
|
|
369
|
+
eq(schema.contentDatabaseSources.metadataJson, current.metadataJson!),
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
.returning({ id: schema.contentDatabaseSources.id });
|
|
373
|
+
if (released.length > 0) return true;
|
|
374
|
+
}
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function renewBuilderCmsSourceRefreshClaim(args: {
|
|
379
|
+
sourceId: string;
|
|
380
|
+
claimId: string;
|
|
381
|
+
now?: string;
|
|
382
|
+
}) {
|
|
383
|
+
const db = getDb();
|
|
384
|
+
const now = args.now ?? new Date().toISOString();
|
|
385
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
386
|
+
const [current] = await db
|
|
387
|
+
.select({ metadataJson: schema.contentDatabaseSources.metadataJson })
|
|
388
|
+
.from(schema.contentDatabaseSources)
|
|
389
|
+
.where(eq(schema.contentDatabaseSources.id, args.sourceId))
|
|
390
|
+
.limit(1);
|
|
391
|
+
const metadata = parseObject<SourceMetadataRecord>(current?.metadataJson);
|
|
392
|
+
if (!metadata || metadata.builderContinuationClaimId !== args.claimId) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
metadata.builderContinuationClaimedAt = now;
|
|
396
|
+
const nextMetadataJson = JSON.stringify(metadata);
|
|
397
|
+
const renewed = await db
|
|
398
|
+
.update(schema.contentDatabaseSources)
|
|
399
|
+
.set({ metadataJson: nextMetadataJson })
|
|
400
|
+
.where(
|
|
401
|
+
and(
|
|
402
|
+
eq(schema.contentDatabaseSources.id, args.sourceId),
|
|
403
|
+
eq(schema.contentDatabaseSources.metadataJson, current.metadataJson!),
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
.returning({ id: schema.contentDatabaseSources.id });
|
|
407
|
+
if (renewed.length > 0) return true;
|
|
408
|
+
}
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
186
412
|
function parseArray<T>(value: string | null | undefined): T[] {
|
|
187
413
|
if (!value) return [];
|
|
188
414
|
try {
|
|
@@ -646,6 +872,26 @@ function stableValueString(value: unknown): string {
|
|
|
646
872
|
return JSON.stringify(value);
|
|
647
873
|
}
|
|
648
874
|
|
|
875
|
+
export function reviewedBuilderChangeSetRevisionId(
|
|
876
|
+
changeSet: ContentDatabaseSourceChangeSet,
|
|
877
|
+
) {
|
|
878
|
+
const revision = createHash("sha256")
|
|
879
|
+
.update(
|
|
880
|
+
stableValueString({
|
|
881
|
+
databaseItemId: changeSet.databaseItemId,
|
|
882
|
+
documentId: changeSet.documentId,
|
|
883
|
+
kind: changeSet.kind,
|
|
884
|
+
direction: "outbound",
|
|
885
|
+
pushMode: changeSet.pushMode ?? "autosave",
|
|
886
|
+
fieldChanges: changeSet.fieldChanges,
|
|
887
|
+
bodyChange: changeSet.bodyChange,
|
|
888
|
+
}),
|
|
889
|
+
)
|
|
890
|
+
.digest("hex")
|
|
891
|
+
.slice(0, 16);
|
|
892
|
+
return `${changeSet.id}-revision-${revision}`;
|
|
893
|
+
}
|
|
894
|
+
|
|
649
895
|
// Equal when both normalize the same. null/undefined/"" are all "empty"; strings
|
|
650
896
|
// are trimmed; objects compared by stable serialization.
|
|
651
897
|
function sameSourceFieldValue(a: unknown, b: unknown): boolean {
|
|
@@ -701,6 +947,15 @@ function normalizeBuilderBodyBaselineContent(value: string | null | undefined) {
|
|
|
701
947
|
return (value ?? "").replace(/\r\n/g, "\n").trim();
|
|
702
948
|
}
|
|
703
949
|
|
|
950
|
+
function builderBodyUsesCurrentMediaConverter(
|
|
951
|
+
content: string | null | undefined,
|
|
952
|
+
) {
|
|
953
|
+
return (
|
|
954
|
+
/!\[[^\]]*\]\(\s*https?:\/\//i.test(content ?? "") ||
|
|
955
|
+
/<(?:img|video)\b/i.test(content ?? "")
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
704
959
|
const BUILDER_BODY_HYDRATION_BACKGROUND_PRIORITY = 10;
|
|
705
960
|
const BUILDER_BODY_HYDRATION_OPEN_PRIORITY = 0;
|
|
706
961
|
const BUILDER_BODY_HYDRATION_BATCH_LIMIT = 25;
|
|
@@ -2355,8 +2610,7 @@ export async function builderBodyChangeForLocalContent(args: {
|
|
|
2355
2610
|
// legacy Text block containing a Markdown image into a real Builder Image
|
|
2356
2611
|
// (or emit a native Video) without changing the document text at all.
|
|
2357
2612
|
const usesCurrentMediaConverter =
|
|
2358
|
-
|
|
2359
|
-
/<(?:img|video)\b/i.test(localContent);
|
|
2613
|
+
builderBodyUsesCurrentMediaConverter(localContent);
|
|
2360
2614
|
const normalizedLocalContent =
|
|
2361
2615
|
normalizeBuilderBodyBaselineContent(localContent);
|
|
2362
2616
|
if (
|
|
@@ -2624,6 +2878,16 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2624
2878
|
stableValueString(candidate.bodyChange),
|
|
2625
2879
|
);
|
|
2626
2880
|
|
|
2881
|
+
const withUniqueReviewIdentity = (
|
|
2882
|
+
candidate: ContentDatabaseSourceChangeSet,
|
|
2883
|
+
): ContentDatabaseSourceChangeSet =>
|
|
2884
|
+
args.storedChangeSets.some((stored) => stored.id === candidate.id)
|
|
2885
|
+
? {
|
|
2886
|
+
...candidate,
|
|
2887
|
+
id: reviewedBuilderChangeSetRevisionId(candidate),
|
|
2888
|
+
}
|
|
2889
|
+
: candidate;
|
|
2890
|
+
|
|
2627
2891
|
const reviewableBuilderValue = (
|
|
2628
2892
|
field: NonNullable<typeof args.writableFields>[number],
|
|
2629
2893
|
localValue: unknown,
|
|
@@ -2842,7 +3106,9 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2842
3106
|
createdAt: now,
|
|
2843
3107
|
updatedAt: now,
|
|
2844
3108
|
};
|
|
2845
|
-
if (!sameCancelledSnapshot(candidate))
|
|
3109
|
+
if (!sameCancelledSnapshot(candidate)) {
|
|
3110
|
+
pending.push(withUniqueReviewIdentity(candidate));
|
|
3111
|
+
}
|
|
2846
3112
|
}
|
|
2847
3113
|
|
|
2848
3114
|
// New-row creates: a local database item NOT linked to a Builder entry (no
|
|
@@ -2934,7 +3200,9 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
2934
3200
|
createdAt: now,
|
|
2935
3201
|
updatedAt: now,
|
|
2936
3202
|
};
|
|
2937
|
-
if (!sameCancelledSnapshot(candidate))
|
|
3203
|
+
if (!sameCancelledSnapshot(candidate)) {
|
|
3204
|
+
pending.push(withUniqueReviewIdentity(candidate));
|
|
3205
|
+
}
|
|
2938
3206
|
}
|
|
2939
3207
|
}
|
|
2940
3208
|
|
|
@@ -3064,6 +3332,181 @@ export async function getContentDatabaseSourceSnapshotForWrite(
|
|
|
3064
3332
|
: null;
|
|
3065
3333
|
}
|
|
3066
3334
|
|
|
3335
|
+
export function knownBuilderReviewDocumentIds(
|
|
3336
|
+
changeSets: Array<{ documentId: string | null }>,
|
|
3337
|
+
limit: number,
|
|
3338
|
+
): string[] | null {
|
|
3339
|
+
if (changeSets.length === 0 || changeSets.length > limit) return null;
|
|
3340
|
+
const documentIds = new Set<string>();
|
|
3341
|
+
for (const changeSet of changeSets) {
|
|
3342
|
+
if (!changeSet.documentId) return null;
|
|
3343
|
+
documentIds.add(changeSet.documentId);
|
|
3344
|
+
}
|
|
3345
|
+
return [...documentIds];
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
type BuilderReviewSourceValueTextKey =
|
|
3349
|
+
| typeof BUILDER_CMS_BODY_BLOCKS_HASH_KEY
|
|
3350
|
+
| typeof BUILDER_CMS_BODY_CONTENT_KEY;
|
|
3351
|
+
|
|
3352
|
+
export function builderReviewSourceValueTextProjection(
|
|
3353
|
+
key: BuilderReviewSourceValueTextKey,
|
|
3354
|
+
) {
|
|
3355
|
+
const sourceValuesJson = schema.contentDatabaseSourceRows.sourceValuesJson;
|
|
3356
|
+
return getDialect() === "postgres"
|
|
3357
|
+
? sql<string>`COALESCE(${sourceValuesJson}::jsonb ->> ${key}, '')`
|
|
3358
|
+
: sql<string>`COALESCE(json_extract(${sourceValuesJson}, ${`$."${key}"`}), '')`;
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
async function findBuilderReviewBodyCandidateDocumentIds(args: {
|
|
3362
|
+
database: ContentDatabaseRow | ContentDatabase;
|
|
3363
|
+
source: ContentDatabaseSourceRowDb;
|
|
3364
|
+
}) {
|
|
3365
|
+
const sourceRows = schema.contentDatabaseSourceRows;
|
|
3366
|
+
const items = schema.contentDatabaseItems;
|
|
3367
|
+
const documents = schema.documents;
|
|
3368
|
+
const currentContent = builderReviewSourceValueTextProjection(
|
|
3369
|
+
BUILDER_CMS_BODY_CONTENT_KEY,
|
|
3370
|
+
);
|
|
3371
|
+
const localContentIsNonEmpty = sql<boolean>`TRIM(${documents.content}) <> ''`;
|
|
3372
|
+
const localContentDiffers = sql<boolean>`TRIM(${documents.content}) <> TRIM(${currentContent})`;
|
|
3373
|
+
const usesCurrentMediaConverter = sql<boolean>`(
|
|
3374
|
+
LOWER(${documents.content}) LIKE '% LIKE '% LIKE '%<img%'
|
|
3377
|
+
OR LOWER(${documents.content}) LIKE '%<video%'
|
|
3378
|
+
)`;
|
|
3379
|
+
const isSyntheticFixture = and(
|
|
3380
|
+
sql<boolean>`${sourceRows.sourceRowId} = 'builder-' || ${sourceRows.documentId}`,
|
|
3381
|
+
or(
|
|
3382
|
+
isNull(sourceRows.provenance),
|
|
3383
|
+
eq(sourceRows.provenance, BUILDER_CMS_FIXTURE_ROW_PROVENANCE),
|
|
3384
|
+
),
|
|
3385
|
+
);
|
|
3386
|
+
const rows = await getDb()
|
|
3387
|
+
.select({
|
|
3388
|
+
documentId: sourceRows.documentId,
|
|
3389
|
+
})
|
|
3390
|
+
.from(sourceRows)
|
|
3391
|
+
.innerJoin(
|
|
3392
|
+
items,
|
|
3393
|
+
and(
|
|
3394
|
+
eq(items.databaseId, args.database.id),
|
|
3395
|
+
eq(items.documentId, sourceRows.documentId),
|
|
3396
|
+
eq(items.ownerEmail, args.source.ownerEmail),
|
|
3397
|
+
),
|
|
3398
|
+
)
|
|
3399
|
+
.innerJoin(
|
|
3400
|
+
documents,
|
|
3401
|
+
and(
|
|
3402
|
+
eq(documents.id, sourceRows.documentId),
|
|
3403
|
+
eq(documents.ownerEmail, args.source.ownerEmail),
|
|
3404
|
+
),
|
|
3405
|
+
)
|
|
3406
|
+
.where(
|
|
3407
|
+
and(
|
|
3408
|
+
eq(sourceRows.sourceId, args.source.id),
|
|
3409
|
+
or(
|
|
3410
|
+
and(isSyntheticFixture, localContentIsNonEmpty),
|
|
3411
|
+
and(
|
|
3412
|
+
eq(items.bodyHydrationStatus, "hydrated"),
|
|
3413
|
+
or(localContentDiffers, usesCurrentMediaConverter),
|
|
3414
|
+
),
|
|
3415
|
+
),
|
|
3416
|
+
),
|
|
3417
|
+
);
|
|
3418
|
+
return rows.map((row) => row.documentId);
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
/**
|
|
3422
|
+
* Load a complete Builder review snapshot without transferring every heavy
|
|
3423
|
+
* Builder body baseline. A light pass identifies field/stored changes, while a
|
|
3424
|
+
* narrow body index compares the editable document with the readable baseline.
|
|
3425
|
+
* Only candidate documents then load lossless body data and sidecars.
|
|
3426
|
+
*/
|
|
3427
|
+
export async function getContentDatabaseSourceSnapshotForReview(
|
|
3428
|
+
database: ContentDatabaseRow | ContentDatabase,
|
|
3429
|
+
sourceId?: string | null,
|
|
3430
|
+
documentIds?: string[],
|
|
3431
|
+
): Promise<ContentDatabaseSource | null> {
|
|
3432
|
+
if (documentIds?.length) {
|
|
3433
|
+
return getContentDatabaseSourceSnapshotForWrite(
|
|
3434
|
+
database,
|
|
3435
|
+
sourceId,
|
|
3436
|
+
documentIds,
|
|
3437
|
+
);
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
const db = getDb();
|
|
3441
|
+
const [source] = sourceId
|
|
3442
|
+
? await db
|
|
3443
|
+
.select()
|
|
3444
|
+
.from(schema.contentDatabaseSources)
|
|
3445
|
+
.where(
|
|
3446
|
+
and(
|
|
3447
|
+
eq(schema.contentDatabaseSources.id, sourceId),
|
|
3448
|
+
eq(schema.contentDatabaseSources.databaseId, database.id),
|
|
3449
|
+
),
|
|
3450
|
+
)
|
|
3451
|
+
: await db
|
|
3452
|
+
.select()
|
|
3453
|
+
.from(schema.contentDatabaseSources)
|
|
3454
|
+
.where(eq(schema.contentDatabaseSources.databaseId, database.id))
|
|
3455
|
+
.orderBy(
|
|
3456
|
+
asc(schema.contentDatabaseSources.createdAt),
|
|
3457
|
+
asc(schema.contentDatabaseSources.id),
|
|
3458
|
+
);
|
|
3459
|
+
if (!source) return null;
|
|
3460
|
+
|
|
3461
|
+
const lightweight = await loadSourceSnapshot(source, database, {
|
|
3462
|
+
includeHeavyBuilderBodyValues: false,
|
|
3463
|
+
});
|
|
3464
|
+
const reviewableChanges = lightweight.changeSets.filter(
|
|
3465
|
+
(changeSet) =>
|
|
3466
|
+
changeSet.direction === "outbound" &&
|
|
3467
|
+
(changeSet.state === "pending_push" ||
|
|
3468
|
+
changeSet.state === "staged_revision" ||
|
|
3469
|
+
changeSet.state === "approved"),
|
|
3470
|
+
);
|
|
3471
|
+
// The interactive review surface is capped at 100 rows. When a complete
|
|
3472
|
+
// pending batch is already known, load heavy body/sidecar data only for those
|
|
3473
|
+
// documents instead of first discovering body-only candidates. When no
|
|
3474
|
+
// pending batch exists, the fallback compares bodies inside SQL and returns
|
|
3475
|
+
// only candidate document IDs; article bodies never cross into application
|
|
3476
|
+
// memory merely to prove that they are unchanged.
|
|
3477
|
+
const knownReviewDocumentIds = knownBuilderReviewDocumentIds(
|
|
3478
|
+
reviewableChanges,
|
|
3479
|
+
100,
|
|
3480
|
+
);
|
|
3481
|
+
if (knownReviewDocumentIds) {
|
|
3482
|
+
return loadSourceSnapshot(source, database, {
|
|
3483
|
+
includeHeavyBuilderBodyValues: true,
|
|
3484
|
+
documentIds: knownReviewDocumentIds,
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3487
|
+
if (reviewableChanges.some((changeSet) => !changeSet.documentId)) {
|
|
3488
|
+
return loadSourceSnapshot(source, database, {
|
|
3489
|
+
includeHeavyBuilderBodyValues: true,
|
|
3490
|
+
});
|
|
3491
|
+
}
|
|
3492
|
+
const candidateDocumentIds = new Set(
|
|
3493
|
+
reviewableChanges.flatMap((changeSet) =>
|
|
3494
|
+
changeSet.documentId ? [changeSet.documentId] : [],
|
|
3495
|
+
),
|
|
3496
|
+
);
|
|
3497
|
+
for (const documentId of await findBuilderReviewBodyCandidateDocumentIds({
|
|
3498
|
+
database,
|
|
3499
|
+
source,
|
|
3500
|
+
})) {
|
|
3501
|
+
candidateDocumentIds.add(documentId);
|
|
3502
|
+
}
|
|
3503
|
+
if (candidateDocumentIds.size === 0) return lightweight;
|
|
3504
|
+
return loadSourceSnapshot(source, database, {
|
|
3505
|
+
includeHeavyBuilderBodyValues: true,
|
|
3506
|
+
documentIds: [...candidateDocumentIds],
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3067
3510
|
/**
|
|
3068
3511
|
* Load every source attached to a database (oldest first → `[0]` is the
|
|
3069
3512
|
* primary). Federation joins read this; single-source callers keep using
|
|
@@ -3941,11 +4384,12 @@ export function serializeBuilderCmsSourceReadMetadataRecord(args: {
|
|
|
3941
4384
|
suspiciousEmpty?: boolean;
|
|
3942
4385
|
builderModelFields?: BuilderCmsModelFieldSummary[];
|
|
3943
4386
|
existingMetadataJson?: string | null;
|
|
4387
|
+
completedBuilderContinuationClaimId?: string;
|
|
3944
4388
|
}) {
|
|
3945
4389
|
const existingMetadata = parseObject<SourceMetadataRecord>(
|
|
3946
4390
|
args.existingMetadataJson,
|
|
3947
4391
|
);
|
|
3948
|
-
|
|
4392
|
+
const metadata: SourceMetadataRecord = {
|
|
3949
4393
|
...builderCmsSourceMetadata(args.sourceTable),
|
|
3950
4394
|
...existingMetadata,
|
|
3951
4395
|
builderModelFields:
|
|
@@ -3970,7 +4414,17 @@ export function serializeBuilderCmsSourceReadMetadataRecord(args: {
|
|
|
3970
4414
|
: args.readState === "error"
|
|
3971
4415
|
? "error"
|
|
3972
4416
|
: "idle"),
|
|
3973
|
-
}
|
|
4417
|
+
};
|
|
4418
|
+
if (
|
|
4419
|
+
args.completedBuilderContinuationClaimId &&
|
|
4420
|
+
existingMetadata?.builderContinuationClaimId ===
|
|
4421
|
+
args.completedBuilderContinuationClaimId
|
|
4422
|
+
) {
|
|
4423
|
+
delete metadata.builderContinuationClaimId;
|
|
4424
|
+
delete metadata.builderContinuationClaimOffset;
|
|
4425
|
+
delete metadata.builderContinuationClaimedAt;
|
|
4426
|
+
}
|
|
4427
|
+
return JSON.stringify(metadata);
|
|
3974
4428
|
}
|
|
3975
4429
|
|
|
3976
4430
|
export function serializeSourceCapabilitiesRecord(
|
|
@@ -4084,6 +4538,63 @@ export function mergeBuilderCmsModelFieldsPreservingReferenceModels(args: {
|
|
|
4084
4538
|
});
|
|
4085
4539
|
}
|
|
4086
4540
|
|
|
4541
|
+
function builderSourceFieldPriority(provenance: string | null) {
|
|
4542
|
+
return provenance === "Builder model field"
|
|
4543
|
+
? 0
|
|
4544
|
+
: provenance === "Builder content field"
|
|
4545
|
+
? 1
|
|
4546
|
+
: 2;
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
export function builderSourcePropertyAssignments(args: {
|
|
4550
|
+
properties: Array<{
|
|
4551
|
+
definition: { id: string; name: string; type: string };
|
|
4552
|
+
}>;
|
|
4553
|
+
existingFields?: Array<{
|
|
4554
|
+
propertyId: string | null;
|
|
4555
|
+
sourceFieldKey: string;
|
|
4556
|
+
provenance: string | null;
|
|
4557
|
+
}>;
|
|
4558
|
+
}) {
|
|
4559
|
+
const existingByPropertyId = new Map(
|
|
4560
|
+
(args.existingFields ?? [])
|
|
4561
|
+
.filter((field) => field.propertyId)
|
|
4562
|
+
.map((field) => [field.propertyId!, field]),
|
|
4563
|
+
);
|
|
4564
|
+
const canonicalPropertyBySourceKey = new Map<string, string>();
|
|
4565
|
+
for (const field of [...(args.existingFields ?? [])]
|
|
4566
|
+
.filter((candidate) => candidate.propertyId)
|
|
4567
|
+
.sort(
|
|
4568
|
+
(left, right) =>
|
|
4569
|
+
builderSourceFieldPriority(left.provenance) -
|
|
4570
|
+
builderSourceFieldPriority(right.provenance),
|
|
4571
|
+
)) {
|
|
4572
|
+
const key = field.sourceFieldKey.trim();
|
|
4573
|
+
if (!canonicalPropertyBySourceKey.has(key)) {
|
|
4574
|
+
canonicalPropertyBySourceKey.set(key, field.propertyId!);
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
|
|
4578
|
+
const claimedSourceKeys = new Set<string>();
|
|
4579
|
+
return args.properties.flatMap((property) => {
|
|
4580
|
+
const existing = existingByPropertyId.get(property.definition.id);
|
|
4581
|
+
const sourceFieldKey =
|
|
4582
|
+
existing?.sourceFieldKey ??
|
|
4583
|
+
builderCmsSourceFieldKey(
|
|
4584
|
+
property.definition.id,
|
|
4585
|
+
property.definition.name,
|
|
4586
|
+
);
|
|
4587
|
+
const normalizedKey = sourceFieldKey.trim();
|
|
4588
|
+
const canonicalPropertyId = canonicalPropertyBySourceKey.get(normalizedKey);
|
|
4589
|
+
if (canonicalPropertyId && canonicalPropertyId !== property.definition.id) {
|
|
4590
|
+
return [];
|
|
4591
|
+
}
|
|
4592
|
+
if (claimedSourceKeys.has(normalizedKey)) return [];
|
|
4593
|
+
claimedSourceKeys.add(normalizedKey);
|
|
4594
|
+
return [{ property, sourceFieldKey }];
|
|
4595
|
+
});
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4087
4598
|
export async function seedMockSourceFields(args: {
|
|
4088
4599
|
sourceId: string;
|
|
4089
4600
|
ownerEmail: string;
|
|
@@ -4096,11 +4607,6 @@ export async function seedMockSourceFields(args: {
|
|
|
4096
4607
|
}) {
|
|
4097
4608
|
const db = getDb();
|
|
4098
4609
|
const isBuilder = args.sourceType === "builder-cms";
|
|
4099
|
-
const existingBuilderFieldByPropertyId = new Map(
|
|
4100
|
-
(args.existingFields ?? [])
|
|
4101
|
-
.filter((field) => isBuilder && field.propertyId)
|
|
4102
|
-
.map((field) => [field.propertyId!, field]),
|
|
4103
|
-
);
|
|
4104
4610
|
const builderModelFieldBySourceKey = new Map(
|
|
4105
4611
|
(args.builderModelFields ?? []).map((field) => [
|
|
4106
4612
|
`data.${field.name.trim()}`,
|
|
@@ -4193,40 +4699,36 @@ export async function seedMockSourceFields(args: {
|
|
|
4193
4699
|
// "Source") and only for Builder sources, so a user's own field happening
|
|
4194
4700
|
// to be named "Source" — or any non-Builder/local-table source — is left
|
|
4195
4701
|
// untouched.
|
|
4196
|
-
...
|
|
4702
|
+
...(isBuilder
|
|
4703
|
+
? builderSourcePropertyAssignments({
|
|
4704
|
+
properties: args.properties,
|
|
4705
|
+
existingFields: args.existingFields,
|
|
4706
|
+
})
|
|
4707
|
+
: args.properties.map((property) => ({
|
|
4708
|
+
property,
|
|
4709
|
+
sourceFieldKey: `fields.${slugifySourceField(property.definition.name)}`,
|
|
4710
|
+
}))
|
|
4711
|
+
)
|
|
4197
4712
|
.filter(
|
|
4198
|
-
(property) =>
|
|
4713
|
+
({ property }) =>
|
|
4199
4714
|
!(
|
|
4200
4715
|
isBuilder &&
|
|
4201
4716
|
property.definition.name === SOURCE_PROPERTY_NAME &&
|
|
4202
4717
|
property.definition.type === "select"
|
|
4203
4718
|
),
|
|
4204
4719
|
)
|
|
4205
|
-
.map((property) => ({
|
|
4720
|
+
.map(({ property, sourceFieldKey }) => ({
|
|
4206
4721
|
id: crypto.randomUUID(),
|
|
4207
4722
|
ownerEmail: args.ownerEmail,
|
|
4208
4723
|
sourceId: args.sourceId,
|
|
4209
4724
|
propertyId: property.definition.id,
|
|
4210
4725
|
localFieldKey: property.definition.id,
|
|
4211
|
-
sourceFieldKey
|
|
4212
|
-
? (existingBuilderFieldByPropertyId.get(property.definition.id)
|
|
4213
|
-
?.sourceFieldKey ??
|
|
4214
|
-
builderCmsSourceFieldKey(
|
|
4215
|
-
property.definition.id,
|
|
4216
|
-
property.definition.name,
|
|
4217
|
-
))
|
|
4218
|
-
: `fields.${slugifySourceField(property.definition.name)}`,
|
|
4726
|
+
sourceFieldKey,
|
|
4219
4727
|
sourceFieldLabel: property.definition.name,
|
|
4220
4728
|
sourceFieldType: isBuilder
|
|
4221
4729
|
? normalizeBuilderCmsSourceFieldType(
|
|
4222
|
-
builderModelFieldBySourceKey.get(
|
|
4223
|
-
|
|
4224
|
-
?.sourceFieldKey ??
|
|
4225
|
-
builderCmsSourceFieldKey(
|
|
4226
|
-
property.definition.id,
|
|
4227
|
-
property.definition.name,
|
|
4228
|
-
),
|
|
4229
|
-
)?.type ?? property.definition.type,
|
|
4730
|
+
builderModelFieldBySourceKey.get(sourceFieldKey)?.type ??
|
|
4731
|
+
property.definition.type,
|
|
4230
4732
|
)
|
|
4231
4733
|
: property.definition.type,
|
|
4232
4734
|
mappingType: "property",
|
|
@@ -4330,15 +4832,28 @@ export async function seedMockSourceFields(args: {
|
|
|
4330
4832
|
}
|
|
4331
4833
|
|
|
4332
4834
|
const existingFieldBySourceKey = new Map(
|
|
4333
|
-
(args.existingFields ?? [])
|
|
4334
|
-
|
|
4335
|
-
field
|
|
4336
|
-
|
|
4835
|
+
[...(args.existingFields ?? [])]
|
|
4836
|
+
// `Map` keeps the last value for a duplicate key, so sort the preferred
|
|
4837
|
+
// provider-discovered field last. This fallback must agree with
|
|
4838
|
+
// `builderSourcePropertyAssignments`, independent of database row order.
|
|
4839
|
+
.sort(
|
|
4840
|
+
(left, right) =>
|
|
4841
|
+
builderSourceFieldPriority(right.provenance) -
|
|
4842
|
+
builderSourceFieldPriority(left.provenance),
|
|
4843
|
+
)
|
|
4844
|
+
.map((field) => [sourceFieldIdentityKey(field.sourceFieldKey), field]),
|
|
4845
|
+
);
|
|
4846
|
+
const existingFieldByPropertyId = new Map(
|
|
4847
|
+
(args.existingFields ?? [])
|
|
4848
|
+
.filter((field) => field.propertyId)
|
|
4849
|
+
.map((field) => [field.propertyId!, field]),
|
|
4337
4850
|
);
|
|
4338
4851
|
const mergedRows = rows.map((row) => {
|
|
4339
|
-
const existing =
|
|
4340
|
-
|
|
4341
|
-
|
|
4852
|
+
const existing =
|
|
4853
|
+
(row.propertyId
|
|
4854
|
+
? existingFieldByPropertyId.get(row.propertyId)
|
|
4855
|
+
: undefined) ??
|
|
4856
|
+
existingFieldBySourceKey.get(sourceFieldIdentityKey(row.sourceFieldKey));
|
|
4342
4857
|
if (!existing) return row;
|
|
4343
4858
|
return {
|
|
4344
4859
|
...row,
|
|
@@ -4367,77 +4882,88 @@ export async function seedMockSourceRows(args: {
|
|
|
4367
4882
|
}) {
|
|
4368
4883
|
if (args.items.length === 0) return;
|
|
4369
4884
|
const db = getDb();
|
|
4885
|
+
const rows = args.items.map((item, index) => {
|
|
4886
|
+
const builderEntry = args.builderEntriesByDocumentId?.get(item.document.id);
|
|
4887
|
+
const existingBuilderRow = args.existingBuilderRows?.get(item.document.id);
|
|
4888
|
+
const builderIdentity =
|
|
4889
|
+
args.sourceType === "builder-cms"
|
|
4890
|
+
? builderCmsSourceRowIdentity({
|
|
4891
|
+
item,
|
|
4892
|
+
sourceTable: args.sourceTable,
|
|
4893
|
+
now: args.now,
|
|
4894
|
+
existing: existingBuilderRow,
|
|
4895
|
+
entry: builderEntry,
|
|
4896
|
+
})
|
|
4897
|
+
: null;
|
|
4898
|
+
const sourceQualifiedId = builderIdentity
|
|
4899
|
+
? builderIdentity.sourceQualifiedId
|
|
4900
|
+
: `${args.sourceType}://${args.sourceTable}/${item.document.id}`;
|
|
4901
|
+
return {
|
|
4902
|
+
id: builderIdentity
|
|
4903
|
+
? stableBuilderImportId("builder-row", [
|
|
4904
|
+
args.sourceId,
|
|
4905
|
+
sourceQualifiedId,
|
|
4906
|
+
item.document.id,
|
|
4907
|
+
])
|
|
4908
|
+
: crypto.randomUUID(),
|
|
4909
|
+
ownerEmail: args.ownerEmail,
|
|
4910
|
+
sourceId: args.sourceId,
|
|
4911
|
+
databaseItemId: item.id,
|
|
4912
|
+
documentId: item.document.id,
|
|
4913
|
+
sourceRowId: builderIdentity
|
|
4914
|
+
? builderIdentity.sourceRowId
|
|
4915
|
+
: `${args.sourceType}-${item.document.id}`,
|
|
4916
|
+
sourceQualifiedId,
|
|
4917
|
+
sourceDisplayKey:
|
|
4918
|
+
builderIdentity?.sourceDisplayKey ??
|
|
4919
|
+
item.document.title?.trim() ??
|
|
4920
|
+
`${args.sourceType}-${index + 1}`,
|
|
4921
|
+
sourceValuesJson: JSON.stringify(
|
|
4922
|
+
sourceValuesForSeededSourceRow({
|
|
4923
|
+
sourceType: args.sourceType,
|
|
4924
|
+
item,
|
|
4925
|
+
sourceTable: args.sourceTable,
|
|
4926
|
+
now: args.now,
|
|
4927
|
+
builderEntry,
|
|
4928
|
+
existingSourceValuesJson: existingBuilderRow?.sourceValuesJson,
|
|
4929
|
+
existingLastSourceUpdatedAt: existingBuilderRow?.lastSourceUpdatedAt,
|
|
4930
|
+
}),
|
|
4931
|
+
),
|
|
4932
|
+
provenance:
|
|
4933
|
+
args.sourceType === "builder-cms"
|
|
4934
|
+
? builderEntry
|
|
4935
|
+
? "Builder CMS read adapter"
|
|
4936
|
+
: (existingBuilderRow?.provenance ??
|
|
4937
|
+
BUILDER_CMS_FIXTURE_ROW_PROVENANCE)
|
|
4938
|
+
: "mock source row",
|
|
4939
|
+
syncState: "linked",
|
|
4940
|
+
freshness: "fresh",
|
|
4941
|
+
lastSyncedAt: args.now,
|
|
4942
|
+
lastSourceUpdatedAt: builderIdentity?.lastSourceUpdatedAt ?? args.now,
|
|
4943
|
+
createdAt: args.now,
|
|
4944
|
+
updatedAt: args.now,
|
|
4945
|
+
};
|
|
4946
|
+
});
|
|
4370
4947
|
await db
|
|
4371
4948
|
.insert(schema.contentDatabaseSourceRows)
|
|
4372
|
-
.values(
|
|
4373
|
-
args.items.map((item, index) => {
|
|
4374
|
-
const builderEntry = args.builderEntriesByDocumentId?.get(
|
|
4375
|
-
item.document.id,
|
|
4376
|
-
);
|
|
4377
|
-
const existingBuilderRow = args.existingBuilderRows?.get(
|
|
4378
|
-
item.document.id,
|
|
4379
|
-
);
|
|
4380
|
-
const builderIdentity =
|
|
4381
|
-
args.sourceType === "builder-cms"
|
|
4382
|
-
? builderCmsSourceRowIdentity({
|
|
4383
|
-
item,
|
|
4384
|
-
sourceTable: args.sourceTable,
|
|
4385
|
-
now: args.now,
|
|
4386
|
-
existing: existingBuilderRow,
|
|
4387
|
-
entry: builderEntry,
|
|
4388
|
-
})
|
|
4389
|
-
: null;
|
|
4390
|
-
const sourceQualifiedId = builderIdentity
|
|
4391
|
-
? builderIdentity.sourceQualifiedId
|
|
4392
|
-
: `${args.sourceType}://${args.sourceTable}/${item.document.id}`;
|
|
4393
|
-
return {
|
|
4394
|
-
id: builderIdentity
|
|
4395
|
-
? stableBuilderImportId("builder-row", [
|
|
4396
|
-
args.sourceId,
|
|
4397
|
-
sourceQualifiedId,
|
|
4398
|
-
item.document.id,
|
|
4399
|
-
])
|
|
4400
|
-
: crypto.randomUUID(),
|
|
4401
|
-
ownerEmail: args.ownerEmail,
|
|
4402
|
-
sourceId: args.sourceId,
|
|
4403
|
-
databaseItemId: item.id,
|
|
4404
|
-
documentId: item.document.id,
|
|
4405
|
-
sourceRowId: builderIdentity
|
|
4406
|
-
? builderIdentity.sourceRowId
|
|
4407
|
-
: `${args.sourceType}-${item.document.id}`,
|
|
4408
|
-
sourceQualifiedId,
|
|
4409
|
-
sourceDisplayKey:
|
|
4410
|
-
builderIdentity?.sourceDisplayKey ??
|
|
4411
|
-
item.document.title?.trim() ??
|
|
4412
|
-
`${args.sourceType}-${index + 1}`,
|
|
4413
|
-
sourceValuesJson: JSON.stringify(
|
|
4414
|
-
sourceValuesForSeededSourceRow({
|
|
4415
|
-
sourceType: args.sourceType,
|
|
4416
|
-
item,
|
|
4417
|
-
sourceTable: args.sourceTable,
|
|
4418
|
-
now: args.now,
|
|
4419
|
-
builderEntry,
|
|
4420
|
-
existingSourceValuesJson: existingBuilderRow?.sourceValuesJson,
|
|
4421
|
-
existingLastSourceUpdatedAt:
|
|
4422
|
-
existingBuilderRow?.lastSourceUpdatedAt,
|
|
4423
|
-
}),
|
|
4424
|
-
),
|
|
4425
|
-
provenance:
|
|
4426
|
-
args.sourceType === "builder-cms"
|
|
4427
|
-
? args.builderEntriesByDocumentId?.has(item.document.id)
|
|
4428
|
-
? "Builder CMS read adapter"
|
|
4429
|
-
: BUILDER_CMS_FIXTURE_ROW_PROVENANCE
|
|
4430
|
-
: "mock source row",
|
|
4431
|
-
syncState: "linked",
|
|
4432
|
-
freshness: "fresh",
|
|
4433
|
-
lastSyncedAt: args.now,
|
|
4434
|
-
lastSourceUpdatedAt: builderIdentity?.lastSourceUpdatedAt ?? args.now,
|
|
4435
|
-
createdAt: args.now,
|
|
4436
|
-
updatedAt: args.now,
|
|
4437
|
-
};
|
|
4438
|
-
}),
|
|
4439
|
-
)
|
|
4949
|
+
.values(rows)
|
|
4440
4950
|
.onConflictDoNothing();
|
|
4951
|
+
|
|
4952
|
+
const fixtureItemIds = rows
|
|
4953
|
+
.filter((row) => row.provenance === BUILDER_CMS_FIXTURE_ROW_PROVENANCE)
|
|
4954
|
+
.map((row) => row.databaseItemId);
|
|
4955
|
+
for (const idChunk of chunks(fixtureItemIds, idChunkSize())) {
|
|
4956
|
+
await db
|
|
4957
|
+
.update(schema.contentDatabaseItems)
|
|
4958
|
+
.set({
|
|
4959
|
+
bodyHydrationStatus: "unavailable",
|
|
4960
|
+
bodyHydrationAttemptedAt: args.now,
|
|
4961
|
+
bodyHydrationError: null,
|
|
4962
|
+
bodyHydrationVersion: null,
|
|
4963
|
+
updatedAt: args.now,
|
|
4964
|
+
})
|
|
4965
|
+
.where(inArray(schema.contentDatabaseItems.id, idChunk));
|
|
4966
|
+
}
|
|
4441
4967
|
}
|
|
4442
4968
|
|
|
4443
4969
|
export function sourceValuesForSeededSourceRow(args: {
|
|
@@ -5339,6 +5865,7 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
5339
5865
|
source: ContentDatabaseSourceRowDb;
|
|
5340
5866
|
now: string;
|
|
5341
5867
|
runFullRefresh?: boolean;
|
|
5868
|
+
refreshClaimId?: string;
|
|
5342
5869
|
}) {
|
|
5343
5870
|
let { properties, response } = await sourceSetupPayload(args.database.id);
|
|
5344
5871
|
const db = getDb();
|
|
@@ -5395,12 +5922,29 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
5395
5922
|
limit: args.runFullRefresh ? 10_000 : undefined,
|
|
5396
5923
|
offset: continueOffset,
|
|
5397
5924
|
});
|
|
5925
|
+
if (builderRead.state === "error") {
|
|
5926
|
+
throw new Error(
|
|
5927
|
+
builderRead.message ??
|
|
5928
|
+
"Builder CMS read failed; the previous source snapshot was preserved.",
|
|
5929
|
+
);
|
|
5930
|
+
}
|
|
5398
5931
|
const incrementalRead =
|
|
5399
5932
|
builderRead.state === "live" &&
|
|
5400
5933
|
(builderRead.progress?.partial === true ||
|
|
5401
5934
|
(builderRead.progress?.startOffset ?? 0) > 0);
|
|
5402
5935
|
const builderEntries =
|
|
5403
5936
|
builderRead.state === "live" ? builderRead.entries : [];
|
|
5937
|
+
if (
|
|
5938
|
+
args.refreshClaimId &&
|
|
5939
|
+
!(await renewBuilderCmsSourceRefreshClaim({
|
|
5940
|
+
sourceId: args.source.id,
|
|
5941
|
+
claimId: args.refreshClaimId,
|
|
5942
|
+
}))
|
|
5943
|
+
) {
|
|
5944
|
+
throw new Error(
|
|
5945
|
+
"Builder source refresh claim was lost before snapshot mutation.",
|
|
5946
|
+
);
|
|
5947
|
+
}
|
|
5404
5948
|
let existingRows = await db
|
|
5405
5949
|
.select()
|
|
5406
5950
|
.from(schema.contentDatabaseSourceRows)
|
|
@@ -5432,6 +5976,7 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
5432
5976
|
sourceFetchState: "error",
|
|
5433
5977
|
syncState: "error",
|
|
5434
5978
|
suspiciousEmpty: true,
|
|
5979
|
+
refreshClaimId: args.refreshClaimId,
|
|
5435
5980
|
});
|
|
5436
5981
|
return;
|
|
5437
5982
|
}
|
|
@@ -5478,6 +6023,7 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
5478
6023
|
sourceRowId: row.sourceRowId,
|
|
5479
6024
|
sourceQualifiedId: row.sourceQualifiedId,
|
|
5480
6025
|
sourceDisplayKey: row.sourceDisplayKey,
|
|
6026
|
+
provenance: row.provenance,
|
|
5481
6027
|
lastSourceUpdatedAt: row.lastSourceUpdatedAt,
|
|
5482
6028
|
sourceValuesJson: row.sourceValuesJson,
|
|
5483
6029
|
},
|
|
@@ -5597,6 +6143,7 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
5597
6143
|
sourceFetchState: hasMore ? "fetching" : "idle",
|
|
5598
6144
|
syncState: hasMore ? "refreshing" : "idle",
|
|
5599
6145
|
activeReadSourceRowIds: hasMore ? nextActiveReadSourceRowIds : undefined,
|
|
6146
|
+
refreshClaimId: args.refreshClaimId,
|
|
5600
6147
|
});
|
|
5601
6148
|
return;
|
|
5602
6149
|
}
|
|
@@ -5708,9 +6255,10 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
5708
6255
|
message: builderRead.message,
|
|
5709
6256
|
builderModelFields,
|
|
5710
6257
|
progress: builderRead.progress,
|
|
5711
|
-
sourceFetchState:
|
|
6258
|
+
sourceFetchState: "idle",
|
|
5712
6259
|
activeReadSourceRowIds: undefined,
|
|
5713
6260
|
syncState: "idle",
|
|
6261
|
+
refreshClaimId: args.refreshClaimId,
|
|
5714
6262
|
});
|
|
5715
6263
|
}
|
|
5716
6264
|
|
|
@@ -6024,18 +6572,18 @@ export async function writeSourceFederation(args: {
|
|
|
6024
6572
|
federation: ContentDatabaseSourceFederation;
|
|
6025
6573
|
now: string;
|
|
6026
6574
|
}) {
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6575
|
+
await mutateContentDatabaseSourceMetadata({
|
|
6576
|
+
sourceId: args.sourceId,
|
|
6577
|
+
now: args.now,
|
|
6578
|
+
buildPatch: (current) => ({
|
|
6579
|
+
metadataJson: JSON.stringify({
|
|
6580
|
+
...(parseObject<SourceMetadataRecord>(current.metadataJson) ?? {}),
|
|
6581
|
+
federation: args.federation,
|
|
6582
|
+
}),
|
|
6583
|
+
}),
|
|
6584
|
+
failureMessage:
|
|
6585
|
+
"Source metadata changed repeatedly while saving federation settings.",
|
|
6586
|
+
});
|
|
6039
6587
|
}
|
|
6040
6588
|
|
|
6041
6589
|
export async function updateBuilderCmsSourceReadMetadata(args: {
|
|
@@ -6053,58 +6601,87 @@ export async function updateBuilderCmsSourceReadMetadata(args: {
|
|
|
6053
6601
|
syncState?: ContentDatabaseSourceSyncState;
|
|
6054
6602
|
builderModelFields?: BuilderCmsModelFieldSummary[];
|
|
6055
6603
|
suspiciousEmpty?: boolean;
|
|
6604
|
+
refreshClaimId?: string;
|
|
6056
6605
|
}) {
|
|
6057
6606
|
const db = getDb();
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6607
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
6608
|
+
const [currentSource] = await db
|
|
6609
|
+
.select({
|
|
6610
|
+
capabilitiesJson: schema.contentDatabaseSources.capabilitiesJson,
|
|
6611
|
+
metadataJson: schema.contentDatabaseSources.metadataJson,
|
|
6612
|
+
lastSourceUpdatedAt: schema.contentDatabaseSources.lastSourceUpdatedAt,
|
|
6613
|
+
})
|
|
6614
|
+
.from(schema.contentDatabaseSources)
|
|
6615
|
+
.where(eq(schema.contentDatabaseSources.id, args.sourceId))
|
|
6616
|
+
.limit(1);
|
|
6617
|
+
if (!currentSource) throw new Error("Builder source not found.");
|
|
6618
|
+
const currentMetadata = parseObject<SourceMetadataRecord>(
|
|
6619
|
+
currentSource.metadataJson,
|
|
6620
|
+
);
|
|
6621
|
+
if (
|
|
6622
|
+
args.refreshClaimId &&
|
|
6623
|
+
currentMetadata?.builderContinuationClaimId !== args.refreshClaimId
|
|
6624
|
+
) {
|
|
6625
|
+
throw new Error(
|
|
6626
|
+
"Builder source refresh claim was lost before completion.",
|
|
6627
|
+
);
|
|
6628
|
+
}
|
|
6629
|
+
const nextJson = mergeBuilderCmsWriteSettingsIntoJson({
|
|
6073
6630
|
sourceTable: args.sourceTable,
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6631
|
+
currentCapabilitiesJson: currentSource.capabilitiesJson,
|
|
6632
|
+
currentMetadataJson: currentSource.metadataJson,
|
|
6633
|
+
nextCapabilitiesJson: sourceCapabilitiesForType("builder-cms"),
|
|
6634
|
+
nextMetadataJson: serializeBuilderCmsSourceReadMetadataRecord({
|
|
6635
|
+
sourceTable: args.sourceTable,
|
|
6636
|
+
readState: args.readState,
|
|
6637
|
+
entryCount: args.entryCount,
|
|
6638
|
+
matchedRowCount: args.matchedRowCount,
|
|
6639
|
+
progress: args.progress,
|
|
6640
|
+
sourceFetchState: args.sourceFetchState,
|
|
6641
|
+
activeReadSourceRowIds: args.activeReadSourceRowIds,
|
|
6642
|
+
suspiciousEmpty: args.suspiciousEmpty,
|
|
6643
|
+
builderModelFields: args.builderModelFields,
|
|
6644
|
+
existingMetadataJson: currentSource.metadataJson,
|
|
6645
|
+
completedBuilderContinuationClaimId: args.refreshClaimId,
|
|
6646
|
+
}),
|
|
6647
|
+
});
|
|
6648
|
+
const updated = await db
|
|
6649
|
+
.update(schema.contentDatabaseSources)
|
|
6650
|
+
.set({
|
|
6651
|
+
syncState: args.syncState ?? "linked",
|
|
6652
|
+
freshness:
|
|
6653
|
+
args.readState === "error" ||
|
|
6654
|
+
args.progress?.partial ||
|
|
6655
|
+
args.suspiciousEmpty
|
|
6656
|
+
? "stale"
|
|
6657
|
+
: "fresh",
|
|
6658
|
+
capabilitiesJson: nextJson.capabilitiesJson,
|
|
6659
|
+
metadataJson: nextJson.metadataJson,
|
|
6660
|
+
lastRefreshedAt: args.now,
|
|
6661
|
+
lastSourceUpdatedAt: args.suspiciousEmpty
|
|
6662
|
+
? currentSource.lastSourceUpdatedAt
|
|
6663
|
+
: args.fetchedAt,
|
|
6664
|
+
lastError:
|
|
6665
|
+
args.readState === "error" || args.suspiciousEmpty
|
|
6666
|
+
? args.message
|
|
6667
|
+
: null,
|
|
6668
|
+
updatedAt: args.now,
|
|
6669
|
+
})
|
|
6670
|
+
.where(
|
|
6671
|
+
and(
|
|
6672
|
+
eq(schema.contentDatabaseSources.id, args.sourceId),
|
|
6673
|
+
currentSource.metadataJson === null
|
|
6674
|
+
? isNull(schema.contentDatabaseSources.metadataJson)
|
|
6675
|
+
: eq(
|
|
6676
|
+
schema.contentDatabaseSources.metadataJson,
|
|
6677
|
+
currentSource.metadataJson,
|
|
6678
|
+
),
|
|
6679
|
+
),
|
|
6680
|
+
)
|
|
6681
|
+
.returning({ id: schema.contentDatabaseSources.id });
|
|
6682
|
+
if (updated.length > 0) return;
|
|
6683
|
+
}
|
|
6684
|
+
throw new Error("Builder source metadata changed repeatedly during refresh.");
|
|
6108
6685
|
}
|
|
6109
6686
|
|
|
6110
6687
|
export async function updateReadOnlySourceMetadata(args: {
|
|
@@ -6117,27 +6694,24 @@ export async function updateReadOnlySourceMetadata(args: {
|
|
|
6117
6694
|
metadata?: Record<string, unknown>;
|
|
6118
6695
|
syncState?: ContentDatabaseSourceSyncState;
|
|
6119
6696
|
}) {
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
.where(eq(schema.contentDatabaseSources.id, args.sourceId))
|
|
6125
|
-
.limit(1);
|
|
6126
|
-
const existing =
|
|
6127
|
-
parseObject<Record<string, unknown>>(current?.metadataJson) ?? {};
|
|
6128
|
-
await db
|
|
6129
|
-
.update(schema.contentDatabaseSources)
|
|
6130
|
-
.set({
|
|
6697
|
+
await mutateContentDatabaseSourceMetadata({
|
|
6698
|
+
sourceId: args.sourceId,
|
|
6699
|
+
now: args.now,
|
|
6700
|
+
buildPatch: (current) => ({
|
|
6131
6701
|
syncState: args.syncState ?? (args.message ? "refreshing" : "linked"),
|
|
6132
6702
|
freshness: args.message ? "stale" : "fresh",
|
|
6133
6703
|
capabilitiesJson: sourceCapabilitiesForType(args.sourceType),
|
|
6134
|
-
metadataJson: JSON.stringify({
|
|
6704
|
+
metadataJson: JSON.stringify({
|
|
6705
|
+
...(parseObject<Record<string, unknown>>(current.metadataJson) ?? {}),
|
|
6706
|
+
...(args.metadata ?? {}),
|
|
6707
|
+
}),
|
|
6135
6708
|
lastRefreshedAt: args.now,
|
|
6136
6709
|
lastSourceUpdatedAt: args.fetchedAt,
|
|
6137
6710
|
lastError: null,
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6711
|
+
}),
|
|
6712
|
+
failureMessage:
|
|
6713
|
+
"Source metadata changed repeatedly while saving read state.",
|
|
6714
|
+
});
|
|
6141
6715
|
}
|
|
6142
6716
|
|
|
6143
6717
|
export async function getExistingSource(databaseId: string) {
|