@agent-native/core 0.123.0 → 0.123.1
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 +16 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/templates-meta.ts +12 -0
- 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/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 +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/templates-meta.ts +12 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { defineAction, type ActionRunContext } from "@agent-native/core/action";
|
|
2
|
+
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
CrmFilterError,
|
|
9
|
+
crmFilterSchema,
|
|
10
|
+
crmSortSchema,
|
|
11
|
+
hydrateSavedViewRow,
|
|
12
|
+
normalizeStoredColumns,
|
|
13
|
+
normalizeStoredFilter,
|
|
14
|
+
} from "../server/lib/crm-query.js";
|
|
15
|
+
import { requireCrmScope, toJson } from "./_crm-action-utils.js";
|
|
16
|
+
|
|
17
|
+
/** A save that lost a race — surfaces as HTTP 409 instead of clobbering. */
|
|
18
|
+
class CrmSavedViewConflictError extends Error {
|
|
19
|
+
readonly statusCode = 409;
|
|
20
|
+
readonly code = "crm-saved-view-conflict";
|
|
21
|
+
|
|
22
|
+
constructor(id: string, expected: string, actual: string) {
|
|
23
|
+
super(
|
|
24
|
+
`CRM saved view "${id}" changed between read and write (expected ${expected}, found ${actual}). Re-read it and re-apply your edit.`,
|
|
25
|
+
);
|
|
26
|
+
this.name = "CrmSavedViewConflictError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const columnsSchema = z
|
|
31
|
+
.array(
|
|
32
|
+
z.union([
|
|
33
|
+
z.string().trim().min(1).max(128),
|
|
34
|
+
z.object({
|
|
35
|
+
attributeId: z.string().trim().min(1).max(128),
|
|
36
|
+
width: z.number().int().min(40).max(1_200).optional(),
|
|
37
|
+
hidden: z.boolean().optional(),
|
|
38
|
+
}),
|
|
39
|
+
]),
|
|
40
|
+
)
|
|
41
|
+
.max(60);
|
|
42
|
+
|
|
43
|
+
export default defineAction({
|
|
44
|
+
description:
|
|
45
|
+
"Create or update a bounded, access-scoped CRM saved view: its typed filter, ordered sort, columns, table/board presentation, and personal/shared audience. Pass expectedUpdatedAt to reject an overwrite when the view changed since it was read. A saved view stores filters and presentation settings, never provider rows.",
|
|
46
|
+
schema: z.object({
|
|
47
|
+
id: z.string().min(1).max(128).optional(),
|
|
48
|
+
name: z.string().trim().min(1).max(120),
|
|
49
|
+
description: z.string().trim().max(500).optional(),
|
|
50
|
+
/** Record kind (account/person/opportunity) — not the table/board kind. */
|
|
51
|
+
kind: z.enum(["account", "person", "opportunity"]).optional(),
|
|
52
|
+
viewKind: z.enum(["table", "board"]).optional(),
|
|
53
|
+
targetKind: z.enum(["object", "list"]).optional(),
|
|
54
|
+
targetId: z.string().trim().min(1).max(128).optional(),
|
|
55
|
+
groupByAttributeId: z
|
|
56
|
+
.string()
|
|
57
|
+
.trim()
|
|
58
|
+
.min(1)
|
|
59
|
+
.max(128)
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Board views only; must reference a status attribute."),
|
|
62
|
+
filter: crmFilterSchema.optional(),
|
|
63
|
+
/** Legacy `{query, fieldEquals}` blob; normalized into `filter`. */
|
|
64
|
+
filters: z.record(z.string().max(120), z.unknown()).optional(),
|
|
65
|
+
columns: columnsSchema.optional(),
|
|
66
|
+
sort: crmSortSchema.optional(),
|
|
67
|
+
dataProgramId: z.string().trim().min(1).max(128).optional(),
|
|
68
|
+
pinned: z.boolean().optional(),
|
|
69
|
+
audience: z.enum(["personal", "shared"]).optional(),
|
|
70
|
+
expectedUpdatedAt: z.string().min(1).max(64).optional(),
|
|
71
|
+
}),
|
|
72
|
+
audit: {
|
|
73
|
+
target: (_args, result) => {
|
|
74
|
+
const view = result as {
|
|
75
|
+
id: string;
|
|
76
|
+
ownerEmail: string;
|
|
77
|
+
orgId: string | null;
|
|
78
|
+
visibility: "private" | "org";
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
type: "crm-saved-view",
|
|
82
|
+
id: view.id,
|
|
83
|
+
ownerEmail: view.ownerEmail,
|
|
84
|
+
orgId: view.orgId,
|
|
85
|
+
visibility: view.visibility,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
summary: (args) => `Saved CRM view ${args.name}`,
|
|
89
|
+
},
|
|
90
|
+
run: async (args, ctx?: ActionRunContext) => {
|
|
91
|
+
if (args.filter && args.filters) {
|
|
92
|
+
throw new CrmFilterError(
|
|
93
|
+
"crm-filter-conflict",
|
|
94
|
+
"Pass either filter or the legacy filters blob, not both.",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const db = getDb();
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
const scope = requireCrmScope(ctx);
|
|
100
|
+
|
|
101
|
+
const filter =
|
|
102
|
+
args.filter ??
|
|
103
|
+
(args.filters === undefined
|
|
104
|
+
? undefined
|
|
105
|
+
: normalizeStoredFilter(args.filters));
|
|
106
|
+
const columns =
|
|
107
|
+
args.columns === undefined
|
|
108
|
+
? undefined
|
|
109
|
+
: normalizeStoredColumns(args.columns);
|
|
110
|
+
|
|
111
|
+
const existing = args.id ? await readView(args.id) : undefined;
|
|
112
|
+
if (args.id && !existing) throw new Error("CRM saved view was not found.");
|
|
113
|
+
|
|
114
|
+
const viewKind = args.viewKind ?? existing?.viewKind ?? "table";
|
|
115
|
+
// Switching a board back to a table drops its grouping instead of
|
|
116
|
+
// inheriting it — otherwise the switch would fail the board-only check.
|
|
117
|
+
const groupByAttributeId =
|
|
118
|
+
args.groupByAttributeId !== undefined
|
|
119
|
+
? args.groupByAttributeId
|
|
120
|
+
: viewKind === "board"
|
|
121
|
+
? (existing?.groupByAttributeId ?? null)
|
|
122
|
+
: null;
|
|
123
|
+
if (viewKind === "board") {
|
|
124
|
+
if (!groupByAttributeId) {
|
|
125
|
+
throw new CrmFilterError(
|
|
126
|
+
"crm-saved-view-group-by",
|
|
127
|
+
"A board view needs groupByAttributeId naming a status attribute.",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
await assertStatusAttribute(groupByAttributeId);
|
|
131
|
+
} else if (groupByAttributeId) {
|
|
132
|
+
throw new CrmFilterError(
|
|
133
|
+
"crm-saved-view-group-by",
|
|
134
|
+
"groupByAttributeId only applies to a board view.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const visibility =
|
|
139
|
+
args.audience === undefined
|
|
140
|
+
? undefined
|
|
141
|
+
: audienceVisibility(args.audience, scope.orgId);
|
|
142
|
+
|
|
143
|
+
if (args.id && existing) {
|
|
144
|
+
await assertAccess("crm-saved-view", args.id, "editor");
|
|
145
|
+
if (
|
|
146
|
+
args.expectedUpdatedAt !== undefined &&
|
|
147
|
+
args.expectedUpdatedAt !== existing.updatedAt
|
|
148
|
+
) {
|
|
149
|
+
throw new CrmSavedViewConflictError(
|
|
150
|
+
args.id,
|
|
151
|
+
args.expectedUpdatedAt,
|
|
152
|
+
existing.updatedAt,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
await db
|
|
156
|
+
.update(schema.crmSavedViews)
|
|
157
|
+
.set({
|
|
158
|
+
name: args.name,
|
|
159
|
+
...(args.description !== undefined
|
|
160
|
+
? { description: args.description }
|
|
161
|
+
: {}),
|
|
162
|
+
...(args.kind !== undefined ? { kind: args.kind } : {}),
|
|
163
|
+
...(args.viewKind !== undefined ? { viewKind } : {}),
|
|
164
|
+
...(args.targetKind !== undefined
|
|
165
|
+
? { targetKind: args.targetKind }
|
|
166
|
+
: {}),
|
|
167
|
+
...(args.targetId !== undefined ? { targetId: args.targetId } : {}),
|
|
168
|
+
groupByAttributeId,
|
|
169
|
+
...(filter !== undefined
|
|
170
|
+
? { filtersJson: toJson(filter, 8_000) }
|
|
171
|
+
: {}),
|
|
172
|
+
...(columns !== undefined
|
|
173
|
+
? { columnsJson: toJson(columns, 8_000) }
|
|
174
|
+
: {}),
|
|
175
|
+
...(args.sort !== undefined
|
|
176
|
+
? { sortJson: toJson(args.sort, 2_000) }
|
|
177
|
+
: {}),
|
|
178
|
+
...(args.dataProgramId !== undefined
|
|
179
|
+
? { dataProgramId: args.dataProgramId }
|
|
180
|
+
: {}),
|
|
181
|
+
...(args.pinned !== undefined ? { pinned: args.pinned } : {}),
|
|
182
|
+
...(visibility !== undefined ? { visibility } : {}),
|
|
183
|
+
updatedAt: now,
|
|
184
|
+
})
|
|
185
|
+
.where(
|
|
186
|
+
and(
|
|
187
|
+
eq(schema.crmSavedViews.id, args.id),
|
|
188
|
+
...(args.expectedUpdatedAt !== undefined
|
|
189
|
+
? [eq(schema.crmSavedViews.updatedAt, args.expectedUpdatedAt)]
|
|
190
|
+
: []),
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
const saved = await readView(args.id);
|
|
194
|
+
if (!saved) throw new Error("CRM saved view was not found.");
|
|
195
|
+
if (args.expectedUpdatedAt !== undefined && saved.updatedAt !== now) {
|
|
196
|
+
// The guarded UPDATE matched nothing: another writer landed between the
|
|
197
|
+
// read above and the write. Report it rather than returning the row we
|
|
198
|
+
// did not write as if the save succeeded.
|
|
199
|
+
throw new CrmSavedViewConflictError(
|
|
200
|
+
args.id,
|
|
201
|
+
args.expectedUpdatedAt,
|
|
202
|
+
saved.updatedAt,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return saved;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const id = crypto.randomUUID();
|
|
209
|
+
await db.insert(schema.crmSavedViews).values({
|
|
210
|
+
id,
|
|
211
|
+
name: args.name,
|
|
212
|
+
description: args.description ?? "",
|
|
213
|
+
kind: args.kind ?? null,
|
|
214
|
+
viewKind,
|
|
215
|
+
targetKind: args.targetKind ?? "object",
|
|
216
|
+
targetId: args.targetId ?? null,
|
|
217
|
+
groupByAttributeId,
|
|
218
|
+
filtersJson: toJson(filter ?? { op: "and", conditions: [] }, 8_000),
|
|
219
|
+
columnsJson: toJson(columns ?? [], 8_000),
|
|
220
|
+
sortJson: toJson(args.sort ?? [], 2_000),
|
|
221
|
+
dataProgramId: args.dataProgramId ?? null,
|
|
222
|
+
pinned: args.pinned ?? false,
|
|
223
|
+
...scope,
|
|
224
|
+
...(visibility !== undefined ? { visibility } : {}),
|
|
225
|
+
createdAt: now,
|
|
226
|
+
updatedAt: now,
|
|
227
|
+
});
|
|
228
|
+
const saved = await readView(id);
|
|
229
|
+
if (!saved)
|
|
230
|
+
throw new Error("CRM saved view could not be verified after saving.");
|
|
231
|
+
return saved;
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
function audienceVisibility(
|
|
236
|
+
audience: "personal" | "shared",
|
|
237
|
+
orgId: string | null,
|
|
238
|
+
): "private" | "org" {
|
|
239
|
+
if (audience === "personal") return "private";
|
|
240
|
+
if (!orgId) {
|
|
241
|
+
throw new CrmFilterError(
|
|
242
|
+
"crm-saved-view-audience",
|
|
243
|
+
"A shared view needs an active organization; this session has none.",
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return "org";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function assertStatusAttribute(attributeId: string): Promise<void> {
|
|
250
|
+
const [row] = await getDb()
|
|
251
|
+
.select({
|
|
252
|
+
attributeType: schema.crmFieldPolicies.attributeType,
|
|
253
|
+
archived: schema.crmFieldPolicies.archived,
|
|
254
|
+
})
|
|
255
|
+
.from(schema.crmFieldPolicies)
|
|
256
|
+
.where(
|
|
257
|
+
and(
|
|
258
|
+
eq(schema.crmFieldPolicies.id, attributeId),
|
|
259
|
+
accessFilter(schema.crmFieldPolicies, schema.crmFieldPolicyShares),
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
.limit(1);
|
|
263
|
+
if (!row) {
|
|
264
|
+
throw new CrmFilterError(
|
|
265
|
+
"crm-saved-view-group-by",
|
|
266
|
+
`Board grouping references unknown attribute "${attributeId}".`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (row.archived) {
|
|
270
|
+
throw new CrmFilterError(
|
|
271
|
+
"crm-saved-view-group-by",
|
|
272
|
+
`Board grouping references archived attribute "${attributeId}".`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (row.attributeType !== "status") {
|
|
276
|
+
throw new CrmFilterError(
|
|
277
|
+
"crm-saved-view-group-by",
|
|
278
|
+
`Board grouping needs a status attribute; "${attributeId}" is ${row.attributeType}.`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function readView(id: string) {
|
|
284
|
+
const [row] = await getDb()
|
|
285
|
+
.select()
|
|
286
|
+
.from(schema.crmSavedViews)
|
|
287
|
+
.where(
|
|
288
|
+
and(
|
|
289
|
+
eq(schema.crmSavedViews.id, id),
|
|
290
|
+
accessFilter(schema.crmSavedViews, schema.crmSavedViewShares),
|
|
291
|
+
),
|
|
292
|
+
)
|
|
293
|
+
.limit(1);
|
|
294
|
+
if (!row) return undefined;
|
|
295
|
+
return { ...row, view: hydrateSavedViewRow(row) };
|
|
296
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { defineAction, type ActionRunContext } from "@agent-native/core/action";
|
|
2
|
+
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createConnectedCrmAdapter,
|
|
8
|
+
isConnectedCrmProvider,
|
|
9
|
+
} from "../server/crm/adapter.js";
|
|
10
|
+
import {
|
|
11
|
+
MAX_SYNC_PAGE_SIZE,
|
|
12
|
+
MAX_SYNC_PAGES,
|
|
13
|
+
syncCrmMirror,
|
|
14
|
+
} from "../server/crm/crm-mirror.js";
|
|
15
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
16
|
+
import { requireCrmScope } from "./_crm-action-utils.js";
|
|
17
|
+
|
|
18
|
+
const idList = z
|
|
19
|
+
.array(z.string().trim().min(1).max(160))
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(50)
|
|
22
|
+
.optional();
|
|
23
|
+
const syncScopeSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
pipelineIds: idList,
|
|
26
|
+
ownerIds: idList,
|
|
27
|
+
recordIds: idList,
|
|
28
|
+
associatedRecordIds: idList,
|
|
29
|
+
updatedAfter: z.string().datetime({ offset: true }).optional(),
|
|
30
|
+
includeDeleted: z.boolean().default(false),
|
|
31
|
+
})
|
|
32
|
+
.superRefine((scope, issue) => {
|
|
33
|
+
if (
|
|
34
|
+
!scope.pipelineIds &&
|
|
35
|
+
!scope.ownerIds &&
|
|
36
|
+
!scope.recordIds &&
|
|
37
|
+
!scope.associatedRecordIds &&
|
|
38
|
+
!scope.updatedAfter
|
|
39
|
+
) {
|
|
40
|
+
issue.addIssue({
|
|
41
|
+
code: "custom",
|
|
42
|
+
message:
|
|
43
|
+
"A sync requires a bounded cohort selector: pipelineIds, ownerIds, recordIds, associatedRecordIds, or updatedAfter.",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export default defineAction({
|
|
49
|
+
description:
|
|
50
|
+
"Thinly mirror one bounded HubSpot or Salesforce CRM object cohort. It discovers field policy first, mirrors only explicit safe fields, and returns a continuation cursor instead of claiming an exhaustive sync.",
|
|
51
|
+
schema: z.object({
|
|
52
|
+
connectionId: z.string().trim().min(1).max(128),
|
|
53
|
+
objectType: z.string().trim().min(1).max(120),
|
|
54
|
+
scope: syncScopeSchema,
|
|
55
|
+
fieldAllowList: z
|
|
56
|
+
.array(z.string().trim().min(1).max(120))
|
|
57
|
+
.min(1)
|
|
58
|
+
.max(80)
|
|
59
|
+
.optional(),
|
|
60
|
+
allowCustomObject: z.boolean().default(false),
|
|
61
|
+
cursor: z.string().trim().min(1).max(2_000).optional(),
|
|
62
|
+
pageSize: z.coerce
|
|
63
|
+
.number()
|
|
64
|
+
.int()
|
|
65
|
+
.min(1)
|
|
66
|
+
.max(MAX_SYNC_PAGE_SIZE)
|
|
67
|
+
.default(MAX_SYNC_PAGE_SIZE),
|
|
68
|
+
maxPages: z.coerce
|
|
69
|
+
.number()
|
|
70
|
+
.int()
|
|
71
|
+
.min(1)
|
|
72
|
+
.max(MAX_SYNC_PAGES)
|
|
73
|
+
.default(MAX_SYNC_PAGES),
|
|
74
|
+
}),
|
|
75
|
+
audit: {
|
|
76
|
+
target: (args, result) => {
|
|
77
|
+
const response = result as {
|
|
78
|
+
ownerEmail: string;
|
|
79
|
+
orgId: string | null;
|
|
80
|
+
visibility: "private" | "org";
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
type: "crm-connection",
|
|
84
|
+
id: args.connectionId,
|
|
85
|
+
ownerEmail: response.ownerEmail,
|
|
86
|
+
orgId: response.orgId,
|
|
87
|
+
visibility: response.visibility,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
summary: (args) => `Synced bounded CRM ${args.objectType} cohort`,
|
|
91
|
+
recordInputs: false,
|
|
92
|
+
},
|
|
93
|
+
run: async (args, ctx?: ActionRunContext) => {
|
|
94
|
+
await assertAccess("crm-connection", args.connectionId, "editor");
|
|
95
|
+
const db = getDb();
|
|
96
|
+
const [connection] = await db
|
|
97
|
+
.select()
|
|
98
|
+
.from(schema.crmConnections)
|
|
99
|
+
.where(
|
|
100
|
+
and(
|
|
101
|
+
eq(schema.crmConnections.id, args.connectionId),
|
|
102
|
+
accessFilter(
|
|
103
|
+
schema.crmConnections,
|
|
104
|
+
schema.crmConnectionShares,
|
|
105
|
+
undefined,
|
|
106
|
+
"editor",
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
.limit(1);
|
|
111
|
+
if (!connection || connection.status === "disconnected")
|
|
112
|
+
throw new Error("CRM connection is unavailable.");
|
|
113
|
+
if (
|
|
114
|
+
!isConnectedCrmProvider(connection.provider) ||
|
|
115
|
+
!connection.workspaceConnectionId
|
|
116
|
+
)
|
|
117
|
+
throw new Error(
|
|
118
|
+
"CRM sync requires a connected HubSpot or Salesforce workspace connection.",
|
|
119
|
+
);
|
|
120
|
+
const ownership = requireCrmScope(ctx);
|
|
121
|
+
const adapter = await createConnectedCrmAdapter({
|
|
122
|
+
provider: connection.provider,
|
|
123
|
+
connectionId: connection.workspaceConnectionId,
|
|
124
|
+
userEmail: ownership.ownerEmail,
|
|
125
|
+
orgId: ownership.orgId,
|
|
126
|
+
});
|
|
127
|
+
const result = await syncCrmMirror({
|
|
128
|
+
connectionId: connection.id,
|
|
129
|
+
objectType: args.objectType,
|
|
130
|
+
scope: { objectType: args.objectType, ...args.scope },
|
|
131
|
+
fieldAllowList: args.fieldAllowList,
|
|
132
|
+
allowCustomObject: args.allowCustomObject,
|
|
133
|
+
cursor: args.cursor,
|
|
134
|
+
pageSize: args.pageSize,
|
|
135
|
+
maxPages: args.maxPages,
|
|
136
|
+
ownership,
|
|
137
|
+
adapter,
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
...result,
|
|
141
|
+
ownerEmail: ownership.ownerEmail,
|
|
142
|
+
orgId: ownership.orgId,
|
|
143
|
+
visibility: ownership.visibility,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core/action";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
6
|
+
import {
|
|
7
|
+
CRM_ATTRIBUTE_FILL_MODES,
|
|
8
|
+
CRM_ATTRIBUTE_TYPES,
|
|
9
|
+
} from "../shared/crm-contract.js";
|
|
10
|
+
import { toJson } from "./_crm-action-utils.js";
|
|
11
|
+
import {
|
|
12
|
+
CrmAttributeValueError,
|
|
13
|
+
loadAttributeOptions,
|
|
14
|
+
requireEditableAttribute,
|
|
15
|
+
toAttributeDefinition,
|
|
16
|
+
} from "./_crm-attribute-utils.js";
|
|
17
|
+
|
|
18
|
+
const MAX_CONFIG_CHARS = 4000;
|
|
19
|
+
|
|
20
|
+
export default defineAction({
|
|
21
|
+
description:
|
|
22
|
+
"Update one CRM attribute's presentation and behaviour: title, description, position, required, historyTracked, config, archived, and the manual fill mode. The api slug and the attribute type are immutable — every stored value row is keyed by the slug and typed by the type, so changing either would orphan history instead of migrating it. Setting fillMode never runs a fill; fills are triggered manually.",
|
|
23
|
+
schema: z.object({
|
|
24
|
+
attributeId: z.string().trim().min(1).max(128),
|
|
25
|
+
title: z.string().trim().min(1).max(200).optional(),
|
|
26
|
+
description: z.string().trim().max(2000).nullish(),
|
|
27
|
+
position: z.number().int().min(0).max(10_000).optional(),
|
|
28
|
+
required: z.boolean().optional(),
|
|
29
|
+
historyTracked: z.boolean().optional(),
|
|
30
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
31
|
+
fillMode: z.enum(CRM_ATTRIBUTE_FILL_MODES).nullish(),
|
|
32
|
+
fillConfig: z.record(z.string(), z.unknown()).optional(),
|
|
33
|
+
archived: z.boolean().optional(),
|
|
34
|
+
apiSlug: z
|
|
35
|
+
.string()
|
|
36
|
+
.trim()
|
|
37
|
+
.min(1)
|
|
38
|
+
.max(128)
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Immutable. Supplying a different slug is rejected."),
|
|
41
|
+
type: z
|
|
42
|
+
.enum(CRM_ATTRIBUTE_TYPES)
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Immutable. Supplying a different type is rejected."),
|
|
45
|
+
}),
|
|
46
|
+
audit: {
|
|
47
|
+
target: (args) => ({ type: "crm-field-policy", id: args.attributeId }),
|
|
48
|
+
summary: (args) => `Updated CRM attribute ${args.attributeId}`,
|
|
49
|
+
},
|
|
50
|
+
run: async (args) => {
|
|
51
|
+
const current = await requireEditableAttribute(args.attributeId);
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
args.apiSlug &&
|
|
55
|
+
args.apiSlug !== (current.apiSlug ?? current.fieldName)
|
|
56
|
+
) {
|
|
57
|
+
throw new CrmAttributeValueError(
|
|
58
|
+
"crm-attribute-slug-immutable",
|
|
59
|
+
`The api slug of attribute ${args.attributeId} is "${current.apiSlug ?? current.fieldName}" and cannot change: every stored value row is keyed by it, so a rename would orphan the attribute's history. Create a new attribute instead.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (args.type && args.type !== current.attributeType) {
|
|
63
|
+
throw new CrmAttributeValueError(
|
|
64
|
+
"crm-attribute-type-immutable",
|
|
65
|
+
`The type of attribute ${args.attributeId} is "${current.attributeType}" and cannot change: stored values live in the column that type chose, and reinterpreting them is a migration, not an edit. Create a new attribute and move the values.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const patch = {
|
|
70
|
+
...(args.title === undefined ? {} : { label: args.title }),
|
|
71
|
+
...(args.description === undefined
|
|
72
|
+
? {}
|
|
73
|
+
: { description: args.description }),
|
|
74
|
+
...(args.position === undefined ? {} : { position: args.position }),
|
|
75
|
+
...(args.required === undefined ? {} : { required: args.required }),
|
|
76
|
+
...(args.historyTracked === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: { historyTracked: args.historyTracked }),
|
|
79
|
+
...(args.config === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: { configJson: toJson(args.config, MAX_CONFIG_CHARS) }),
|
|
82
|
+
...(args.fillMode === undefined ? {} : { fillMode: args.fillMode }),
|
|
83
|
+
...(args.fillConfig === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: { fillConfigJson: toJson(args.fillConfig, MAX_CONFIG_CHARS) }),
|
|
86
|
+
...(args.archived === undefined ? {} : { archived: args.archived }),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const db = getDb();
|
|
90
|
+
if (Object.keys(patch).length > 0) {
|
|
91
|
+
await db
|
|
92
|
+
.update(schema.crmFieldPolicies)
|
|
93
|
+
.set({ ...patch, updatedAt: new Date().toISOString() })
|
|
94
|
+
.where(eq(schema.crmFieldPolicies.id, args.attributeId));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [row] = await db
|
|
98
|
+
.select()
|
|
99
|
+
.from(schema.crmFieldPolicies)
|
|
100
|
+
.where(eq(schema.crmFieldPolicies.id, args.attributeId))
|
|
101
|
+
.limit(1);
|
|
102
|
+
if (!row) {
|
|
103
|
+
throw new Error("CRM attribute could not be verified after saving.");
|
|
104
|
+
}
|
|
105
|
+
const options = await loadAttributeOptions({
|
|
106
|
+
attributeIds: [row.id],
|
|
107
|
+
includeArchived: true,
|
|
108
|
+
});
|
|
109
|
+
return toAttributeDefinition(row, options.get(row.id) ?? []);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { defineAction, type ActionRunContext } from "@agent-native/core/action";
|
|
2
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import { requireCrmScope } from "./_crm-action-utils.js";
|
|
8
|
+
import {
|
|
9
|
+
crmActorFrom,
|
|
10
|
+
indexAttributes,
|
|
11
|
+
loadCrmListAttributes,
|
|
12
|
+
requireCrmList,
|
|
13
|
+
requireCrmListEntry,
|
|
14
|
+
writeCrmListEntryValues,
|
|
15
|
+
} from "./_crm-list-utils.js";
|
|
16
|
+
import { listEntryValuesSchema } from "./add-crm-record-to-list.js";
|
|
17
|
+
|
|
18
|
+
export default defineAction({
|
|
19
|
+
description:
|
|
20
|
+
"Set values on a CRM list entry's own attributes and/or reorder it. Moving an entry to another stage is exactly this call: the previous value's row is closed and a new one opened, so time-in-stage is derivable from the entry's own history. Entry attributes belong to the list, not to the record — this never writes to the record or to a provider.",
|
|
21
|
+
schema: z.object({
|
|
22
|
+
entryId: z.string().trim().min(1).max(128),
|
|
23
|
+
values: listEntryValuesSchema.optional(),
|
|
24
|
+
position: z.number().int().min(0).max(100_000).optional(),
|
|
25
|
+
}),
|
|
26
|
+
audit: {
|
|
27
|
+
target: (_args, result) => {
|
|
28
|
+
const entry = result as {
|
|
29
|
+
entryId: string;
|
|
30
|
+
ownerEmail: string;
|
|
31
|
+
orgId: string | null;
|
|
32
|
+
visibility: "private" | "org" | "public";
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
type: "crm-list-entry",
|
|
36
|
+
id: entry.entryId,
|
|
37
|
+
ownerEmail: entry.ownerEmail,
|
|
38
|
+
orgId: entry.orgId,
|
|
39
|
+
visibility: entry.visibility,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
summary: (args) => `Updated CRM list entry ${args.entryId}`,
|
|
43
|
+
recordInputs: false,
|
|
44
|
+
},
|
|
45
|
+
run: async (args, ctx?: ActionRunContext) => {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
const entry = await requireCrmListEntry(db, args.entryId, "editor");
|
|
48
|
+
const list = await requireCrmList(db, entry.listId, "editor");
|
|
49
|
+
const attributes = indexAttributes(
|
|
50
|
+
await loadCrmListAttributes(db, list.id),
|
|
51
|
+
);
|
|
52
|
+
const actor = crmActorFrom(ctx);
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
// Value rows inherit the ENTRY's ownership, not the caller's, so an editor
|
|
55
|
+
// writing to a shared entry cannot fork its visibility.
|
|
56
|
+
const ownership = {
|
|
57
|
+
ownerEmail: entry.ownerEmail,
|
|
58
|
+
orgId: entry.orgId,
|
|
59
|
+
visibility: entry.visibility,
|
|
60
|
+
};
|
|
61
|
+
// Reject an unauthenticated caller the same way a fresh write would.
|
|
62
|
+
requireCrmScope(ctx);
|
|
63
|
+
|
|
64
|
+
const writes = await db.transaction(async (tx) => {
|
|
65
|
+
if (args.position !== undefined) {
|
|
66
|
+
await tx
|
|
67
|
+
.update(schema.crmListEntries)
|
|
68
|
+
.set({ position: args.position, updatedAt: now })
|
|
69
|
+
.where(
|
|
70
|
+
and(
|
|
71
|
+
eq(schema.crmListEntries.id, entry.id),
|
|
72
|
+
accessFilter(
|
|
73
|
+
schema.crmListEntries,
|
|
74
|
+
schema.crmListEntryShares,
|
|
75
|
+
undefined,
|
|
76
|
+
"editor",
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return writeCrmListEntryValues({
|
|
82
|
+
db: tx,
|
|
83
|
+
recordId: entry.recordId,
|
|
84
|
+
entryId: entry.id,
|
|
85
|
+
attributes,
|
|
86
|
+
values: args.values ?? {},
|
|
87
|
+
actor,
|
|
88
|
+
ownership,
|
|
89
|
+
now,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
entryId: entry.id,
|
|
95
|
+
listId: list.id,
|
|
96
|
+
recordId: entry.recordId,
|
|
97
|
+
position: args.position ?? entry.position,
|
|
98
|
+
values: writes,
|
|
99
|
+
...ownership,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
});
|