@agent-native/core 0.101.7 → 0.101.10
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 +22 -0
- package/corpus/core/docs/content/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
- package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
- package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
- package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
- package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
- package/corpus/core/src/server/auth.ts +25 -1
- package/corpus/core/src/server/builder-browser.ts +353 -1
- package/corpus/core/src/server/core-routes-plugin.ts +601 -223
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
- package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
- package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
- package/corpus/templates/content/AGENTS.md +3 -2
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
- package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
- package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
- package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +1041 -154
- package/corpus/templates/content/actions/_database-utils.ts +89 -12
- package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
- package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
- package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
- package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
- package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2353 -576
- package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
- package/corpus/templates/content/app/global.css +18 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
- package/corpus/templates/content/app/i18n/zh-TW.ts +25 -0
- package/corpus/templates/content/app/i18n-data.ts +287 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
- package/corpus/templates/content/parity/matrix.md +27 -25
- package/corpus/templates/content/parity/matrix.ts +50 -2
- package/corpus/templates/content/server/db/schema.ts +47 -0
- package/corpus/templates/content/server/plugins/auth.ts +1 -0
- package/corpus/templates/content/server/plugins/db.ts +53 -0
- package/corpus/templates/content/shared/api.ts +70 -1
- package/corpus/templates/content/shared/builder-mdx.ts +705 -15
- package/corpus/templates/content/shared/nfm.ts +3 -3
- package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
- package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
- package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/design/app/i18n/en-US.ts +1 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
- package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
- package/corpus/templates/design/shared/review-anchor.ts +48 -0
- package/corpus/templates/plan/server/plan-mdx.ts +53 -7
- package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
- package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
- package/dist/client/review/ReviewThreadPanel.js +6 -4
- package/dist/client/review/ReviewThreadPanel.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +11 -4
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +8 -4
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +19 -6
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
- package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +19 -2
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-browser.d.ts +78 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +246 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +41 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +260 -14
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/deployment.mdx +9 -7
- package/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/package.json +2 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
getRequestOrgId,
|
|
4
|
+
getRequestUserEmail,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
import { readPreviewDocumentDraft } from "./_preview-document-draft.js";
|
|
10
|
+
|
|
11
|
+
export default defineAction({
|
|
12
|
+
description: "Read the current user's private preview draft for a document.",
|
|
13
|
+
schema: z.object({ documentId: z.string().min(1) }),
|
|
14
|
+
http: { method: "GET" },
|
|
15
|
+
agentTool: false,
|
|
16
|
+
toolCallable: false,
|
|
17
|
+
run: async ({ documentId }) => {
|
|
18
|
+
const userEmail = getRequestUserEmail();
|
|
19
|
+
const orgId = getRequestOrgId() ?? "";
|
|
20
|
+
if (!userEmail) throw new Error("Not authenticated.");
|
|
21
|
+
await assertAccess("document", documentId, "editor");
|
|
22
|
+
return {
|
|
23
|
+
draft: await readPreviewDocumentDraft(userEmail, orgId, documentId),
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq, isNull, sql } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
BUILDER_CMS_SAFE_WRITE_MODEL,
|
|
9
|
+
type BuilderCmsModelFieldSummary,
|
|
10
|
+
type DocumentPropertyValue,
|
|
11
|
+
} from "../shared/api.js";
|
|
12
|
+
import {
|
|
13
|
+
normalizePropertyVisibility,
|
|
14
|
+
serializePropertyOptions,
|
|
15
|
+
serializePropertyValue,
|
|
16
|
+
type DocumentPropertyOptionColor,
|
|
17
|
+
type DocumentPropertyOptions,
|
|
18
|
+
type DocumentPropertyType,
|
|
19
|
+
} from "../shared/properties.js";
|
|
20
|
+
import { chunks } from "./_batch-utils.js";
|
|
21
|
+
import { readBuilderCmsContentEntries } from "./_builder-cms-read-client.js";
|
|
22
|
+
import {
|
|
23
|
+
builderReferenceIdSourceValueKey,
|
|
24
|
+
resolveDatabaseForSourceMutation,
|
|
25
|
+
} from "./_database-source-utils.js";
|
|
26
|
+
import { getContentDatabaseResponse } from "./_database-utils.js";
|
|
27
|
+
import { nanoid } from "./_property-utils.js";
|
|
28
|
+
import {
|
|
29
|
+
propertyTypeForSourceField,
|
|
30
|
+
sourceFieldPropertyOptions,
|
|
31
|
+
sourceFieldPropertyValuesFromRows,
|
|
32
|
+
} from "./add-content-database-source-field-property.js";
|
|
33
|
+
|
|
34
|
+
const REFERENCE_TYPE = "reference";
|
|
35
|
+
const FILE_TYPE = "file";
|
|
36
|
+
const OMITTED_BODY_FIELDS = new Set(["blocks", "blocksString"]);
|
|
37
|
+
const OPTION_COLORS: DocumentPropertyOptionColor[] = [
|
|
38
|
+
"blue",
|
|
39
|
+
"green",
|
|
40
|
+
"purple",
|
|
41
|
+
"pink",
|
|
42
|
+
"orange",
|
|
43
|
+
"yellow",
|
|
44
|
+
"red",
|
|
45
|
+
"gray",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
type SourceRow = typeof schema.contentDatabaseSourceRows.$inferSelect;
|
|
49
|
+
type SourceField = typeof schema.contentDatabaseSourceFields.$inferSelect;
|
|
50
|
+
|
|
51
|
+
function parseMetadata(value: string | null) {
|
|
52
|
+
if (!value)
|
|
53
|
+
return {} as { builderModelFields?: BuilderCmsModelFieldSummary[] };
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(value) as unknown;
|
|
56
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
57
|
+
? (parsed as { builderModelFields?: BuilderCmsModelFieldSummary[] })
|
|
58
|
+
: {};
|
|
59
|
+
} catch {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseSourceValues(value: string | null | undefined) {
|
|
65
|
+
if (!value) return {} as Record<string, DocumentPropertyValue>;
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(value) as unknown;
|
|
68
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
69
|
+
? (parsed as Record<string, DocumentPropertyValue>)
|
|
70
|
+
: {};
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function requiredFieldKey(field: BuilderCmsModelFieldSummary) {
|
|
77
|
+
return `data.${field.name.trim()}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isRequiredEditableBuilderField(
|
|
81
|
+
field: BuilderCmsModelFieldSummary,
|
|
82
|
+
) {
|
|
83
|
+
const name = field.name.trim();
|
|
84
|
+
return (
|
|
85
|
+
field.required === true &&
|
|
86
|
+
name.length > 0 &&
|
|
87
|
+
name !== "title" &&
|
|
88
|
+
!OMITTED_BODY_FIELDS.has(name)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function propertyTypeForRequiredBuilderField(
|
|
93
|
+
field: SourceField,
|
|
94
|
+
metadata: BuilderCmsModelFieldSummary,
|
|
95
|
+
): DocumentPropertyType {
|
|
96
|
+
const normalized = field.sourceFieldType.trim().toLowerCase();
|
|
97
|
+
if (isBuilderReferenceModelField(metadata) || normalized === REFERENCE_TYPE)
|
|
98
|
+
return "select";
|
|
99
|
+
if (normalized === FILE_TYPE) return "files_media";
|
|
100
|
+
return propertyTypeForSourceField(field.sourceFieldType, metadata);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Builder's model schema is authoritative for provider-native value shape.
|
|
105
|
+
* The projected source field type can be `select` because Content renders
|
|
106
|
+
* references with a select editor, including on sources created before raw
|
|
107
|
+
* reference metadata was available.
|
|
108
|
+
*/
|
|
109
|
+
export function isBuilderReferenceModelField(
|
|
110
|
+
field: BuilderCmsModelFieldSummary,
|
|
111
|
+
) {
|
|
112
|
+
return [field.type, field.inputType]
|
|
113
|
+
.filter((value): value is string => typeof value === "string")
|
|
114
|
+
.some((value) => /\b(reference|relation)\b/i.test(value));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function rawReference(value: unknown) {
|
|
118
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
119
|
+
const record = value as Record<string, unknown>;
|
|
120
|
+
if (
|
|
121
|
+
record["@type"] !== "@builder.io/core:Reference" ||
|
|
122
|
+
typeof record.id !== "string" ||
|
|
123
|
+
!record.id.trim()
|
|
124
|
+
) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
id: record.id.trim(),
|
|
129
|
+
model:
|
|
130
|
+
typeof record.model === "string" && record.model.trim()
|
|
131
|
+
? record.model.trim()
|
|
132
|
+
: null,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type ReferenceSnapshot = {
|
|
137
|
+
bySourceRowId: Map<string, { id: string; model: string; label: string }>;
|
|
138
|
+
options: DocumentPropertyOptions;
|
|
139
|
+
model: string;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export async function readBuilderReferenceSnapshot(args: {
|
|
143
|
+
sourceTable: string;
|
|
144
|
+
field: BuilderCmsModelFieldSummary;
|
|
145
|
+
visibleValueBySourceRowId?: ReadonlyMap<string, unknown>;
|
|
146
|
+
readEntries?: typeof readBuilderCmsContentEntries;
|
|
147
|
+
}): Promise<ReferenceSnapshot> {
|
|
148
|
+
const sourceFieldKey = requiredFieldKey(args.field);
|
|
149
|
+
const read = await (args.readEntries ?? readBuilderCmsContentEntries)({
|
|
150
|
+
model: args.sourceTable,
|
|
151
|
+
fieldPaths: [sourceFieldKey],
|
|
152
|
+
rawData: true,
|
|
153
|
+
requirePrivateKey: true,
|
|
154
|
+
limit: 10_000,
|
|
155
|
+
maxPages: 100,
|
|
156
|
+
});
|
|
157
|
+
if (read.state !== "live" || read.progress.partial || read.progress.hasMore) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Builder reference choices for ${args.field.label ?? args.field.name} could not be read exhaustively. No fields were added.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const bySourceRowId = new Map<
|
|
164
|
+
string,
|
|
165
|
+
{ id: string; model: string; label: string }
|
|
166
|
+
>();
|
|
167
|
+
const choiceById = new Map<
|
|
168
|
+
string,
|
|
169
|
+
{ id: string; model: string; label: string }
|
|
170
|
+
>();
|
|
171
|
+
for (const entry of read.entries) {
|
|
172
|
+
const data = entry.rawEntry?.data;
|
|
173
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) continue;
|
|
174
|
+
const reference = rawReference(
|
|
175
|
+
(data as Record<string, unknown>)[args.field.name],
|
|
176
|
+
);
|
|
177
|
+
if (!reference) continue;
|
|
178
|
+
const model = reference.model ?? args.field.model;
|
|
179
|
+
if (!model) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Builder reference field ${args.field.label ?? args.field.name} has no target model. No fields were added.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
// The raw reference read is authoritative for the Builder-native id, but
|
|
185
|
+
// its projected display value can be absent or reduced to `model:id`.
|
|
186
|
+
// Prefer the already-synced row label so the local select option and the
|
|
187
|
+
// source baseline normalize to the same id immediately after setup.
|
|
188
|
+
const storedVisible = args.visibleValueBySourceRowId?.get(entry.id);
|
|
189
|
+
const visible =
|
|
190
|
+
typeof storedVisible === "string" && storedVisible.trim()
|
|
191
|
+
? storedVisible
|
|
192
|
+
: entry.sourceValues[sourceFieldKey];
|
|
193
|
+
const label =
|
|
194
|
+
typeof visible === "string" && visible.trim()
|
|
195
|
+
? visible.trim()
|
|
196
|
+
: `${model}:${reference.id.slice(0, 8)}`;
|
|
197
|
+
const choice = { id: reference.id, model, label };
|
|
198
|
+
bySourceRowId.set(entry.id, choice);
|
|
199
|
+
choiceById.set(reference.id, choice);
|
|
200
|
+
}
|
|
201
|
+
if (choiceById.size === 0) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Builder reference field ${args.field.label ?? args.field.name} has no usable choices. No fields were added.`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const models = new Set(
|
|
207
|
+
Array.from(choiceById.values(), (choice) => choice.model),
|
|
208
|
+
);
|
|
209
|
+
if (models.size !== 1) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Builder reference field ${args.field.label ?? args.field.name} points to more than one model. No fields were added.`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const labelCounts = new Map<string, number>();
|
|
216
|
+
for (const choice of choiceById.values()) {
|
|
217
|
+
const key = choice.label.toLowerCase();
|
|
218
|
+
labelCounts.set(key, (labelCounts.get(key) ?? 0) + 1);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
bySourceRowId,
|
|
222
|
+
model: models.values().next().value!,
|
|
223
|
+
options: {
|
|
224
|
+
options: Array.from(choiceById.values())
|
|
225
|
+
.sort((a, b) => a.label.localeCompare(b.label))
|
|
226
|
+
.map((choice, index) => ({
|
|
227
|
+
id: choice.id,
|
|
228
|
+
name:
|
|
229
|
+
(labelCounts.get(choice.label.toLowerCase()) ?? 0) > 1
|
|
230
|
+
? `${choice.label} · ${choice.id.slice(0, 8)}`
|
|
231
|
+
: choice.label,
|
|
232
|
+
color: OPTION_COLORS[index % OPTION_COLORS.length]!,
|
|
233
|
+
})),
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function referenceItemValues(args: {
|
|
239
|
+
rows: SourceRow[];
|
|
240
|
+
snapshot: ReferenceSnapshot;
|
|
241
|
+
existingDocumentIds?: ReadonlySet<string>;
|
|
242
|
+
}) {
|
|
243
|
+
return args.rows.flatMap((row) => {
|
|
244
|
+
if (args.existingDocumentIds?.has(row.documentId)) return [];
|
|
245
|
+
const choice = args.snapshot.bySourceRowId.get(row.sourceRowId);
|
|
246
|
+
return choice
|
|
247
|
+
? [
|
|
248
|
+
{
|
|
249
|
+
itemId: row.databaseItemId,
|
|
250
|
+
documentId: row.documentId,
|
|
251
|
+
value: choice.id as DocumentPropertyValue,
|
|
252
|
+
},
|
|
253
|
+
]
|
|
254
|
+
: [];
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export default defineAction({
|
|
259
|
+
description:
|
|
260
|
+
"Materialize every required Builder field as an editable Content property for the safe Builder test model in one atomic local mutation.",
|
|
261
|
+
agentTool: false,
|
|
262
|
+
schema: z.object({
|
|
263
|
+
databaseId: z.string().optional(),
|
|
264
|
+
documentId: z.string().optional(),
|
|
265
|
+
sourceId: z.string().optional(),
|
|
266
|
+
}),
|
|
267
|
+
run: async (args) => {
|
|
268
|
+
const database = await resolveDatabaseForSourceMutation(args);
|
|
269
|
+
if (!database) throw new Error("Database not found.");
|
|
270
|
+
await assertAccess("document", database.documentId, "editor");
|
|
271
|
+
const db = getDb();
|
|
272
|
+
const sourceFilters = [
|
|
273
|
+
eq(schema.contentDatabaseSources.databaseId, database.id),
|
|
274
|
+
eq(
|
|
275
|
+
schema.contentDatabaseSources.sourceTable,
|
|
276
|
+
BUILDER_CMS_SAFE_WRITE_MODEL,
|
|
277
|
+
),
|
|
278
|
+
eq(schema.contentDatabaseSources.sourceType, "builder-cms"),
|
|
279
|
+
];
|
|
280
|
+
if (args.sourceId) {
|
|
281
|
+
sourceFilters.push(eq(schema.contentDatabaseSources.id, args.sourceId));
|
|
282
|
+
}
|
|
283
|
+
const [source] = await db
|
|
284
|
+
.select()
|
|
285
|
+
.from(schema.contentDatabaseSources)
|
|
286
|
+
.where(and(...sourceFilters));
|
|
287
|
+
if (!source) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`Required-field setup is available only for ${BUILDER_CMS_SAFE_WRITE_MODEL}.`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const modelFields =
|
|
294
|
+
parseMetadata(source.metadataJson).builderModelFields ?? [];
|
|
295
|
+
const requiredModelFields = modelFields.filter(
|
|
296
|
+
isRequiredEditableBuilderField,
|
|
297
|
+
);
|
|
298
|
+
if (requiredModelFields.length === 0) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
"Builder did not provide required-field metadata. Refresh the safe source and try again.",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const [fieldRows, sourceRows] = await Promise.all([
|
|
304
|
+
db
|
|
305
|
+
.select()
|
|
306
|
+
.from(schema.contentDatabaseSourceFields)
|
|
307
|
+
.where(eq(schema.contentDatabaseSourceFields.sourceId, source.id)),
|
|
308
|
+
db
|
|
309
|
+
.select()
|
|
310
|
+
.from(schema.contentDatabaseSourceRows)
|
|
311
|
+
.where(eq(schema.contentDatabaseSourceRows.sourceId, source.id)),
|
|
312
|
+
]);
|
|
313
|
+
const fieldByKey = new Map(
|
|
314
|
+
fieldRows.map((field) => [field.sourceFieldKey, field]),
|
|
315
|
+
);
|
|
316
|
+
const referenceSnapshots = new Map<string, ReferenceSnapshot>();
|
|
317
|
+
// Reference snapshots also repair already-materialized mappings. Existing
|
|
318
|
+
// property values are canonical Builder ids and must never be overwritten;
|
|
319
|
+
// reruns only correct option labels and seed rows that still have no value.
|
|
320
|
+
for (const metadata of requiredModelFields) {
|
|
321
|
+
const field = fieldByKey.get(requiredFieldKey(metadata));
|
|
322
|
+
if (!field || !isBuilderReferenceModelField(metadata)) continue;
|
|
323
|
+
referenceSnapshots.set(
|
|
324
|
+
field.sourceFieldKey,
|
|
325
|
+
await readBuilderReferenceSnapshot({
|
|
326
|
+
sourceTable: source.sourceTable,
|
|
327
|
+
field: metadata,
|
|
328
|
+
visibleValueBySourceRowId: new Map(
|
|
329
|
+
sourceRows.map((row) => [
|
|
330
|
+
row.sourceRowId,
|
|
331
|
+
parseSourceValues(row.sourceValuesJson)[field.sourceFieldKey],
|
|
332
|
+
]),
|
|
333
|
+
),
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const enrichedModelFields = modelFields.map((field) => {
|
|
338
|
+
const reference = referenceSnapshots.get(requiredFieldKey(field));
|
|
339
|
+
return reference && !field.model
|
|
340
|
+
? { ...field, model: reference.model }
|
|
341
|
+
: field;
|
|
342
|
+
});
|
|
343
|
+
const enrichedMetadataJson = JSON.stringify({
|
|
344
|
+
...parseMetadata(source.metadataJson),
|
|
345
|
+
builderModelFields: enrichedModelFields,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await db.transaction(async (tx) => {
|
|
349
|
+
const currentFields = await tx
|
|
350
|
+
.select()
|
|
351
|
+
.from(schema.contentDatabaseSourceFields)
|
|
352
|
+
.where(eq(schema.contentDatabaseSourceFields.sourceId, source.id));
|
|
353
|
+
const currentByKey = new Map(
|
|
354
|
+
currentFields.map((field) => [field.sourceFieldKey, field]),
|
|
355
|
+
);
|
|
356
|
+
const currentSourceRows = await tx
|
|
357
|
+
.select()
|
|
358
|
+
.from(schema.contentDatabaseSourceRows)
|
|
359
|
+
.where(eq(schema.contentDatabaseSourceRows.sourceId, source.id));
|
|
360
|
+
const [maxPosition] = await tx
|
|
361
|
+
.select({ max: sql<number>`COALESCE(MAX(position), -1)` })
|
|
362
|
+
.from(schema.documentPropertyDefinitions)
|
|
363
|
+
.where(
|
|
364
|
+
and(
|
|
365
|
+
eq(
|
|
366
|
+
schema.documentPropertyDefinitions.ownerEmail,
|
|
367
|
+
database.ownerEmail,
|
|
368
|
+
),
|
|
369
|
+
eq(schema.documentPropertyDefinitions.databaseId, database.id),
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
let position = maxPosition?.max ?? -1;
|
|
373
|
+
const now = new Date().toISOString();
|
|
374
|
+
const canonicalSourceRows = currentSourceRows.flatMap((row) => {
|
|
375
|
+
const sourceValues = parseSourceValues(row.sourceValuesJson);
|
|
376
|
+
let changed = false;
|
|
377
|
+
for (const [sourceFieldKey, snapshot] of referenceSnapshots) {
|
|
378
|
+
const choice = snapshot.bySourceRowId.get(row.sourceRowId);
|
|
379
|
+
if (!choice) continue;
|
|
380
|
+
const referenceIdKey =
|
|
381
|
+
builderReferenceIdSourceValueKey(sourceFieldKey);
|
|
382
|
+
if (sourceValues[referenceIdKey] === choice.id) continue;
|
|
383
|
+
sourceValues[referenceIdKey] = choice.id;
|
|
384
|
+
changed = true;
|
|
385
|
+
}
|
|
386
|
+
return changed
|
|
387
|
+
? [
|
|
388
|
+
{
|
|
389
|
+
...row,
|
|
390
|
+
sourceValuesJson: JSON.stringify(sourceValues),
|
|
391
|
+
updatedAt: now,
|
|
392
|
+
},
|
|
393
|
+
]
|
|
394
|
+
: [];
|
|
395
|
+
});
|
|
396
|
+
for (const batch of chunks(canonicalSourceRows, 5)) {
|
|
397
|
+
await tx
|
|
398
|
+
.insert(schema.contentDatabaseSourceRows)
|
|
399
|
+
.values(batch)
|
|
400
|
+
.onConflictDoUpdate({
|
|
401
|
+
target: schema.contentDatabaseSourceRows.id,
|
|
402
|
+
set: {
|
|
403
|
+
sourceValuesJson: sql`excluded.source_values_json`,
|
|
404
|
+
updatedAt: sql`excluded.updated_at`,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
for (const metadata of requiredModelFields) {
|
|
409
|
+
const field = currentByKey.get(requiredFieldKey(metadata));
|
|
410
|
+
if (!field) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`Required Builder field ${metadata.label ?? metadata.name} is missing from the source mapping. No fields were added.`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
const reference = referenceSnapshots.get(field.sourceFieldKey);
|
|
416
|
+
if (field.propertyId) {
|
|
417
|
+
if (!reference) continue;
|
|
418
|
+
const [property] = await tx
|
|
419
|
+
.select()
|
|
420
|
+
.from(schema.documentPropertyDefinitions)
|
|
421
|
+
.where(
|
|
422
|
+
and(
|
|
423
|
+
eq(schema.documentPropertyDefinitions.id, field.propertyId),
|
|
424
|
+
eq(schema.documentPropertyDefinitions.databaseId, database.id),
|
|
425
|
+
eq(
|
|
426
|
+
schema.documentPropertyDefinitions.ownerEmail,
|
|
427
|
+
database.ownerEmail,
|
|
428
|
+
),
|
|
429
|
+
),
|
|
430
|
+
);
|
|
431
|
+
if (!property || property.type !== "select") {
|
|
432
|
+
throw new Error(
|
|
433
|
+
`Required Builder reference field ${field.sourceFieldLabel} is not bound to a valid select property. No fields were repaired.`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const repairedOptionsJson = serializePropertyOptions(
|
|
437
|
+
reference.options,
|
|
438
|
+
);
|
|
439
|
+
if (property.optionsJson !== repairedOptionsJson) {
|
|
440
|
+
await tx
|
|
441
|
+
.update(schema.documentPropertyDefinitions)
|
|
442
|
+
.set({ optionsJson: repairedOptionsJson, updatedAt: now })
|
|
443
|
+
.where(eq(schema.documentPropertyDefinitions.id, property.id));
|
|
444
|
+
}
|
|
445
|
+
const existingValues = await tx
|
|
446
|
+
.select({ documentId: schema.documentPropertyValues.documentId })
|
|
447
|
+
.from(schema.documentPropertyValues)
|
|
448
|
+
.where(
|
|
449
|
+
eq(schema.documentPropertyValues.propertyId, field.propertyId),
|
|
450
|
+
);
|
|
451
|
+
const missingItemValues = referenceItemValues({
|
|
452
|
+
rows: currentSourceRows,
|
|
453
|
+
snapshot: reference,
|
|
454
|
+
existingDocumentIds: new Set(
|
|
455
|
+
existingValues.map((value) => value.documentId),
|
|
456
|
+
),
|
|
457
|
+
});
|
|
458
|
+
for (const batch of chunks(missingItemValues, 200)) {
|
|
459
|
+
await tx.insert(schema.documentPropertyValues).values(
|
|
460
|
+
batch.map((item) => ({
|
|
461
|
+
id: nanoid(),
|
|
462
|
+
ownerEmail: database.ownerEmail,
|
|
463
|
+
documentId: item.documentId,
|
|
464
|
+
propertyId: field.propertyId!,
|
|
465
|
+
valueJson: serializePropertyValue(item.value),
|
|
466
|
+
createdAt: now,
|
|
467
|
+
updatedAt: now,
|
|
468
|
+
})),
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const type = propertyTypeForRequiredBuilderField(field, metadata);
|
|
474
|
+
const options = reference
|
|
475
|
+
? reference.options
|
|
476
|
+
: sourceFieldPropertyOptions({
|
|
477
|
+
type,
|
|
478
|
+
metadata,
|
|
479
|
+
rows: currentSourceRows,
|
|
480
|
+
sourceFieldKey: field.sourceFieldKey,
|
|
481
|
+
});
|
|
482
|
+
const propertyId = nanoid();
|
|
483
|
+
position += 1;
|
|
484
|
+
await tx.insert(schema.documentPropertyDefinitions).values({
|
|
485
|
+
id: propertyId,
|
|
486
|
+
ownerEmail: database.ownerEmail,
|
|
487
|
+
orgId: database.orgId ?? null,
|
|
488
|
+
databaseId: database.id,
|
|
489
|
+
name: field.sourceFieldLabel,
|
|
490
|
+
type,
|
|
491
|
+
visibility: normalizePropertyVisibility(undefined),
|
|
492
|
+
optionsJson: serializePropertyOptions(options),
|
|
493
|
+
position,
|
|
494
|
+
createdAt: now,
|
|
495
|
+
updatedAt: now,
|
|
496
|
+
});
|
|
497
|
+
const [mapped] = await tx
|
|
498
|
+
.update(schema.contentDatabaseSourceFields)
|
|
499
|
+
.set({
|
|
500
|
+
propertyId,
|
|
501
|
+
localFieldKey: propertyId,
|
|
502
|
+
mappingType: "property",
|
|
503
|
+
updatedAt: now,
|
|
504
|
+
})
|
|
505
|
+
.where(
|
|
506
|
+
and(
|
|
507
|
+
eq(schema.contentDatabaseSourceFields.id, field.id),
|
|
508
|
+
isNull(schema.contentDatabaseSourceFields.propertyId),
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
.returning({ id: schema.contentDatabaseSourceFields.id });
|
|
512
|
+
if (!mapped) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`Required Builder field ${field.sourceFieldLabel} changed while setup was running. No fields were added.`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
const itemValues = reference
|
|
518
|
+
? referenceItemValues({
|
|
519
|
+
rows: currentSourceRows,
|
|
520
|
+
snapshot: reference,
|
|
521
|
+
})
|
|
522
|
+
: sourceFieldPropertyValuesFromRows(
|
|
523
|
+
currentSourceRows,
|
|
524
|
+
field.sourceFieldKey,
|
|
525
|
+
type,
|
|
526
|
+
options,
|
|
527
|
+
);
|
|
528
|
+
for (const batch of chunks(itemValues, 200)) {
|
|
529
|
+
await tx.insert(schema.documentPropertyValues).values(
|
|
530
|
+
batch.map((item) => ({
|
|
531
|
+
id: nanoid(),
|
|
532
|
+
ownerEmail: database.ownerEmail,
|
|
533
|
+
documentId: item.documentId,
|
|
534
|
+
propertyId,
|
|
535
|
+
valueJson: serializePropertyValue(item.value),
|
|
536
|
+
createdAt: now,
|
|
537
|
+
updatedAt: now,
|
|
538
|
+
})),
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
await tx
|
|
543
|
+
.update(schema.contentDatabaseSources)
|
|
544
|
+
.set({ metadataJson: enrichedMetadataJson, updatedAt: now })
|
|
545
|
+
.where(eq(schema.contentDatabaseSources.id, source.id));
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
return getContentDatabaseResponse(database.id, { limit: 100, offset: 0 });
|
|
549
|
+
},
|
|
550
|
+
});
|