@agent-native/core 0.70.0 → 0.70.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/README.md +15 -15
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +67 -0
- package/corpus/core/docs/design/durable-agent-runs.md +217 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +89 -0
- package/corpus/core/src/agent/model-config.ts +0 -2
- package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
- package/corpus/core/src/cli/skills.ts +2 -2
- package/corpus/core/src/cli/sync-builder-starter-manifest.ts +275 -0
- package/corpus/core/src/client/AgentPanel.tsx +5 -3
- package/corpus/core/src/client/AssistantChat.tsx +8 -0
- package/corpus/core/src/client/client-surface.ts +41 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
- package/corpus/core/src/client/feedback-context.ts +4 -0
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/sse-event-processor.ts +7 -1
- package/corpus/core/src/mcp/build-server.ts +57 -1
- package/corpus/core/src/mcp/embed-app.ts +86 -2
- package/corpus/core/src/mcp/oauth-route.ts +109 -13
- package/corpus/core/src/server/core-routes-plugin.ts +78 -0
- package/corpus/core/src/server/onboarding-html.ts +35 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
- package/corpus/templates/analytics/AGENTS.md +8 -1
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +97 -2
- package/corpus/templates/analytics/actions/update-dashboard.ts +36 -7
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
- package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -4
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
- package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
- package/corpus/templates/assets/actions/generate-image.ts +24 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
- package/corpus/templates/assets/app/global.css +17 -1
- package/corpus/templates/assets/app/routes/_index.tsx +94 -14
- package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
- package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
- package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
- package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
- package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
- package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
- package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
- package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
- package/corpus/templates/clips/desktop/src/styles.css +70 -2
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +73 -0
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +34 -5
- package/corpus/templates/content/AGENTS.md +20 -3
- package/corpus/templates/content/actions/_database-utils.ts +15 -0
- package/corpus/templates/content/actions/_property-utils.ts +371 -4
- package/corpus/templates/content/actions/configure-document-property.ts +35 -1
- package/corpus/templates/content/actions/create-content-database.ts +5 -1
- package/corpus/templates/content/actions/delete-document-property.ts +52 -2
- package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
- package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
- package/corpus/templates/content/actions/set-document-property.ts +39 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
- package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
- package/corpus/templates/content/server/db/schema.ts +30 -0
- package/corpus/templates/content/server/plugins/db.ts +87 -0
- package/corpus/templates/content/shared/api.ts +7 -0
- package/corpus/templates/content/shared/properties.ts +109 -0
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
- package/corpus/templates/forms/actions/export-responses.ts +6 -0
- package/corpus/templates/forms/actions/list-responses.ts +2 -0
- package/corpus/templates/forms/actions/response-insights.ts +2 -0
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
- package/corpus/templates/forms/server/db/schema.ts +7 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
- package/corpus/templates/forms/server/lib/integrations.ts +76 -2
- package/corpus/templates/forms/server/plugins/db.ts +20 -0
- package/corpus/templates/forms/shared/types.ts +12 -0
- package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
- package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
- package/dist/action.js +87 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/engine/builder-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/model-config.d.ts +2 -2
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +0 -2
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +13 -0
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +44 -0
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/cli/skills.js +2 -2
- package/dist/cli/skills.js.map +1 -1
- package/dist/cli/sync-builder-starter-manifest.d.ts +32 -0
- package/dist/cli/sync-builder-starter-manifest.d.ts.map +1 -0
- package/dist/cli/sync-builder-starter-manifest.js +179 -0
- package/dist/cli/sync-builder-starter-manifest.js.map +1 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/client-surface.d.ts +17 -0
- package/dist/client/client-surface.d.ts.map +1 -0
- package/dist/client/client-surface.js +24 -0
- package/dist/client/client-surface.js.map +1 -0
- package/dist/client/composer/TiptapComposer.d.ts +26 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +27 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/feedback-context.d.ts +3 -0
- package/dist/client/feedback-context.d.ts.map +1 -1
- package/dist/client/feedback-context.js +2 -0
- package/dist/client/feedback-context.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +21 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +43 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +86 -2
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/oauth-route.d.ts.map +1 -1
- package/dist/mcp/oauth-route.js +95 -12
- package/dist/mcp/oauth-route.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +63 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +35 -0
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/docs/design/durable-agent-runs.md +217 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/templates/analytics/app/pages/About.tsx +0 -108
- package/corpus/templates/analytics/app/routes/about.tsx +0 -9
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
2
|
+
import { and, asc, eq, inArray, sql, type InferSelectModel } from "drizzle-orm";
|
|
2
3
|
import { getDb, schema } from "../server/db/index.js";
|
|
3
4
|
import {
|
|
5
|
+
DEFAULT_BLOCKS_FIELD_NAME,
|
|
4
6
|
defaultPropertyOptions,
|
|
5
7
|
evaluatePropertyFormula,
|
|
6
8
|
formulaValueText,
|
|
9
|
+
isBlocksPropertyType,
|
|
7
10
|
isComputedPropertyType,
|
|
8
11
|
isEmptyPropertyValue,
|
|
12
|
+
isPrimaryBlocksField,
|
|
9
13
|
normalizePropertyValue,
|
|
14
|
+
resolveBlocksFieldValue,
|
|
10
15
|
normalizePropertyVisibility,
|
|
11
16
|
parsePropertyOptions,
|
|
12
17
|
parsePropertyValue,
|
|
@@ -386,6 +391,10 @@ function normalizeStringList(value: unknown) {
|
|
|
386
391
|
export async function listPropertiesForDocument(document: DocumentRow) {
|
|
387
392
|
const database = await resolvePropertyDatabaseForDocument(document);
|
|
388
393
|
if (!database) return [];
|
|
394
|
+
// Read path: PURE read. Seeding the primary Blocks field happens at create
|
|
395
|
+
// time and via the one-time startup repair (repairUnseededBlocksFields) —
|
|
396
|
+
// never here. A viewer opening a shared/legacy row must not trigger writes on
|
|
397
|
+
// another owner's database.
|
|
389
398
|
return listPropertiesForDatabase(database.id, document);
|
|
390
399
|
}
|
|
391
400
|
|
|
@@ -416,6 +425,13 @@ export async function listPropertiesForDatabase(
|
|
|
416
425
|
? await databaseRowNumbersByDocumentId(databaseId)
|
|
417
426
|
: new Map<string, number>();
|
|
418
427
|
|
|
428
|
+
// Additional (non-primary) Blocks fields keep their content in their own
|
|
429
|
+
// store, keyed by (documentId, propertyId). Load this row's contents up front
|
|
430
|
+
// so each Blocks field resolves to its OWN independent content.
|
|
431
|
+
const blockContentByPropertyId = valueDocument
|
|
432
|
+
? await blockFieldContentsForDocument(valueDocument.id)
|
|
433
|
+
: new Map<string, string>();
|
|
434
|
+
|
|
419
435
|
const properties = definitions.map((definition) => {
|
|
420
436
|
const type = definition.type as DocumentPropertyType;
|
|
421
437
|
const storedValue = valueByPropertyId.get(definition.id);
|
|
@@ -437,7 +453,15 @@ export async function listPropertiesForDatabase(
|
|
|
437
453
|
? computedPropertyValue(type, valueDocument, {
|
|
438
454
|
databaseRowNumber: rowNumberByDocumentId.get(valueDocument.id),
|
|
439
455
|
})
|
|
440
|
-
:
|
|
456
|
+
: valueDocument && isBlocksPropertyType(type)
|
|
457
|
+
? // Each Blocks field reads from exactly one place: the primary from
|
|
458
|
+
// the document body, additional fields from their own store.
|
|
459
|
+
resolveBlocksFieldValue({
|
|
460
|
+
options,
|
|
461
|
+
documentBody: valueDocument.content,
|
|
462
|
+
blockFieldContent: blockContentByPropertyId.get(definition.id),
|
|
463
|
+
})
|
|
464
|
+
: parsePropertyValue(storedValue?.valueJson),
|
|
441
465
|
editable: !isComputedPropertyType(type),
|
|
442
466
|
};
|
|
443
467
|
});
|
|
@@ -544,8 +568,14 @@ async function propertyValuesForLinkedDocuments(
|
|
|
544
568
|
const docs = await db
|
|
545
569
|
.select()
|
|
546
570
|
.from(schema.documents)
|
|
547
|
-
.where(
|
|
571
|
+
.where(
|
|
572
|
+
and(
|
|
573
|
+
inArray(schema.documents.id, documentIds),
|
|
574
|
+
accessFilter(schema.documents, schema.documentShares),
|
|
575
|
+
),
|
|
576
|
+
);
|
|
548
577
|
const docById = new Map(docs.map((doc) => [doc.id, doc]));
|
|
578
|
+
const accessibleDocumentIds = docs.map((doc) => doc.id);
|
|
549
579
|
|
|
550
580
|
if (isComputedPropertyType(property.definition.type)) {
|
|
551
581
|
const rowNumberByDocumentId = property.definition.databaseId
|
|
@@ -561,12 +591,55 @@ async function propertyValuesForLinkedDocuments(
|
|
|
561
591
|
});
|
|
562
592
|
}
|
|
563
593
|
|
|
594
|
+
if (accessibleDocumentIds.length === 0) {
|
|
595
|
+
return documentIds.map(() => null);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (isBlocksPropertyType(property.definition.type)) {
|
|
599
|
+
const blockFieldRows = isPrimaryBlocksField(property.definition.options)
|
|
600
|
+
? []
|
|
601
|
+
: await db
|
|
602
|
+
.select({
|
|
603
|
+
documentId: schema.documentBlockFieldContents.documentId,
|
|
604
|
+
content: schema.documentBlockFieldContents.content,
|
|
605
|
+
})
|
|
606
|
+
.from(schema.documentBlockFieldContents)
|
|
607
|
+
.where(
|
|
608
|
+
and(
|
|
609
|
+
inArray(
|
|
610
|
+
schema.documentBlockFieldContents.documentId,
|
|
611
|
+
accessibleDocumentIds,
|
|
612
|
+
),
|
|
613
|
+
eq(
|
|
614
|
+
schema.documentBlockFieldContents.propertyId,
|
|
615
|
+
property.definition.id,
|
|
616
|
+
),
|
|
617
|
+
),
|
|
618
|
+
);
|
|
619
|
+
const blockFieldContentByDocumentId = new Map(
|
|
620
|
+
blockFieldRows.map((row) => [row.documentId, row.content]),
|
|
621
|
+
);
|
|
622
|
+
return documentIds.map((documentId) => {
|
|
623
|
+
const doc = docById.get(documentId);
|
|
624
|
+
return doc
|
|
625
|
+
? resolveBlocksFieldValue({
|
|
626
|
+
options: property.definition.options,
|
|
627
|
+
documentBody: doc.content,
|
|
628
|
+
blockFieldContent: blockFieldContentByDocumentId.get(documentId),
|
|
629
|
+
})
|
|
630
|
+
: null;
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
564
634
|
const storedValues = await db
|
|
565
635
|
.select()
|
|
566
636
|
.from(schema.documentPropertyValues)
|
|
567
637
|
.where(
|
|
568
638
|
and(
|
|
569
|
-
inArray(
|
|
639
|
+
inArray(
|
|
640
|
+
schema.documentPropertyValues.documentId,
|
|
641
|
+
accessibleDocumentIds,
|
|
642
|
+
),
|
|
570
643
|
eq(schema.documentPropertyValues.propertyId, property.definition.id),
|
|
571
644
|
),
|
|
572
645
|
);
|
|
@@ -612,3 +685,297 @@ export function normalizedValueJson(
|
|
|
612
685
|
) {
|
|
613
686
|
return serializePropertyValue(normalizePropertyValue(type, value));
|
|
614
687
|
}
|
|
688
|
+
|
|
689
|
+
// --- Blocks fields ---------------------------------------------------------
|
|
690
|
+
//
|
|
691
|
+
// Storage model: the default/primary "Content" Blocks field is backed by
|
|
692
|
+
// `documents.content`. Every ADDITIONAL Blocks field stores its content in its
|
|
693
|
+
// own row in `document_block_field_contents`, keyed by (documentId,
|
|
694
|
+
// propertyId). This guarantees independence — no two Blocks fields ever share
|
|
695
|
+
// content.
|
|
696
|
+
|
|
697
|
+
// Load all additional-Blocks-field contents for a single document, keyed by
|
|
698
|
+
// propertyId. The primary field is intentionally absent here (its content lives
|
|
699
|
+
// on the document itself).
|
|
700
|
+
export async function blockFieldContentsForDocument(
|
|
701
|
+
documentId: string,
|
|
702
|
+
): Promise<Map<string, string>> {
|
|
703
|
+
const db = getDb();
|
|
704
|
+
const rows = await db
|
|
705
|
+
.select({
|
|
706
|
+
propertyId: schema.documentBlockFieldContents.propertyId,
|
|
707
|
+
content: schema.documentBlockFieldContents.content,
|
|
708
|
+
})
|
|
709
|
+
.from(schema.documentBlockFieldContents)
|
|
710
|
+
.where(eq(schema.documentBlockFieldContents.documentId, documentId));
|
|
711
|
+
return new Map(rows.map((row) => [row.propertyId, row.content ?? ""]));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export async function readBlockFieldContent(
|
|
715
|
+
documentId: string,
|
|
716
|
+
propertyId: string,
|
|
717
|
+
): Promise<string> {
|
|
718
|
+
const db = getDb();
|
|
719
|
+
const [row] = await db
|
|
720
|
+
.select({ content: schema.documentBlockFieldContents.content })
|
|
721
|
+
.from(schema.documentBlockFieldContents)
|
|
722
|
+
.where(
|
|
723
|
+
and(
|
|
724
|
+
eq(schema.documentBlockFieldContents.documentId, documentId),
|
|
725
|
+
eq(schema.documentBlockFieldContents.propertyId, propertyId),
|
|
726
|
+
),
|
|
727
|
+
);
|
|
728
|
+
return row?.content ?? "";
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Upsert the content for an additional (non-primary) Blocks field.
|
|
732
|
+
//
|
|
733
|
+
// Atomic insert-or-update on the UNIQUE (document_id, property_id) index — no
|
|
734
|
+
// read-then-write window. Two concurrent first-saves can no longer race into a
|
|
735
|
+
// duplicate-key throw: the loser falls through to the conflict UPDATE branch.
|
|
736
|
+
export async function writeBlockFieldContent(args: {
|
|
737
|
+
documentId: string;
|
|
738
|
+
propertyId: string;
|
|
739
|
+
ownerEmail: string;
|
|
740
|
+
content: string;
|
|
741
|
+
now: string;
|
|
742
|
+
}): Promise<void> {
|
|
743
|
+
const db = getDb();
|
|
744
|
+
await db
|
|
745
|
+
.insert(schema.documentBlockFieldContents)
|
|
746
|
+
.values({
|
|
747
|
+
id: nanoid(),
|
|
748
|
+
ownerEmail: args.ownerEmail,
|
|
749
|
+
documentId: args.documentId,
|
|
750
|
+
propertyId: args.propertyId,
|
|
751
|
+
content: args.content,
|
|
752
|
+
createdAt: args.now,
|
|
753
|
+
updatedAt: args.now,
|
|
754
|
+
})
|
|
755
|
+
.onConflictDoUpdate({
|
|
756
|
+
target: [
|
|
757
|
+
schema.documentBlockFieldContents.documentId,
|
|
758
|
+
schema.documentBlockFieldContents.propertyId,
|
|
759
|
+
],
|
|
760
|
+
set: { content: args.content, updatedAt: args.now },
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Write the primary Blocks field's content — i.e. the document body.
|
|
765
|
+
export async function writePrimaryBlocksContent(args: {
|
|
766
|
+
documentId: string;
|
|
767
|
+
content: string;
|
|
768
|
+
now: string;
|
|
769
|
+
}): Promise<void> {
|
|
770
|
+
await assertAccess("document", args.documentId, "editor");
|
|
771
|
+
const db = getDb();
|
|
772
|
+
await db
|
|
773
|
+
.update(schema.documents)
|
|
774
|
+
.set({ content: args.content, updatedAt: args.now })
|
|
775
|
+
.where(eq(schema.documents.id, args.documentId));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Fetch a single property definition scoped to a database (and owner).
|
|
779
|
+
export async function getPropertyDefinitionForDatabase(args: {
|
|
780
|
+
propertyId: string;
|
|
781
|
+
databaseId: string;
|
|
782
|
+
ownerEmail: string;
|
|
783
|
+
}) {
|
|
784
|
+
const db = getDb();
|
|
785
|
+
const [definition] = await db
|
|
786
|
+
.select()
|
|
787
|
+
.from(schema.documentPropertyDefinitions)
|
|
788
|
+
.where(
|
|
789
|
+
and(
|
|
790
|
+
eq(schema.documentPropertyDefinitions.id, args.propertyId),
|
|
791
|
+
eq(schema.documentPropertyDefinitions.ownerEmail, args.ownerEmail),
|
|
792
|
+
eq(schema.documentPropertyDefinitions.databaseId, args.databaseId),
|
|
793
|
+
),
|
|
794
|
+
);
|
|
795
|
+
return definition ?? null;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// How many Blocks-type property definitions a database has. Used to drive the
|
|
799
|
+
// solo (chromeless) vs. multi (headers + collapsible) rendering decision and
|
|
800
|
+
// the "only Blocks field" delete warning.
|
|
801
|
+
export async function countBlocksFieldsForDatabase(
|
|
802
|
+
databaseId: string,
|
|
803
|
+
): Promise<number> {
|
|
804
|
+
const db = getDb();
|
|
805
|
+
const definitions = await db
|
|
806
|
+
.select({ type: schema.documentPropertyDefinitions.type })
|
|
807
|
+
.from(schema.documentPropertyDefinitions)
|
|
808
|
+
.where(eq(schema.documentPropertyDefinitions.databaseId, databaseId));
|
|
809
|
+
return definitions.filter((definition) =>
|
|
810
|
+
isBlocksPropertyType(definition.type as DocumentPropertyType),
|
|
811
|
+
).length;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// The id of a database's existing primary Blocks definition, if any. Used to
|
|
815
|
+
// adopt a legacy primary created by the old read-path seeder rather than
|
|
816
|
+
// creating a duplicate.
|
|
817
|
+
async function findExistingPrimaryBlocksDefinition(
|
|
818
|
+
databaseId: string,
|
|
819
|
+
): Promise<string | null> {
|
|
820
|
+
const db = getDb();
|
|
821
|
+
const definitions = await db
|
|
822
|
+
.select({
|
|
823
|
+
id: schema.documentPropertyDefinitions.id,
|
|
824
|
+
type: schema.documentPropertyDefinitions.type,
|
|
825
|
+
optionsJson: schema.documentPropertyDefinitions.optionsJson,
|
|
826
|
+
})
|
|
827
|
+
.from(schema.documentPropertyDefinitions)
|
|
828
|
+
.where(eq(schema.documentPropertyDefinitions.databaseId, databaseId));
|
|
829
|
+
const primary = definitions.find(
|
|
830
|
+
(definition) =>
|
|
831
|
+
isBlocksPropertyType(definition.type as DocumentPropertyType) &&
|
|
832
|
+
isPrimaryBlocksField(parsePropertyOptions(definition.optionsJson)),
|
|
833
|
+
);
|
|
834
|
+
return primary?.id ?? null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Seed the primary "Content" Blocks field for a database exactly ONCE.
|
|
838
|
+
//
|
|
839
|
+
// `content_databases.primary_blocks_property_id` is the single source of truth
|
|
840
|
+
// and the concurrency guard. The deterministic primary definition is inserted
|
|
841
|
+
// before the database row is marked seeded, so we never publish blocks_seeded=1
|
|
842
|
+
// before the definition row exists. Two concurrent calls converge on the same
|
|
843
|
+
// property id and the loser returns the already-claimed id.
|
|
844
|
+
//
|
|
845
|
+
// Returns the primary property id (existing or newly created). Never reseeds a
|
|
846
|
+
// database whose primary was intentionally deleted (blocks_seeded=1, id NULL).
|
|
847
|
+
export async function seedDefaultBlocksField(args: {
|
|
848
|
+
databaseId: string;
|
|
849
|
+
ownerEmail: string;
|
|
850
|
+
orgId: string | null;
|
|
851
|
+
now: string;
|
|
852
|
+
}): Promise<string | null> {
|
|
853
|
+
const db = getDb();
|
|
854
|
+
|
|
855
|
+
// Deterministic id keyed to the database so concurrent claimants converge on
|
|
856
|
+
// the same value; the UNIQUE primary-key on definitions also rejects a
|
|
857
|
+
// duplicate insert if two callers somehow both attempt it.
|
|
858
|
+
const id = `blocks_primary_${args.databaseId}`;
|
|
859
|
+
|
|
860
|
+
// Legacy adoption: a database seeded by the OLD read-path safety net already
|
|
861
|
+
// has a primary "Content" definition but a NULL column (if the v52 backfill
|
|
862
|
+
// somehow didn't run for it). Adopt that existing definition instead of
|
|
863
|
+
// creating a second primary — guarantees the invariant even off the migration
|
|
864
|
+
// path. The atomic UPDATE (column still NULL) makes this race-safe.
|
|
865
|
+
const existingPrimary = await findExistingPrimaryBlocksDefinition(
|
|
866
|
+
args.databaseId,
|
|
867
|
+
);
|
|
868
|
+
if (existingPrimary) {
|
|
869
|
+
await db
|
|
870
|
+
.update(schema.contentDatabases)
|
|
871
|
+
.set({
|
|
872
|
+
primaryBlocksPropertyId: existingPrimary,
|
|
873
|
+
blocksSeeded: 1,
|
|
874
|
+
updatedAt: args.now,
|
|
875
|
+
})
|
|
876
|
+
.where(
|
|
877
|
+
and(
|
|
878
|
+
eq(schema.contentDatabases.id, args.databaseId),
|
|
879
|
+
sql`primary_blocks_property_id IS NULL`,
|
|
880
|
+
),
|
|
881
|
+
);
|
|
882
|
+
return existingPrimary;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const [database] = await db
|
|
886
|
+
.select({
|
|
887
|
+
primaryId: schema.contentDatabases.primaryBlocksPropertyId,
|
|
888
|
+
blocksSeeded: schema.contentDatabases.blocksSeeded,
|
|
889
|
+
})
|
|
890
|
+
.from(schema.contentDatabases)
|
|
891
|
+
.where(eq(schema.contentDatabases.id, args.databaseId));
|
|
892
|
+
if (!database) return null;
|
|
893
|
+
if (database.primaryId) return database.primaryId;
|
|
894
|
+
if (database.blocksSeeded === 1) return null;
|
|
895
|
+
|
|
896
|
+
const [maxPos] = await db
|
|
897
|
+
.select({ max: sql<number>`COALESCE(MAX(position), -1)` })
|
|
898
|
+
.from(schema.documentPropertyDefinitions)
|
|
899
|
+
.where(eq(schema.documentPropertyDefinitions.databaseId, args.databaseId));
|
|
900
|
+
|
|
901
|
+
await db
|
|
902
|
+
.insert(schema.documentPropertyDefinitions)
|
|
903
|
+
.values({
|
|
904
|
+
id,
|
|
905
|
+
ownerEmail: args.ownerEmail,
|
|
906
|
+
orgId: args.orgId,
|
|
907
|
+
databaseId: args.databaseId,
|
|
908
|
+
name: DEFAULT_BLOCKS_FIELD_NAME,
|
|
909
|
+
type: "blocks",
|
|
910
|
+
visibility: "always_show",
|
|
911
|
+
optionsJson: serializePropertyOptions({ blocks: { primary: true } }),
|
|
912
|
+
position: (maxPos?.max ?? -1) + 1,
|
|
913
|
+
createdAt: args.now,
|
|
914
|
+
updatedAt: args.now,
|
|
915
|
+
})
|
|
916
|
+
.onConflictDoNothing();
|
|
917
|
+
|
|
918
|
+
const claim = await db
|
|
919
|
+
.update(schema.contentDatabases)
|
|
920
|
+
.set({
|
|
921
|
+
primaryBlocksPropertyId: id,
|
|
922
|
+
blocksSeeded: 1,
|
|
923
|
+
updatedAt: args.now,
|
|
924
|
+
})
|
|
925
|
+
.where(
|
|
926
|
+
and(
|
|
927
|
+
eq(schema.contentDatabases.id, args.databaseId),
|
|
928
|
+
sql`primary_blocks_property_id IS NULL`,
|
|
929
|
+
sql`blocks_seeded = 0`,
|
|
930
|
+
),
|
|
931
|
+
)
|
|
932
|
+
.returning({ id: schema.contentDatabases.primaryBlocksPropertyId });
|
|
933
|
+
|
|
934
|
+
if (claim[0]?.id === id) return id;
|
|
935
|
+
|
|
936
|
+
const [afterClaim] = await db
|
|
937
|
+
.select({ primaryId: schema.contentDatabases.primaryBlocksPropertyId })
|
|
938
|
+
.from(schema.contentDatabases)
|
|
939
|
+
.where(eq(schema.contentDatabases.id, args.databaseId));
|
|
940
|
+
if (afterClaim?.primaryId) return afterClaim.primaryId;
|
|
941
|
+
|
|
942
|
+
await db
|
|
943
|
+
.delete(schema.documentPropertyDefinitions)
|
|
944
|
+
.where(eq(schema.documentPropertyDefinitions.id, id));
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// One-time startup repair for LEGACY databases that have never been seeded —
|
|
949
|
+
// i.e. databases created before the Blocks type existed and which have NO
|
|
950
|
+
// primary Blocks field yet (blocks_seeded = 0). Their `documents.content` body
|
|
951
|
+
// still works; seeding the primary field exposes it as a first-class property.
|
|
952
|
+
//
|
|
953
|
+
// Runs at boot from the migration plugin, NOT from any read path, so opening a
|
|
954
|
+
// shared/legacy row never triggers a write. Uses each database's own owner/org
|
|
955
|
+
// (no request context). Idempotent: the atomic claim in seedDefaultBlocksField
|
|
956
|
+
// makes re-runs no-ops, and databases whose primary was intentionally deleted
|
|
957
|
+
// (blocks_seeded = 1) are skipped.
|
|
958
|
+
export async function repairUnseededBlocksFields(): Promise<number> {
|
|
959
|
+
const db = getDb();
|
|
960
|
+
const databases = await db
|
|
961
|
+
.select({
|
|
962
|
+
id: schema.contentDatabases.id,
|
|
963
|
+
ownerEmail: schema.contentDatabases.ownerEmail,
|
|
964
|
+
orgId: schema.contentDatabases.orgId,
|
|
965
|
+
})
|
|
966
|
+
.from(schema.contentDatabases)
|
|
967
|
+
.where(eq(schema.contentDatabases.blocksSeeded, 0));
|
|
968
|
+
|
|
969
|
+
const now = new Date().toISOString();
|
|
970
|
+
let seeded = 0;
|
|
971
|
+
for (const database of databases) {
|
|
972
|
+
await seedDefaultBlocksField({
|
|
973
|
+
databaseId: database.id,
|
|
974
|
+
ownerEmail: database.ownerEmail,
|
|
975
|
+
orgId: database.orgId ?? null,
|
|
976
|
+
now,
|
|
977
|
+
});
|
|
978
|
+
seeded += 1;
|
|
979
|
+
}
|
|
980
|
+
return seeded;
|
|
981
|
+
}
|
|
@@ -7,7 +7,11 @@ import { getDb, schema } from "../server/db/index.js";
|
|
|
7
7
|
import {
|
|
8
8
|
CREATABLE_DOCUMENT_PROPERTY_TYPES,
|
|
9
9
|
DOCUMENT_PROPERTY_VISIBILITIES,
|
|
10
|
+
isBlocksPropertyType,
|
|
10
11
|
isComputedPropertyType,
|
|
12
|
+
isPrimaryBlocksField,
|
|
13
|
+
parsePropertyOptions,
|
|
14
|
+
serializePropertyOptions,
|
|
11
15
|
normalizePropertyVisibility,
|
|
12
16
|
type DocumentPropertyType,
|
|
13
17
|
} from "../shared/properties.js";
|
|
@@ -79,7 +83,7 @@ export default defineAction({
|
|
|
79
83
|
const now = new Date().toISOString();
|
|
80
84
|
const name = args.name.trim();
|
|
81
85
|
const type = args.type as DocumentPropertyType;
|
|
82
|
-
|
|
86
|
+
let optionsJson = optionsForNewProperty(type, args.options as any);
|
|
83
87
|
const database = await resolvePropertyDatabaseForDocument(document);
|
|
84
88
|
if (!database) {
|
|
85
89
|
throw new Error(
|
|
@@ -108,6 +112,27 @@ export default defineAction({
|
|
|
108
112
|
) {
|
|
109
113
|
throw new Error("Computed property types cannot be changed.");
|
|
110
114
|
}
|
|
115
|
+
|
|
116
|
+
const existingOptions = parsePropertyOptions(existing.optionsJson);
|
|
117
|
+
const existingIsPrimaryBlocks =
|
|
118
|
+
isBlocksPropertyType(existing.type as DocumentPropertyType) &&
|
|
119
|
+
isPrimaryBlocksField(existingOptions);
|
|
120
|
+
|
|
121
|
+
// The primary "Content" Blocks field backs the document body — it can be
|
|
122
|
+
// renamed/hidden but not retyped (delete it from the database view to
|
|
123
|
+
// remove the body). Block the type switch defensively.
|
|
124
|
+
if (existingIsPrimaryBlocks && existing.type !== type) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"The primary Content (Blocks) field cannot change type. Delete it from the database view to remove the body.",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Preserve the primary flag when re-saving the primary Blocks field (a
|
|
131
|
+
// rename or visibility change must NOT demote it to a normal Blocks field).
|
|
132
|
+
if (existingIsPrimaryBlocks && isBlocksPropertyType(type)) {
|
|
133
|
+
optionsJson = serializePropertyOptions({ blocks: { primary: true } });
|
|
134
|
+
}
|
|
135
|
+
|
|
111
136
|
if (existing.type !== type) {
|
|
112
137
|
await db
|
|
113
138
|
.delete(schema.documentPropertyValues)
|
|
@@ -117,6 +142,15 @@ export default defineAction({
|
|
|
117
142
|
eq(schema.documentPropertyValues.ownerEmail, document.ownerEmail),
|
|
118
143
|
),
|
|
119
144
|
);
|
|
145
|
+
// Switching a Blocks field to another type drops its independent content.
|
|
146
|
+
if (
|
|
147
|
+
isBlocksPropertyType(existing.type as DocumentPropertyType) &&
|
|
148
|
+
!isBlocksPropertyType(type)
|
|
149
|
+
) {
|
|
150
|
+
await db
|
|
151
|
+
.delete(schema.documentBlockFieldContents)
|
|
152
|
+
.where(eq(schema.documentBlockFieldContents.propertyId, args.id));
|
|
153
|
+
}
|
|
120
154
|
}
|
|
121
155
|
|
|
122
156
|
await db
|
|
@@ -10,7 +10,7 @@ import { and, eq, sql } from "drizzle-orm";
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { getDb, schema } from "../server/db/index.js";
|
|
12
12
|
import { getContentDatabaseResponse } from "./_database-utils.js";
|
|
13
|
-
import { nanoid } from "./_property-utils.js";
|
|
13
|
+
import { nanoid, seedDefaultBlocksField } from "./_property-utils.js";
|
|
14
14
|
|
|
15
15
|
export default defineAction({
|
|
16
16
|
description:
|
|
@@ -151,6 +151,10 @@ export default defineAction({
|
|
|
151
151
|
updatedAt: now,
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
// Every database is seeded with one primary "Content" Blocks field, backed
|
|
155
|
+
// by `documents.content`, so each row's body is a first-class property.
|
|
156
|
+
await seedDefaultBlocksField({ databaseId, ownerEmail, orgId, now });
|
|
157
|
+
|
|
154
158
|
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
155
159
|
|
|
156
160
|
return getContentDatabaseResponse(databaseId);
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
3
3
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
4
|
-
import { and, eq } from "drizzle-orm";
|
|
4
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
isBlocksPropertyType,
|
|
9
|
+
isPrimaryBlocksField,
|
|
10
|
+
parsePropertyOptions,
|
|
11
|
+
type DocumentPropertyType,
|
|
12
|
+
} from "../shared/properties.js";
|
|
7
13
|
import {
|
|
8
14
|
listPropertiesForDocument,
|
|
9
15
|
resolvePropertyDatabaseForDocument,
|
|
@@ -24,7 +30,7 @@ export default defineAction({
|
|
|
24
30
|
if (!database) throw new Error("Document is not part of a database.");
|
|
25
31
|
|
|
26
32
|
const [definition] = await db
|
|
27
|
-
.select(
|
|
33
|
+
.select()
|
|
28
34
|
.from(schema.documentPropertyDefinitions)
|
|
29
35
|
.where(
|
|
30
36
|
and(
|
|
@@ -38,6 +44,13 @@ export default defineAction({
|
|
|
38
44
|
);
|
|
39
45
|
if (!definition) throw new Error(`Property "${propertyId}" not found`);
|
|
40
46
|
|
|
47
|
+
const isBlocks = isBlocksPropertyType(
|
|
48
|
+
definition.type as DocumentPropertyType,
|
|
49
|
+
);
|
|
50
|
+
const isPrimaryBlocks =
|
|
51
|
+
isBlocks &&
|
|
52
|
+
isPrimaryBlocksField(parsePropertyOptions(definition.optionsJson));
|
|
53
|
+
|
|
41
54
|
await db
|
|
42
55
|
.delete(schema.documentPropertyValues)
|
|
43
56
|
.where(eq(schema.documentPropertyValues.propertyId, propertyId));
|
|
@@ -45,6 +58,43 @@ export default defineAction({
|
|
|
45
58
|
.delete(schema.documentPropertyDefinitions)
|
|
46
59
|
.where(eq(schema.documentPropertyDefinitions.id, propertyId));
|
|
47
60
|
|
|
61
|
+
if (isBlocks) {
|
|
62
|
+
// Drop the independent content for this Blocks field across every row.
|
|
63
|
+
await db
|
|
64
|
+
.delete(schema.documentBlockFieldContents)
|
|
65
|
+
.where(eq(schema.documentBlockFieldContents.propertyId, propertyId));
|
|
66
|
+
|
|
67
|
+
// Deleting the primary "Content" field removes the body (documents.content)
|
|
68
|
+
// for every object of this type, per the delete warning shown in the UI.
|
|
69
|
+
if (isPrimaryBlocks) {
|
|
70
|
+
// Record that the primary was intentionally removed: clear the single
|
|
71
|
+
// source of truth but LEAVE blocks_seeded = 1, so neither the read path
|
|
72
|
+
// nor the startup repair ever recreates it. Deleting the only Blocks
|
|
73
|
+
// field is an allowed product action that leaves the row metadata-only
|
|
74
|
+
// with ZERO Blocks fields.
|
|
75
|
+
await db
|
|
76
|
+
.update(schema.contentDatabases)
|
|
77
|
+
.set({
|
|
78
|
+
primaryBlocksPropertyId: null,
|
|
79
|
+
updatedAt: new Date().toISOString(),
|
|
80
|
+
})
|
|
81
|
+
.where(eq(schema.contentDatabases.id, database.id));
|
|
82
|
+
|
|
83
|
+
const items = await db
|
|
84
|
+
.select({ documentId: schema.contentDatabaseItems.documentId })
|
|
85
|
+
.from(schema.contentDatabaseItems)
|
|
86
|
+
.where(eq(schema.contentDatabaseItems.databaseId, database.id));
|
|
87
|
+
const documentIds = items.map((item) => item.documentId);
|
|
88
|
+
if (documentIds.length > 0) {
|
|
89
|
+
const now = new Date().toISOString();
|
|
90
|
+
await db
|
|
91
|
+
.update(schema.documents)
|
|
92
|
+
.set({ content: "", updatedAt: now })
|
|
93
|
+
.where(inArray(schema.documents.id, documentIds));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
48
98
|
// Free any source field that was mapped to this property so it returns to
|
|
49
99
|
// the "From source" picker immediately, instead of staying orphaned until
|
|
50
100
|
// the next source refresh reconciles it.
|
|
@@ -4,6 +4,11 @@ import { assertAccess } from "@agent-native/core/sharing";
|
|
|
4
4
|
import { and, eq, sql } from "drizzle-orm";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
isBlocksPropertyType,
|
|
9
|
+
serializePropertyOptions,
|
|
10
|
+
type DocumentPropertyType,
|
|
11
|
+
} from "../shared/properties.js";
|
|
7
12
|
import {
|
|
8
13
|
listPropertiesForDocument,
|
|
9
14
|
nanoid,
|
|
@@ -56,6 +61,15 @@ export default defineAction({
|
|
|
56
61
|
|
|
57
62
|
const now = new Date().toISOString();
|
|
58
63
|
const newPropertyId = nanoid();
|
|
64
|
+
const isBlocks = isBlocksPropertyType(
|
|
65
|
+
definition.type as DocumentPropertyType,
|
|
66
|
+
);
|
|
67
|
+
// A duplicated Blocks field is a brand-new, independent, EMPTY field — never
|
|
68
|
+
// primary (only one field backs the body) and with no copied content.
|
|
69
|
+
const optionsJson = isBlocks
|
|
70
|
+
? serializePropertyOptions({ blocks: { primary: false } })
|
|
71
|
+
: definition.optionsJson;
|
|
72
|
+
|
|
59
73
|
await db.insert(schema.documentPropertyDefinitions).values({
|
|
60
74
|
id: newPropertyId,
|
|
61
75
|
ownerEmail: definition.ownerEmail,
|
|
@@ -64,28 +78,31 @@ export default defineAction({
|
|
|
64
78
|
name: `${definition.name} copy`,
|
|
65
79
|
type: definition.type,
|
|
66
80
|
visibility: definition.visibility,
|
|
67
|
-
optionsJson
|
|
81
|
+
optionsJson,
|
|
68
82
|
position: (maxPos?.max ?? -1) + 1,
|
|
69
83
|
createdAt: now,
|
|
70
84
|
updatedAt: now,
|
|
71
85
|
});
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// Blocks fields don't use document_property_values; a duplicate starts empty.
|
|
88
|
+
if (!isBlocks) {
|
|
89
|
+
const values = await db
|
|
90
|
+
.select()
|
|
91
|
+
.from(schema.documentPropertyValues)
|
|
92
|
+
.where(eq(schema.documentPropertyValues.propertyId, propertyId));
|
|
93
|
+
if (values.length > 0) {
|
|
94
|
+
await db.insert(schema.documentPropertyValues).values(
|
|
95
|
+
values.map((value) => ({
|
|
96
|
+
id: nanoid(),
|
|
97
|
+
ownerEmail: value.ownerEmail,
|
|
98
|
+
documentId: value.documentId,
|
|
99
|
+
propertyId: newPropertyId,
|
|
100
|
+
valueJson: value.valueJson,
|
|
101
|
+
createdAt: now,
|
|
102
|
+
updatedAt: now,
|
|
103
|
+
})),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
89
106
|
}
|
|
90
107
|
|
|
91
108
|
await writeAppState("refresh-signal", { ts: Date.now() });
|