@agent-native/core 0.82.0 → 0.83.0
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 +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/CommandMenu.tsx +34 -12
- package/corpus/templates/analytics/actions/hubspot-deals.ts +64 -4
- package/corpus/templates/content/.env.local.example +6 -0
- package/corpus/templates/content/AGENTS.md +10 -1
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +14 -1
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +43 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +79 -7
- package/corpus/templates/content/actions/_database-source-utils.ts +987 -22
- package/corpus/templates/content/actions/_database-utils.ts +41 -0
- package/corpus/templates/content/actions/_local-file-documents.ts +74 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +33 -8
- package/corpus/templates/content/actions/change-content-database-source-role.ts +14 -0
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +3 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +71 -3
- package/corpus/templates/content/actions/list-content-databases.ts +41 -33
- package/corpus/templates/content/actions/process-builder-body-hydration.ts +47 -0
- package/corpus/templates/content/actions/update-document.ts +93 -0
- package/corpus/templates/content/app/blocks/SourceComponentBlock.tsx +277 -0
- package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +45 -4
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +5 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +92 -0
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +5 -3
- package/corpus/templates/content/app/components/editor/extensions/registryBlocks.ts +41 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +4 -0
- package/corpus/templates/content/app/global.css +10 -3
- package/corpus/templates/content/app/hooks/use-content-database.ts +24 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +24 -0
- package/corpus/templates/content/app/i18n-data.ts +212 -0
- package/corpus/templates/content/app/lib/content-command-search.ts +64 -0
- package/corpus/templates/content/app/root.tsx +236 -8
- package/corpus/templates/content/changelog/2026-06-29-builder-cms-sources-now-sync-article-bodies-through-content.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-article-media-now-render-as-images-and-embeds-when-r.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-source-components-now-render-as-preserved-preview-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-cmd-k-search-now-opens-from-the-editor-and-finds-real-content.md +6 -0
- package/corpus/templates/content/package.json +3 -1
- package/corpus/templates/content/scripts/check-native-deps.mjs +57 -0
- package/corpus/templates/content/scripts/dev-database.mjs +83 -0
- package/corpus/templates/content/scripts/seed-demo-user.mjs +107 -0
- package/corpus/templates/content/server/db/schema.ts +27 -0
- package/corpus/templates/content/server/plugins/db.ts +33 -0
- package/corpus/templates/content/shared/api.ts +45 -0
- package/corpus/templates/content/shared/builder-mdx.ts +905 -5
- package/corpus/templates/content/shared/nfm-registry.ts +2 -0
- package/corpus/templates/content/shared/source-component-block.ts +141 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +5 -1
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +229 -131
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +103 -6
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +6 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +382 -72
- package/corpus/templates/design/changelog/2026-06-30-canvas-editing-now-keeps-undo-redo-and-cross-screen-layer-mo.md +6 -0
- package/dist/client/CommandMenu.d.ts +6 -2
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +25 -14
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { deleteCollabState, releaseDoc } from "@agent-native/core/collab";
|
|
1
2
|
import { and, asc, eq, inArray, isNull, sql } from "drizzle-orm";
|
|
2
3
|
|
|
3
4
|
import { getDb, schema } from "../server/db/index.js";
|
|
4
5
|
import type {
|
|
5
6
|
ContentDatabase,
|
|
7
|
+
ContentDatabaseBodyHydrationSummary,
|
|
6
8
|
ContentDatabaseItem,
|
|
7
9
|
ContentDatabaseResponse,
|
|
8
10
|
ContentDatabaseSource,
|
|
@@ -31,6 +33,14 @@ import type {
|
|
|
31
33
|
DocumentProperty,
|
|
32
34
|
DocumentPropertyValue,
|
|
33
35
|
} from "../shared/api.js";
|
|
36
|
+
import {
|
|
37
|
+
builderBlocksHash,
|
|
38
|
+
builderBlocksToReadableMarkdown,
|
|
39
|
+
builderEntryToReadableMdxBundle,
|
|
40
|
+
builderEntryToMdxBundle,
|
|
41
|
+
builderReadableBodyToBuilderBlocks,
|
|
42
|
+
builderMdxBodyToBuilderBlocks,
|
|
43
|
+
} from "../shared/builder-mdx.js";
|
|
34
44
|
import {
|
|
35
45
|
parsePropertyOptions,
|
|
36
46
|
serializePropertyOptions,
|
|
@@ -44,6 +54,11 @@ import {
|
|
|
44
54
|
type BuilderCmsReadState,
|
|
45
55
|
} from "./_builder-cms-read-client.js";
|
|
46
56
|
import {
|
|
57
|
+
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
58
|
+
BUILDER_CMS_BODY_CONTENT_KEY,
|
|
59
|
+
BUILDER_CMS_BODY_LOSSLESS_CONTENT_KEY,
|
|
60
|
+
BUILDER_CMS_BODY_READABLE_MAP_KEY,
|
|
61
|
+
BUILDER_CMS_BODY_SIDECARS_KEY,
|
|
47
62
|
BUILDER_CMS_FIXTURE_ROW_PROVENANCE,
|
|
48
63
|
buildBuilderCmsFixtureEntry,
|
|
49
64
|
builderCmsQualifiedId,
|
|
@@ -63,6 +78,9 @@ type ContentDatabaseSourceFieldRowDb =
|
|
|
63
78
|
typeof schema.contentDatabaseSourceFields.$inferSelect;
|
|
64
79
|
type ContentDatabaseSourceRecordRowDb =
|
|
65
80
|
typeof schema.contentDatabaseSourceRows.$inferSelect;
|
|
81
|
+
type ContentDatabaseItemRowDb = typeof schema.contentDatabaseItems.$inferSelect;
|
|
82
|
+
type ContentDatabaseBodyHydrationQueueRowDb =
|
|
83
|
+
typeof schema.contentDatabaseBodyHydrationQueue.$inferSelect;
|
|
66
84
|
type ContentDatabaseSourceChangeSetRowDb =
|
|
67
85
|
typeof schema.contentDatabaseSourceChangeSets.$inferSelect;
|
|
68
86
|
type ContentDatabaseSourceChangeReviewRowDb =
|
|
@@ -446,6 +464,836 @@ function sameSourceFieldValue(a: unknown, b: unknown): boolean {
|
|
|
446
464
|
return normalize(a) === normalize(b);
|
|
447
465
|
}
|
|
448
466
|
|
|
467
|
+
function stringSourceValue(
|
|
468
|
+
values: Record<string, DocumentPropertyValue>,
|
|
469
|
+
key: string,
|
|
470
|
+
) {
|
|
471
|
+
const value = values[key];
|
|
472
|
+
return typeof value === "string" ? value : null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function bodyExcerpt(value: string | null | undefined) {
|
|
476
|
+
const excerpt = value?.trim().slice(0, 140) ?? "";
|
|
477
|
+
return excerpt || null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const BUILDER_BODY_HYDRATION_BACKGROUND_PRIORITY = 10;
|
|
481
|
+
const BUILDER_BODY_HYDRATION_OPEN_PRIORITY = 0;
|
|
482
|
+
const BUILDER_BODY_HYDRATION_BATCH_LIMIT = 8;
|
|
483
|
+
const BUILDER_BODY_HYDRATION_CODEC_VERSION = "readable-native-images-v4";
|
|
484
|
+
|
|
485
|
+
async function builderBodySnapshotForEntry(entry: BuilderCmsSourceEntry) {
|
|
486
|
+
if (!entry.rawEntry) return null;
|
|
487
|
+
const [readableBundle, losslessBundle] = await Promise.all([
|
|
488
|
+
builderEntryToReadableMdxBundle(entry.rawEntry),
|
|
489
|
+
builderEntryToMdxBundle(entry.rawEntry),
|
|
490
|
+
]);
|
|
491
|
+
const sidecars: Record<string, string> = {};
|
|
492
|
+
for (const [path, source] of Object.entries(losslessBundle.files)) {
|
|
493
|
+
if (path !== losslessBundle.mdx.path) sidecars[path] = source;
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
content: readableBundle.mdx.body,
|
|
497
|
+
losslessContent: losslessBundle.mdx.body,
|
|
498
|
+
blocksHash: builderBlocksHash(losslessBundle.blocks),
|
|
499
|
+
readableMapJson: null,
|
|
500
|
+
sidecarsJson: JSON.stringify(sidecars),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function builderBodyHydrationVersion(entry: BuilderCmsSourceEntry) {
|
|
505
|
+
const hash = stringSourceValue(
|
|
506
|
+
entry.sourceValues,
|
|
507
|
+
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
508
|
+
);
|
|
509
|
+
return `${hash ?? entry.updatedAt ?? entry.id}:${BUILDER_BODY_HYDRATION_CODEC_VERSION}`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function normalizeHydrationLimit(limit: number | null | undefined) {
|
|
513
|
+
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
514
|
+
return BUILDER_BODY_HYDRATION_BATCH_LIMIT;
|
|
515
|
+
}
|
|
516
|
+
return Math.max(1, Math.min(Math.floor(limit), 50));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function builderBodyIsRawPlaceholderOnly(content: string | null | undefined) {
|
|
520
|
+
const trimmed = content?.trim() ?? "";
|
|
521
|
+
return (
|
|
522
|
+
trimmed.startsWith("<BuilderRawBlock") &&
|
|
523
|
+
!trimmed.includes("<BuilderText") &&
|
|
524
|
+
!trimmed.includes("<BuilderTabbedContent") &&
|
|
525
|
+
!trimmed.includes("<BuilderCodeBlock")
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function builderBodyHasLegacyPreservedComponentPlaceholders(
|
|
530
|
+
content: string | null | undefined,
|
|
531
|
+
) {
|
|
532
|
+
const value = content ?? "";
|
|
533
|
+
return /^>\s*Builder .+ component preserved from source\.$/m.test(value);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function builderBodyWithoutSourceComponentMarkers(
|
|
537
|
+
content: string | null | undefined,
|
|
538
|
+
) {
|
|
539
|
+
return (content ?? "")
|
|
540
|
+
.replace(/(?:^|\n)<SourceComponent\b[\s\S]*?\/>[ \t]*(?=\n|$)/g, "\n")
|
|
541
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
542
|
+
.trim();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function builderBodyWithoutImageSourceComponentMarkers(
|
|
546
|
+
content: string | null | undefined,
|
|
547
|
+
) {
|
|
548
|
+
return (content ?? "")
|
|
549
|
+
.replace(/(?:^|\n)<SourceComponent\b[\s\S]*?\/>[ \t]*(?=\n|$)/g, (marker) =>
|
|
550
|
+
marker.includes('componentName="Image"') ? "\n" : marker,
|
|
551
|
+
)
|
|
552
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
553
|
+
.trim();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function builderBodyWithoutMarkdownImages(content: string | null | undefined) {
|
|
557
|
+
return (content ?? "")
|
|
558
|
+
.replace(
|
|
559
|
+
/(?:^|\n)!\[(?:\\.|[^\]\\])*\]\(\S+?(?:\s+"[^"]*")?\)[ \t]*(?=\n|$)/g,
|
|
560
|
+
"\n",
|
|
561
|
+
)
|
|
562
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
563
|
+
.trim();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function normalizedBuilderBodyProse(content: string | null | undefined) {
|
|
567
|
+
return (content ?? "")
|
|
568
|
+
.normalize("NFKC")
|
|
569
|
+
.replace(/\u00a0/g, " ")
|
|
570
|
+
.replace(/\s+/g, " ")
|
|
571
|
+
.trim();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function builderBodyNeedsSourceComponentWrite(args: {
|
|
575
|
+
currentContent: string | null | undefined;
|
|
576
|
+
nextContent: string | null | undefined;
|
|
577
|
+
}) {
|
|
578
|
+
const nextContent = args.nextContent ?? "";
|
|
579
|
+
const currentContent = args.currentContent ?? "";
|
|
580
|
+
const needsSourceComponentRefresh =
|
|
581
|
+
nextContent.includes("<SourceComponent") &&
|
|
582
|
+
normalizedBuilderBodyProse(
|
|
583
|
+
builderBodyWithoutSourceComponentMarkers(nextContent),
|
|
584
|
+
) ===
|
|
585
|
+
normalizedBuilderBodyProse(
|
|
586
|
+
builderBodyWithoutSourceComponentMarkers(currentContent),
|
|
587
|
+
);
|
|
588
|
+
if (needsSourceComponentRefresh) return true;
|
|
589
|
+
if (
|
|
590
|
+
!currentContent.includes('componentName="Image"') ||
|
|
591
|
+
!nextContent.includes("![")
|
|
592
|
+
) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
return (
|
|
596
|
+
normalizedBuilderBodyProse(
|
|
597
|
+
builderBodyWithoutMarkdownImages(nextContent),
|
|
598
|
+
) ===
|
|
599
|
+
normalizedBuilderBodyProse(
|
|
600
|
+
builderBodyWithoutImageSourceComponentMarkers(currentContent),
|
|
601
|
+
)
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function builderStoredBodyIsStale(args: {
|
|
606
|
+
item: Pick<
|
|
607
|
+
ContentDatabaseItemRowDb,
|
|
608
|
+
"bodyHydrationStatus" | "bodyHydrationVersion"
|
|
609
|
+
>;
|
|
610
|
+
entry: BuilderCmsSourceEntry;
|
|
611
|
+
}) {
|
|
612
|
+
return (
|
|
613
|
+
args.item.bodyHydrationStatus !== "hydrated" ||
|
|
614
|
+
args.item.bodyHydrationVersion !== builderBodyHydrationVersion(args.entry)
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function readableBuilderBodyFromStoredLossless(args: {
|
|
619
|
+
losslessContent: string | null;
|
|
620
|
+
sidecarsJson: string | null;
|
|
621
|
+
}) {
|
|
622
|
+
if (!args.losslessContent?.trim()) return null;
|
|
623
|
+
const sidecars =
|
|
624
|
+
parseObject<Record<string, string>>(args.sidecarsJson ?? "{}") ?? {};
|
|
625
|
+
const blocks = await builderMdxBodyToBuilderBlocks(
|
|
626
|
+
args.losslessContent,
|
|
627
|
+
sidecars,
|
|
628
|
+
);
|
|
629
|
+
if (blocks.length === 0) return null;
|
|
630
|
+
return builderBlocksToReadableMarkdown(blocks);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function refreshBuilderBodySourceValuesFromStoredLossless(
|
|
634
|
+
entry: BuilderCmsSourceEntry,
|
|
635
|
+
) {
|
|
636
|
+
const losslessContent = stringSourceValue(
|
|
637
|
+
entry.sourceValues,
|
|
638
|
+
BUILDER_CMS_BODY_LOSSLESS_CONTENT_KEY,
|
|
639
|
+
);
|
|
640
|
+
const sidecarsJson = stringSourceValue(
|
|
641
|
+
entry.sourceValues,
|
|
642
|
+
BUILDER_CMS_BODY_SIDECARS_KEY,
|
|
643
|
+
);
|
|
644
|
+
const content = await readableBuilderBodyFromStoredLossless({
|
|
645
|
+
losslessContent,
|
|
646
|
+
sidecarsJson,
|
|
647
|
+
});
|
|
648
|
+
if (!content) return entry;
|
|
649
|
+
return {
|
|
650
|
+
...entry,
|
|
651
|
+
sourceValues: {
|
|
652
|
+
...entry.sourceValues,
|
|
653
|
+
[BUILDER_CMS_BODY_CONTENT_KEY]: content,
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function builderEntryFromSourceRow(args: {
|
|
659
|
+
row: Pick<
|
|
660
|
+
ContentDatabaseSourceRecordRowDb,
|
|
661
|
+
"sourceRowId" | "sourceValuesJson" | "lastSourceUpdatedAt"
|
|
662
|
+
>;
|
|
663
|
+
sourceTable: string;
|
|
664
|
+
fallbackTitle: string;
|
|
665
|
+
}): BuilderCmsSourceEntry | null {
|
|
666
|
+
const sourceValues =
|
|
667
|
+
parseObject<Record<string, DocumentPropertyValue>>(
|
|
668
|
+
args.row.sourceValuesJson,
|
|
669
|
+
) ?? {};
|
|
670
|
+
const id = args.row.sourceRowId;
|
|
671
|
+
if (!id) return null;
|
|
672
|
+
return {
|
|
673
|
+
id,
|
|
674
|
+
model: args.sourceTable,
|
|
675
|
+
title:
|
|
676
|
+
stringSourceValue(sourceValues, "data.title") ??
|
|
677
|
+
stringSourceValue(sourceValues, "title") ??
|
|
678
|
+
args.fallbackTitle,
|
|
679
|
+
urlPath:
|
|
680
|
+
stringSourceValue(sourceValues, "data.url") ??
|
|
681
|
+
stringSourceValue(sourceValues, "url") ??
|
|
682
|
+
"",
|
|
683
|
+
updatedAt:
|
|
684
|
+
args.row.lastSourceUpdatedAt ??
|
|
685
|
+
stringSourceValue(sourceValues, "lastUpdated") ??
|
|
686
|
+
id,
|
|
687
|
+
sourceValues,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export async function enqueueBuilderBodyHydration(args: {
|
|
692
|
+
sourceId: string;
|
|
693
|
+
ownerEmail: string;
|
|
694
|
+
orgId: string | null;
|
|
695
|
+
databaseItemId: string;
|
|
696
|
+
documentId: string;
|
|
697
|
+
sourceTable: string;
|
|
698
|
+
entry: BuilderCmsSourceEntry;
|
|
699
|
+
now: string;
|
|
700
|
+
priority?: number;
|
|
701
|
+
}) {
|
|
702
|
+
const db = getDb();
|
|
703
|
+
const sourceRowId = args.entry.id;
|
|
704
|
+
const priority = args.priority ?? BUILDER_BODY_HYDRATION_BACKGROUND_PRIORITY;
|
|
705
|
+
const [existing] = await db
|
|
706
|
+
.select()
|
|
707
|
+
.from(schema.contentDatabaseBodyHydrationQueue)
|
|
708
|
+
.where(
|
|
709
|
+
eq(
|
|
710
|
+
schema.contentDatabaseBodyHydrationQueue.databaseItemId,
|
|
711
|
+
args.databaseItemId,
|
|
712
|
+
),
|
|
713
|
+
);
|
|
714
|
+
const values = {
|
|
715
|
+
ownerEmail: args.ownerEmail,
|
|
716
|
+
orgId: args.orgId,
|
|
717
|
+
sourceId: args.sourceId,
|
|
718
|
+
databaseItemId: args.databaseItemId,
|
|
719
|
+
documentId: args.documentId,
|
|
720
|
+
sourceRowId,
|
|
721
|
+
sourceTable: args.sourceTable,
|
|
722
|
+
sourceEntryJson: JSON.stringify(args.entry),
|
|
723
|
+
priority: Math.min(existing?.priority ?? priority, priority),
|
|
724
|
+
lastError: null,
|
|
725
|
+
updatedAt: args.now,
|
|
726
|
+
};
|
|
727
|
+
if (existing) {
|
|
728
|
+
await db
|
|
729
|
+
.update(schema.contentDatabaseBodyHydrationQueue)
|
|
730
|
+
.set(values)
|
|
731
|
+
.where(eq(schema.contentDatabaseBodyHydrationQueue.id, existing.id));
|
|
732
|
+
} else {
|
|
733
|
+
await db.insert(schema.contentDatabaseBodyHydrationQueue).values({
|
|
734
|
+
id: crypto.randomUUID(),
|
|
735
|
+
...values,
|
|
736
|
+
attempts: 0,
|
|
737
|
+
lastAttemptedAt: null,
|
|
738
|
+
createdAt: args.now,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
await db
|
|
742
|
+
.update(schema.contentDatabaseItems)
|
|
743
|
+
.set({
|
|
744
|
+
bodyHydrationStatus: "pending",
|
|
745
|
+
bodyHydrationError: null,
|
|
746
|
+
updatedAt: args.now,
|
|
747
|
+
})
|
|
748
|
+
.where(eq(schema.contentDatabaseItems.id, args.databaseItemId));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export async function enqueueBuilderBodyHydrationForItems(args: {
|
|
752
|
+
sourceId: string;
|
|
753
|
+
ownerEmail: string;
|
|
754
|
+
orgId: string | null;
|
|
755
|
+
sourceTable: string;
|
|
756
|
+
items: ContentDatabaseItem[];
|
|
757
|
+
builderEntriesByDocumentId: Map<string, BuilderCmsSourceEntry> | undefined;
|
|
758
|
+
now: string;
|
|
759
|
+
}) {
|
|
760
|
+
if (!args.builderEntriesByDocumentId?.size) return;
|
|
761
|
+
for (const item of args.items) {
|
|
762
|
+
const entry = args.builderEntriesByDocumentId.get(item.document.id);
|
|
763
|
+
if (!entry?.rawEntry) continue;
|
|
764
|
+
if (
|
|
765
|
+
item.bodyHydration?.status === "hydrated" &&
|
|
766
|
+
item.bodyHydration.version === builderBodyHydrationVersion(entry) &&
|
|
767
|
+
!builderBodyIsRawPlaceholderOnly(item.document.content)
|
|
768
|
+
) {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
await enqueueBuilderBodyHydration({
|
|
772
|
+
sourceId: args.sourceId,
|
|
773
|
+
ownerEmail: args.ownerEmail,
|
|
774
|
+
orgId: args.orgId,
|
|
775
|
+
databaseItemId: item.id,
|
|
776
|
+
documentId: item.document.id,
|
|
777
|
+
sourceTable: args.sourceTable,
|
|
778
|
+
entry,
|
|
779
|
+
now: args.now,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
void processBuilderBodyHydrationQueue({
|
|
783
|
+
sourceId: args.sourceId,
|
|
784
|
+
limit: BUILDER_BODY_HYDRATION_BATCH_LIMIT,
|
|
785
|
+
}).catch(() => undefined);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function parseHydrationEntry(
|
|
789
|
+
row: ContentDatabaseBodyHydrationQueueRowDb,
|
|
790
|
+
): BuilderCmsSourceEntry | null {
|
|
791
|
+
const parsed = parseObject<BuilderCmsSourceEntry>(row.sourceEntryJson);
|
|
792
|
+
return parsed?.id ? parsed : null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function processBuilderBodyHydrationJob(
|
|
796
|
+
row: ContentDatabaseBodyHydrationQueueRowDb,
|
|
797
|
+
now: string,
|
|
798
|
+
) {
|
|
799
|
+
const db = getDb();
|
|
800
|
+
const [claimed] = await db
|
|
801
|
+
.update(schema.contentDatabaseBodyHydrationQueue)
|
|
802
|
+
.set({
|
|
803
|
+
attempts: row.attempts + 1,
|
|
804
|
+
lastAttemptedAt: now,
|
|
805
|
+
lastError: null,
|
|
806
|
+
updatedAt: now,
|
|
807
|
+
})
|
|
808
|
+
.where(
|
|
809
|
+
and(
|
|
810
|
+
eq(schema.contentDatabaseBodyHydrationQueue.id, row.id),
|
|
811
|
+
eq(
|
|
812
|
+
schema.contentDatabaseBodyHydrationQueue.sourceEntryJson,
|
|
813
|
+
row.sourceEntryJson,
|
|
814
|
+
),
|
|
815
|
+
),
|
|
816
|
+
)
|
|
817
|
+
.returning({ id: schema.contentDatabaseBodyHydrationQueue.id });
|
|
818
|
+
if (!claimed) return;
|
|
819
|
+
|
|
820
|
+
await db
|
|
821
|
+
.update(schema.contentDatabaseItems)
|
|
822
|
+
.set({
|
|
823
|
+
bodyHydrationStatus: "hydrating",
|
|
824
|
+
bodyHydrationAttemptedAt: now,
|
|
825
|
+
bodyHydrationError: null,
|
|
826
|
+
updatedAt: now,
|
|
827
|
+
})
|
|
828
|
+
.where(eq(schema.contentDatabaseItems.id, row.databaseItemId));
|
|
829
|
+
|
|
830
|
+
const entry = parseHydrationEntry(row);
|
|
831
|
+
if (!entry) throw new Error("Builder body hydration entry is missing.");
|
|
832
|
+
const entryWithBody = await refreshBuilderBodySourceValuesFromStoredLossless(
|
|
833
|
+
await withBuilderBodySourceValues(entry),
|
|
834
|
+
);
|
|
835
|
+
const sourceValues =
|
|
836
|
+
parseObject<Record<string, DocumentPropertyValue>>(
|
|
837
|
+
(
|
|
838
|
+
await db
|
|
839
|
+
.select({
|
|
840
|
+
sourceValuesJson: schema.contentDatabaseSourceRows.sourceValuesJson,
|
|
841
|
+
})
|
|
842
|
+
.from(schema.contentDatabaseSourceRows)
|
|
843
|
+
.where(
|
|
844
|
+
and(
|
|
845
|
+
eq(schema.contentDatabaseSourceRows.sourceId, row.sourceId),
|
|
846
|
+
eq(
|
|
847
|
+
schema.contentDatabaseSourceRows.databaseItemId,
|
|
848
|
+
row.databaseItemId,
|
|
849
|
+
),
|
|
850
|
+
),
|
|
851
|
+
)
|
|
852
|
+
)[0]?.sourceValuesJson ?? "{}",
|
|
853
|
+
) ?? {};
|
|
854
|
+
const nextValues = {
|
|
855
|
+
...sourceValues,
|
|
856
|
+
...entryWithBody.sourceValues,
|
|
857
|
+
};
|
|
858
|
+
const nextContent =
|
|
859
|
+
stringSourceValue(nextValues, BUILDER_CMS_BODY_CONTENT_KEY) ?? "";
|
|
860
|
+
const [document] = await db
|
|
861
|
+
.select({ content: schema.documents.content })
|
|
862
|
+
.from(schema.documents)
|
|
863
|
+
.where(eq(schema.documents.id, row.documentId));
|
|
864
|
+
const previousContent =
|
|
865
|
+
stringSourceValue(sourceValues, BUILDER_CMS_BODY_CONTENT_KEY) ?? "";
|
|
866
|
+
const currentContent = document?.content ?? "";
|
|
867
|
+
const shouldWriteBody =
|
|
868
|
+
currentContent === "" ||
|
|
869
|
+
currentContent === previousContent ||
|
|
870
|
+
currentContent.trim() === "" ||
|
|
871
|
+
builderBodyIsRawPlaceholderOnly(currentContent) ||
|
|
872
|
+
builderBodyHasLegacyPreservedComponentPlaceholders(currentContent) ||
|
|
873
|
+
builderBodyNeedsSourceComponentWrite({
|
|
874
|
+
currentContent,
|
|
875
|
+
nextContent,
|
|
876
|
+
});
|
|
877
|
+
let wroteBody = false;
|
|
878
|
+
await db.transaction(async (tx) => {
|
|
879
|
+
if (shouldWriteBody) {
|
|
880
|
+
const [updatedDocument] = await tx
|
|
881
|
+
.update(schema.documents)
|
|
882
|
+
.set({ content: nextContent, updatedAt: now })
|
|
883
|
+
.where(
|
|
884
|
+
and(
|
|
885
|
+
eq(schema.documents.id, row.documentId),
|
|
886
|
+
eq(schema.documents.content, currentContent),
|
|
887
|
+
),
|
|
888
|
+
)
|
|
889
|
+
.returning({ id: schema.documents.id });
|
|
890
|
+
wroteBody = Boolean(updatedDocument);
|
|
891
|
+
}
|
|
892
|
+
if (shouldWriteBody && !wroteBody) {
|
|
893
|
+
await tx
|
|
894
|
+
.update(schema.contentDatabaseItems)
|
|
895
|
+
.set({
|
|
896
|
+
bodyHydrationStatus: "pending",
|
|
897
|
+
bodyHydrationAttemptedAt: now,
|
|
898
|
+
bodyHydrationError:
|
|
899
|
+
"Skipped Builder body hydration because the document changed during sync.",
|
|
900
|
+
updatedAt: now,
|
|
901
|
+
})
|
|
902
|
+
.where(eq(schema.contentDatabaseItems.id, row.databaseItemId));
|
|
903
|
+
await tx
|
|
904
|
+
.update(schema.contentDatabaseBodyHydrationQueue)
|
|
905
|
+
.set({
|
|
906
|
+
lastError:
|
|
907
|
+
"Skipped Builder body hydration because the document changed during sync.",
|
|
908
|
+
updatedAt: now,
|
|
909
|
+
})
|
|
910
|
+
.where(
|
|
911
|
+
and(
|
|
912
|
+
eq(schema.contentDatabaseBodyHydrationQueue.id, row.id),
|
|
913
|
+
eq(
|
|
914
|
+
schema.contentDatabaseBodyHydrationQueue.sourceEntryJson,
|
|
915
|
+
row.sourceEntryJson,
|
|
916
|
+
),
|
|
917
|
+
),
|
|
918
|
+
);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const [deleted] = await tx
|
|
922
|
+
.delete(schema.contentDatabaseBodyHydrationQueue)
|
|
923
|
+
.where(
|
|
924
|
+
and(
|
|
925
|
+
eq(schema.contentDatabaseBodyHydrationQueue.id, row.id),
|
|
926
|
+
eq(
|
|
927
|
+
schema.contentDatabaseBodyHydrationQueue.sourceEntryJson,
|
|
928
|
+
row.sourceEntryJson,
|
|
929
|
+
),
|
|
930
|
+
),
|
|
931
|
+
)
|
|
932
|
+
.returning({ id: schema.contentDatabaseBodyHydrationQueue.id });
|
|
933
|
+
if (!deleted) {
|
|
934
|
+
if (wroteBody) {
|
|
935
|
+
await tx
|
|
936
|
+
.update(schema.contentDatabaseSourceRows)
|
|
937
|
+
.set({
|
|
938
|
+
sourceValuesJson: JSON.stringify(nextValues),
|
|
939
|
+
lastSyncedAt: now,
|
|
940
|
+
lastSourceUpdatedAt: entry.updatedAt ?? now,
|
|
941
|
+
updatedAt: now,
|
|
942
|
+
})
|
|
943
|
+
.where(
|
|
944
|
+
and(
|
|
945
|
+
eq(schema.contentDatabaseSourceRows.sourceId, row.sourceId),
|
|
946
|
+
eq(
|
|
947
|
+
schema.contentDatabaseSourceRows.databaseItemId,
|
|
948
|
+
row.databaseItemId,
|
|
949
|
+
),
|
|
950
|
+
),
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
await tx
|
|
954
|
+
.update(schema.contentDatabaseItems)
|
|
955
|
+
.set({
|
|
956
|
+
bodyHydrationStatus: "pending",
|
|
957
|
+
bodyHydrationAttemptedAt: now,
|
|
958
|
+
bodyHydrationError: null,
|
|
959
|
+
updatedAt: now,
|
|
960
|
+
})
|
|
961
|
+
.where(eq(schema.contentDatabaseItems.id, row.databaseItemId));
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
await tx
|
|
965
|
+
.update(schema.contentDatabaseSourceRows)
|
|
966
|
+
.set({
|
|
967
|
+
sourceValuesJson: JSON.stringify(nextValues),
|
|
968
|
+
lastSyncedAt: now,
|
|
969
|
+
lastSourceUpdatedAt: entry.updatedAt ?? now,
|
|
970
|
+
updatedAt: now,
|
|
971
|
+
})
|
|
972
|
+
.where(
|
|
973
|
+
and(
|
|
974
|
+
eq(schema.contentDatabaseSourceRows.sourceId, row.sourceId),
|
|
975
|
+
eq(
|
|
976
|
+
schema.contentDatabaseSourceRows.databaseItemId,
|
|
977
|
+
row.databaseItemId,
|
|
978
|
+
),
|
|
979
|
+
),
|
|
980
|
+
);
|
|
981
|
+
await tx
|
|
982
|
+
.update(schema.contentDatabaseItems)
|
|
983
|
+
.set({
|
|
984
|
+
bodyHydrationStatus: "hydrated",
|
|
985
|
+
bodyHydrationAttemptedAt: now,
|
|
986
|
+
bodyHydrationError: null,
|
|
987
|
+
bodyHydrationVersion: builderBodyHydrationVersion(entryWithBody),
|
|
988
|
+
updatedAt: now,
|
|
989
|
+
})
|
|
990
|
+
.where(eq(schema.contentDatabaseItems.id, row.databaseItemId));
|
|
991
|
+
});
|
|
992
|
+
if (wroteBody) {
|
|
993
|
+
try {
|
|
994
|
+
await deleteCollabState(row.documentId);
|
|
995
|
+
releaseDoc(row.documentId);
|
|
996
|
+
} catch {
|
|
997
|
+
// Non-fatal: the updated document row still lets open editors reconcile.
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async function enqueueStaleBuilderBodyHydrationForOpenDocument(args: {
|
|
1003
|
+
sourceId: string;
|
|
1004
|
+
documentId: string;
|
|
1005
|
+
now: string;
|
|
1006
|
+
}) {
|
|
1007
|
+
const db = getDb();
|
|
1008
|
+
const [row] = await db
|
|
1009
|
+
.select({
|
|
1010
|
+
source: schema.contentDatabaseSources,
|
|
1011
|
+
item: schema.contentDatabaseItems,
|
|
1012
|
+
sourceRow: schema.contentDatabaseSourceRows,
|
|
1013
|
+
document: schema.documents,
|
|
1014
|
+
})
|
|
1015
|
+
.from(schema.contentDatabaseSourceRows)
|
|
1016
|
+
.innerJoin(
|
|
1017
|
+
schema.contentDatabaseSources,
|
|
1018
|
+
eq(
|
|
1019
|
+
schema.contentDatabaseSources.id,
|
|
1020
|
+
schema.contentDatabaseSourceRows.sourceId,
|
|
1021
|
+
),
|
|
1022
|
+
)
|
|
1023
|
+
.innerJoin(
|
|
1024
|
+
schema.contentDatabaseItems,
|
|
1025
|
+
eq(
|
|
1026
|
+
schema.contentDatabaseItems.id,
|
|
1027
|
+
schema.contentDatabaseSourceRows.databaseItemId,
|
|
1028
|
+
),
|
|
1029
|
+
)
|
|
1030
|
+
.innerJoin(
|
|
1031
|
+
schema.documents,
|
|
1032
|
+
eq(schema.documents.id, schema.contentDatabaseSourceRows.documentId),
|
|
1033
|
+
)
|
|
1034
|
+
.where(
|
|
1035
|
+
and(
|
|
1036
|
+
eq(schema.contentDatabaseSourceRows.sourceId, args.sourceId),
|
|
1037
|
+
eq(schema.contentDatabaseSourceRows.documentId, args.documentId),
|
|
1038
|
+
),
|
|
1039
|
+
);
|
|
1040
|
+
if (!row || row.source.sourceType !== "builder-cms") return;
|
|
1041
|
+
const entry = builderEntryFromSourceRow({
|
|
1042
|
+
row: row.sourceRow,
|
|
1043
|
+
sourceTable: row.source.sourceTable,
|
|
1044
|
+
fallbackTitle: row.document.title,
|
|
1045
|
+
});
|
|
1046
|
+
if (!entry) return;
|
|
1047
|
+
const refreshedEntry =
|
|
1048
|
+
await refreshBuilderBodySourceValuesFromStoredLossless(entry);
|
|
1049
|
+
if (
|
|
1050
|
+
!builderStoredBodyIsStale({ item: row.item, entry: refreshedEntry }) &&
|
|
1051
|
+
!builderBodyHasLegacyPreservedComponentPlaceholders(row.document.content) &&
|
|
1052
|
+
!builderBodyNeedsSourceComponentWrite({
|
|
1053
|
+
currentContent: row.document.content,
|
|
1054
|
+
nextContent: stringSourceValue(
|
|
1055
|
+
refreshedEntry.sourceValues,
|
|
1056
|
+
BUILDER_CMS_BODY_CONTENT_KEY,
|
|
1057
|
+
),
|
|
1058
|
+
})
|
|
1059
|
+
) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
await enqueueBuilderBodyHydration({
|
|
1063
|
+
sourceId: args.sourceId,
|
|
1064
|
+
ownerEmail: row.item.ownerEmail,
|
|
1065
|
+
orgId: row.item.orgId,
|
|
1066
|
+
databaseItemId: row.item.id,
|
|
1067
|
+
documentId: row.item.documentId,
|
|
1068
|
+
sourceTable: row.source.sourceTable,
|
|
1069
|
+
entry: refreshedEntry,
|
|
1070
|
+
now: args.now,
|
|
1071
|
+
priority: BUILDER_BODY_HYDRATION_OPEN_PRIORITY,
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
export async function processBuilderBodyHydrationQueue(args: {
|
|
1076
|
+
sourceId: string;
|
|
1077
|
+
documentId?: string | null;
|
|
1078
|
+
limit?: number | null;
|
|
1079
|
+
}) {
|
|
1080
|
+
const db = getDb();
|
|
1081
|
+
const limit = normalizeHydrationLimit(args.limit);
|
|
1082
|
+
const now = new Date().toISOString();
|
|
1083
|
+
if (args.documentId) {
|
|
1084
|
+
await enqueueStaleBuilderBodyHydrationForOpenDocument({
|
|
1085
|
+
sourceId: args.sourceId,
|
|
1086
|
+
documentId: args.documentId,
|
|
1087
|
+
now,
|
|
1088
|
+
});
|
|
1089
|
+
await db
|
|
1090
|
+
.update(schema.contentDatabaseBodyHydrationQueue)
|
|
1091
|
+
.set({
|
|
1092
|
+
priority: BUILDER_BODY_HYDRATION_OPEN_PRIORITY,
|
|
1093
|
+
updatedAt: now,
|
|
1094
|
+
})
|
|
1095
|
+
.where(
|
|
1096
|
+
and(
|
|
1097
|
+
eq(schema.contentDatabaseBodyHydrationQueue.sourceId, args.sourceId),
|
|
1098
|
+
eq(
|
|
1099
|
+
schema.contentDatabaseBodyHydrationQueue.documentId,
|
|
1100
|
+
args.documentId,
|
|
1101
|
+
),
|
|
1102
|
+
),
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
const jobs = await db
|
|
1106
|
+
.select()
|
|
1107
|
+
.from(schema.contentDatabaseBodyHydrationQueue)
|
|
1108
|
+
.where(
|
|
1109
|
+
args.documentId
|
|
1110
|
+
? and(
|
|
1111
|
+
eq(
|
|
1112
|
+
schema.contentDatabaseBodyHydrationQueue.sourceId,
|
|
1113
|
+
args.sourceId,
|
|
1114
|
+
),
|
|
1115
|
+
eq(
|
|
1116
|
+
schema.contentDatabaseBodyHydrationQueue.documentId,
|
|
1117
|
+
args.documentId,
|
|
1118
|
+
),
|
|
1119
|
+
)
|
|
1120
|
+
: eq(schema.contentDatabaseBodyHydrationQueue.sourceId, args.sourceId),
|
|
1121
|
+
)
|
|
1122
|
+
.orderBy(
|
|
1123
|
+
asc(schema.contentDatabaseBodyHydrationQueue.priority),
|
|
1124
|
+
asc(schema.contentDatabaseBodyHydrationQueue.createdAt),
|
|
1125
|
+
)
|
|
1126
|
+
.limit(limit);
|
|
1127
|
+
|
|
1128
|
+
let succeeded = 0;
|
|
1129
|
+
let failed = 0;
|
|
1130
|
+
for (const job of jobs) {
|
|
1131
|
+
const attemptNow = new Date().toISOString();
|
|
1132
|
+
try {
|
|
1133
|
+
await processBuilderBodyHydrationJob(job, attemptNow);
|
|
1134
|
+
succeeded += 1;
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
failed += 1;
|
|
1137
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1138
|
+
await db
|
|
1139
|
+
.update(schema.contentDatabaseItems)
|
|
1140
|
+
.set({
|
|
1141
|
+
bodyHydrationStatus: "error",
|
|
1142
|
+
bodyHydrationAttemptedAt: attemptNow,
|
|
1143
|
+
bodyHydrationError: message,
|
|
1144
|
+
updatedAt: attemptNow,
|
|
1145
|
+
})
|
|
1146
|
+
.where(eq(schema.contentDatabaseItems.id, job.databaseItemId));
|
|
1147
|
+
await db
|
|
1148
|
+
.update(schema.contentDatabaseBodyHydrationQueue)
|
|
1149
|
+
.set({
|
|
1150
|
+
attempts: job.attempts + 1,
|
|
1151
|
+
lastAttemptedAt: attemptNow,
|
|
1152
|
+
lastError: message,
|
|
1153
|
+
priority: job.priority + 10,
|
|
1154
|
+
updatedAt: attemptNow,
|
|
1155
|
+
})
|
|
1156
|
+
.where(eq(schema.contentDatabaseBodyHydrationQueue.id, job.id));
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const [remaining] = await db
|
|
1160
|
+
.select({ count: sql<number>`COUNT(*)` })
|
|
1161
|
+
.from(schema.contentDatabaseBodyHydrationQueue)
|
|
1162
|
+
.where(
|
|
1163
|
+
eq(schema.contentDatabaseBodyHydrationQueue.sourceId, args.sourceId),
|
|
1164
|
+
);
|
|
1165
|
+
return {
|
|
1166
|
+
sourceId: args.sourceId,
|
|
1167
|
+
processed: jobs.length,
|
|
1168
|
+
succeeded,
|
|
1169
|
+
failed,
|
|
1170
|
+
remaining: Number(remaining?.count ?? 0),
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
export async function withBuilderBodySourceValues(
|
|
1175
|
+
entry: BuilderCmsSourceEntry,
|
|
1176
|
+
): Promise<BuilderCmsSourceEntry> {
|
|
1177
|
+
const snapshot = await builderBodySnapshotForEntry(entry);
|
|
1178
|
+
if (!snapshot) return entry;
|
|
1179
|
+
return {
|
|
1180
|
+
...entry,
|
|
1181
|
+
sourceValues: {
|
|
1182
|
+
...entry.sourceValues,
|
|
1183
|
+
[BUILDER_CMS_BODY_CONTENT_KEY]: snapshot.content,
|
|
1184
|
+
[BUILDER_CMS_BODY_LOSSLESS_CONTENT_KEY]: snapshot.losslessContent,
|
|
1185
|
+
[BUILDER_CMS_BODY_READABLE_MAP_KEY]: snapshot.readableMapJson,
|
|
1186
|
+
[BUILDER_CMS_BODY_BLOCKS_HASH_KEY]: snapshot.blocksHash,
|
|
1187
|
+
[BUILDER_CMS_BODY_SIDECARS_KEY]: snapshot.sidecarsJson,
|
|
1188
|
+
},
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
export async function withBuilderBodiesSourceValues(
|
|
1193
|
+
entries: BuilderCmsSourceEntry[],
|
|
1194
|
+
) {
|
|
1195
|
+
return Promise.all(
|
|
1196
|
+
entries.map((entry) => withBuilderBodySourceValues(entry)),
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
async function builderBodyChangeForLocalContent(args: {
|
|
1201
|
+
row: Pick<ContentDatabaseSourceRecordRowDb, "sourceValuesJson">;
|
|
1202
|
+
localContent: string | null | undefined;
|
|
1203
|
+
}): Promise<ContentDatabaseSourceBodyChange | null> {
|
|
1204
|
+
const sourceValues =
|
|
1205
|
+
parseObject<Record<string, DocumentPropertyValue>>(
|
|
1206
|
+
args.row.sourceValuesJson,
|
|
1207
|
+
) ?? {};
|
|
1208
|
+
const currentHash = stringSourceValue(
|
|
1209
|
+
sourceValues,
|
|
1210
|
+
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
1211
|
+
);
|
|
1212
|
+
const currentContent = stringSourceValue(
|
|
1213
|
+
sourceValues,
|
|
1214
|
+
BUILDER_CMS_BODY_CONTENT_KEY,
|
|
1215
|
+
);
|
|
1216
|
+
const losslessContent = stringSourceValue(
|
|
1217
|
+
sourceValues,
|
|
1218
|
+
BUILDER_CMS_BODY_LOSSLESS_CONTENT_KEY,
|
|
1219
|
+
);
|
|
1220
|
+
const sidecarsJson =
|
|
1221
|
+
stringSourceValue(sourceValues, BUILDER_CMS_BODY_SIDECARS_KEY) ?? "{}";
|
|
1222
|
+
const localContent = args.localContent ?? "";
|
|
1223
|
+
if (!currentHash && !currentContent && !localContent.trim()) return null;
|
|
1224
|
+
|
|
1225
|
+
let sidecars: Record<string, string> = {};
|
|
1226
|
+
try {
|
|
1227
|
+
const parsed = JSON.parse(sidecarsJson) as unknown;
|
|
1228
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1229
|
+
sidecars = Object.fromEntries(
|
|
1230
|
+
Object.entries(parsed as Record<string, unknown>).filter(
|
|
1231
|
+
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
1232
|
+
),
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
} catch {
|
|
1236
|
+
sidecars = {};
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
try {
|
|
1240
|
+
const proposed =
|
|
1241
|
+
losslessContent && !localContent.includes("<Builder")
|
|
1242
|
+
? await builderReadableBodyToBuilderBlocks({
|
|
1243
|
+
localContent,
|
|
1244
|
+
losslessContent,
|
|
1245
|
+
sidecars,
|
|
1246
|
+
})
|
|
1247
|
+
: {
|
|
1248
|
+
blocks: await builderMdxBodyToBuilderBlocks(localContent, sidecars),
|
|
1249
|
+
warnings: [] as string[],
|
|
1250
|
+
};
|
|
1251
|
+
if (!proposed.blocks) {
|
|
1252
|
+
return {
|
|
1253
|
+
summary: "Builder body blocks changed, but need attention before push.",
|
|
1254
|
+
currentExcerpt: bodyExcerpt(currentContent),
|
|
1255
|
+
proposedExcerpt: bodyExcerpt(localContent),
|
|
1256
|
+
currentHash,
|
|
1257
|
+
proposedHash: null,
|
|
1258
|
+
proposedContent: localContent,
|
|
1259
|
+
proposedBlocksJson: null,
|
|
1260
|
+
sidecarsJson,
|
|
1261
|
+
warnings: proposed.warnings,
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
const proposedBlocks = proposed.blocks;
|
|
1265
|
+
const proposedHash = builderBlocksHash(proposedBlocks);
|
|
1266
|
+
if (currentHash && proposedHash === currentHash) return null;
|
|
1267
|
+
return {
|
|
1268
|
+
summary: "Builder body blocks changed.",
|
|
1269
|
+
currentExcerpt: bodyExcerpt(currentContent),
|
|
1270
|
+
proposedExcerpt: bodyExcerpt(localContent),
|
|
1271
|
+
currentHash,
|
|
1272
|
+
proposedHash,
|
|
1273
|
+
proposedContent: localContent,
|
|
1274
|
+
proposedBlocksJson: JSON.stringify(proposedBlocks),
|
|
1275
|
+
sidecarsJson,
|
|
1276
|
+
warnings: proposed.warnings,
|
|
1277
|
+
};
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
const message =
|
|
1280
|
+
error instanceof Error
|
|
1281
|
+
? error.message
|
|
1282
|
+
: "Builder body could not be converted.";
|
|
1283
|
+
return {
|
|
1284
|
+
summary: "Builder body blocks changed, but need attention before push.",
|
|
1285
|
+
currentExcerpt: bodyExcerpt(currentContent),
|
|
1286
|
+
proposedExcerpt: bodyExcerpt(localContent),
|
|
1287
|
+
currentHash,
|
|
1288
|
+
proposedHash: null,
|
|
1289
|
+
proposedContent: localContent,
|
|
1290
|
+
proposedBlocksJson: null,
|
|
1291
|
+
sidecarsJson,
|
|
1292
|
+
warnings: [message],
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
449
1297
|
export function buildBuilderLocalOutboundChangeSets(args: {
|
|
450
1298
|
source: ContentDatabaseSourceRowDb;
|
|
451
1299
|
rowRows: ContentDatabaseSourceRecordRowDb[];
|
|
@@ -473,6 +1321,7 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
473
1321
|
// collection is adopted as a create_draft by THAT collection only; an
|
|
474
1322
|
// untagged / "Local" row falls back to the primary (allowUnsourcedCreates).
|
|
475
1323
|
taggedSourceByDocumentId?: Map<string, string>;
|
|
1324
|
+
bodyChangeByDocumentId?: Map<string, ContentDatabaseSourceBodyChange>;
|
|
476
1325
|
}): ContentDatabaseSourceChangeSet[] {
|
|
477
1326
|
if (normalizeSourceType(args.source.sourceType) !== "builder-cms") return [];
|
|
478
1327
|
|
|
@@ -528,7 +1377,8 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
528
1377
|
});
|
|
529
1378
|
}
|
|
530
1379
|
}
|
|
531
|
-
|
|
1380
|
+
const bodyChange = args.bodyChangeByDocumentId?.get(row.documentId) ?? null;
|
|
1381
|
+
if (fieldChanges.length === 0 && !bodyChange) continue;
|
|
532
1382
|
// Skip if this row already has a live (non-rejected/applied) stored outbound
|
|
533
1383
|
// autosave change-set — the stored one is what's being reviewed/pushed.
|
|
534
1384
|
const matchesStoredChange = args.storedChangeSets.some((changeSet) => {
|
|
@@ -541,35 +1391,45 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
541
1391
|
) {
|
|
542
1392
|
return false;
|
|
543
1393
|
}
|
|
544
|
-
return
|
|
545
|
-
fieldChanges.some(
|
|
546
|
-
(
|
|
547
|
-
change
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1394
|
+
return (
|
|
1395
|
+
changeSet.fieldChanges.some((stored) =>
|
|
1396
|
+
fieldChanges.some(
|
|
1397
|
+
(change) =>
|
|
1398
|
+
change.localFieldKey === stored.localFieldKey &&
|
|
1399
|
+
sameSourceFieldValue(change.currentValue, stored.currentValue) &&
|
|
1400
|
+
sameSourceFieldValue(change.proposedValue, stored.proposedValue),
|
|
1401
|
+
),
|
|
1402
|
+
) ||
|
|
1403
|
+
(!!bodyChange && !!changeSet.bodyChange)
|
|
551
1404
|
);
|
|
552
1405
|
});
|
|
553
1406
|
if (matchesStoredChange) continue;
|
|
554
1407
|
|
|
555
1408
|
const now = new Date().toISOString();
|
|
1409
|
+
const displayTitle = localTitle || sourceTitle;
|
|
556
1410
|
pending.push({
|
|
557
|
-
id: `local-pending-${row.id}
|
|
1411
|
+
id: `local-pending-${row.id}-${bodyChange ? "body" : "fields"}`,
|
|
558
1412
|
databaseItemId: row.databaseItemId,
|
|
559
1413
|
documentId: row.documentId,
|
|
560
|
-
kind:
|
|
1414
|
+
kind:
|
|
1415
|
+
bodyChange && fieldChanges.length === 0
|
|
1416
|
+
? "body_update"
|
|
1417
|
+
: "field_update",
|
|
561
1418
|
direction: "outbound",
|
|
562
1419
|
state: "pending_push",
|
|
563
1420
|
pushMode: "autosave",
|
|
564
1421
|
localOnly: true,
|
|
565
1422
|
summary:
|
|
566
|
-
fieldChanges.length ===
|
|
567
|
-
? `Pending local Builder CMS
|
|
568
|
-
:
|
|
1423
|
+
bodyChange && fieldChanges.length === 0
|
|
1424
|
+
? `Pending local Builder CMS body change for "${displayTitle}".`
|
|
1425
|
+
: fieldChanges.length === 1 &&
|
|
1426
|
+
fieldChanges[0]?.localFieldKey === "title"
|
|
1427
|
+
? `Pending local Builder CMS title change for "${localTitle}".`
|
|
1428
|
+
: `Pending local Builder CMS changes for "${displayTitle}".`,
|
|
569
1429
|
fieldChanges,
|
|
570
|
-
bodyChange
|
|
1430
|
+
bodyChange,
|
|
571
1431
|
riskLevel: "low",
|
|
572
|
-
riskReasons: ["single field diff"],
|
|
1432
|
+
riskReasons: bodyChange ? ["body diff"] : ["single field diff"],
|
|
573
1433
|
conflictState: "none",
|
|
574
1434
|
reviewEvents: [],
|
|
575
1435
|
executions: [],
|
|
@@ -617,6 +1477,8 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
617
1477
|
const title = args.documentTitleById.get(item.documentId)?.trim() ?? "";
|
|
618
1478
|
if (!title) continue;
|
|
619
1479
|
const localValues = args.localValuesByDocument?.get(item.documentId);
|
|
1480
|
+
const bodyChange =
|
|
1481
|
+
args.bodyChangeByDocumentId?.get(item.documentId) ?? null;
|
|
620
1482
|
const fieldChanges: ContentDatabaseSourceFieldChange[] = [
|
|
621
1483
|
{
|
|
622
1484
|
propertyId: null,
|
|
@@ -651,9 +1513,11 @@ export function buildBuilderLocalOutboundChangeSets(args: {
|
|
|
651
1513
|
localOnly: true,
|
|
652
1514
|
summary: `Pending new Builder entry "${title}".`,
|
|
653
1515
|
fieldChanges,
|
|
654
|
-
bodyChange
|
|
1516
|
+
bodyChange,
|
|
655
1517
|
riskLevel: "low",
|
|
656
|
-
riskReasons:
|
|
1518
|
+
riskReasons: bodyChange
|
|
1519
|
+
? ["new Builder entry (create as draft)", "body diff"]
|
|
1520
|
+
: ["new Builder entry (create as draft)"],
|
|
657
1521
|
conflictState: "none",
|
|
658
1522
|
reviewEvents: [],
|
|
659
1523
|
executions: [],
|
|
@@ -867,6 +1731,7 @@ async function loadSourceSnapshot(
|
|
|
867
1731
|
.select({
|
|
868
1732
|
id: schema.contentDatabaseItems.id,
|
|
869
1733
|
documentId: schema.contentDatabaseItems.documentId,
|
|
1734
|
+
bodyHydrationStatus: schema.contentDatabaseItems.bodyHydrationStatus,
|
|
870
1735
|
})
|
|
871
1736
|
.from(schema.contentDatabaseItems)
|
|
872
1737
|
.where(
|
|
@@ -888,6 +1753,7 @@ async function loadSourceSnapshot(
|
|
|
888
1753
|
.select({
|
|
889
1754
|
id: schema.documents.id,
|
|
890
1755
|
title: schema.documents.title,
|
|
1756
|
+
content: schema.documents.content,
|
|
891
1757
|
})
|
|
892
1758
|
.from(schema.documents)
|
|
893
1759
|
.where(
|
|
@@ -900,6 +1766,9 @@ async function loadSourceSnapshot(
|
|
|
900
1766
|
const documentTitleById = new Map(
|
|
901
1767
|
rowDocuments.map((document) => [document.id, document.title]),
|
|
902
1768
|
);
|
|
1769
|
+
const documentContentById = new Map(
|
|
1770
|
+
rowDocuments.map((document) => [document.id, document.content]),
|
|
1771
|
+
);
|
|
903
1772
|
const propertyValueRows =
|
|
904
1773
|
isBuilderSource && allDocumentIds.length > 0
|
|
905
1774
|
? await db
|
|
@@ -1003,6 +1872,34 @@ async function loadSourceSnapshot(
|
|
|
1003
1872
|
}
|
|
1004
1873
|
}
|
|
1005
1874
|
}
|
|
1875
|
+
const bodyChangeByDocumentId = new Map<
|
|
1876
|
+
string,
|
|
1877
|
+
ContentDatabaseSourceBodyChange
|
|
1878
|
+
>();
|
|
1879
|
+
if (isBuilderSource) {
|
|
1880
|
+
const sourceRowByDocumentId = new Map(
|
|
1881
|
+
rowRows.map((row) => [row.documentId, row]),
|
|
1882
|
+
);
|
|
1883
|
+
const hydratedDocumentIds = new Set(
|
|
1884
|
+
databaseItemRows
|
|
1885
|
+
.filter((item) => item.bodyHydrationStatus === "hydrated")
|
|
1886
|
+
.map((item) => item.documentId),
|
|
1887
|
+
);
|
|
1888
|
+
await Promise.all(
|
|
1889
|
+
allDocumentIds.map(async (documentId) => {
|
|
1890
|
+
if (!hydratedDocumentIds.has(documentId)) return;
|
|
1891
|
+
const row = sourceRowByDocumentId.get(documentId) ?? {
|
|
1892
|
+
sourceValuesJson: "{}",
|
|
1893
|
+
};
|
|
1894
|
+
const bodyChange = await builderBodyChangeForLocalContent({
|
|
1895
|
+
row,
|
|
1896
|
+
localContent: documentContentById.get(documentId),
|
|
1897
|
+
});
|
|
1898
|
+
if (bodyChange) bodyChangeByDocumentId.set(documentId, bodyChange);
|
|
1899
|
+
}),
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1006
1903
|
const localOutboundChangeSets = buildBuilderLocalOutboundChangeSets({
|
|
1007
1904
|
source,
|
|
1008
1905
|
rowRows,
|
|
@@ -1017,6 +1914,7 @@ async function loadSourceSnapshot(
|
|
|
1017
1914
|
otherSourceDocumentIds,
|
|
1018
1915
|
allowUnsourcedCreates: isPrimarySource,
|
|
1019
1916
|
taggedSourceByDocumentId,
|
|
1917
|
+
bodyChangeByDocumentId,
|
|
1020
1918
|
});
|
|
1021
1919
|
const rowByDocumentId = new Map(rowRows.map((row) => [row.documentId, row]));
|
|
1022
1920
|
const changeSets = [...storedChangeSets, ...localOutboundChangeSets].map(
|
|
@@ -1052,6 +1950,13 @@ async function loadSourceSnapshot(
|
|
|
1052
1950
|
if (target?.title) displaySourceName = target.title;
|
|
1053
1951
|
}
|
|
1054
1952
|
|
|
1953
|
+
const bodyHydration = isBuilderSource
|
|
1954
|
+
? await sourceBodyHydrationSummary({
|
|
1955
|
+
sourceId: source.id,
|
|
1956
|
+
databaseId: database.id,
|
|
1957
|
+
})
|
|
1958
|
+
: undefined;
|
|
1959
|
+
|
|
1055
1960
|
return {
|
|
1056
1961
|
id: source.id,
|
|
1057
1962
|
databaseId: source.databaseId,
|
|
@@ -1097,7 +2002,44 @@ async function loadSourceSnapshot(
|
|
|
1097
2002
|
fields,
|
|
1098
2003
|
rows,
|
|
1099
2004
|
changeSets,
|
|
2005
|
+
bodyHydration,
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
async function sourceBodyHydrationSummary(args: {
|
|
2010
|
+
sourceId: string;
|
|
2011
|
+
databaseId: string;
|
|
2012
|
+
}): Promise<ContentDatabaseBodyHydrationSummary> {
|
|
2013
|
+
const rows = await getDb()
|
|
2014
|
+
.select({ status: schema.contentDatabaseItems.bodyHydrationStatus })
|
|
2015
|
+
.from(schema.contentDatabaseItems)
|
|
2016
|
+
.innerJoin(
|
|
2017
|
+
schema.contentDatabaseSourceRows,
|
|
2018
|
+
eq(
|
|
2019
|
+
schema.contentDatabaseSourceRows.databaseItemId,
|
|
2020
|
+
schema.contentDatabaseItems.id,
|
|
2021
|
+
),
|
|
2022
|
+
)
|
|
2023
|
+
.where(
|
|
2024
|
+
and(
|
|
2025
|
+
eq(schema.contentDatabaseItems.databaseId, args.databaseId),
|
|
2026
|
+
eq(schema.contentDatabaseSourceRows.sourceId, args.sourceId),
|
|
2027
|
+
),
|
|
2028
|
+
);
|
|
2029
|
+
const summary: ContentDatabaseBodyHydrationSummary = {
|
|
2030
|
+
pending: 0,
|
|
2031
|
+
hydrating: 0,
|
|
2032
|
+
hydrated: 0,
|
|
2033
|
+
error: 0,
|
|
2034
|
+
total: rows.length,
|
|
1100
2035
|
};
|
|
2036
|
+
for (const row of rows) {
|
|
2037
|
+
if (row.status === "pending") summary.pending += 1;
|
|
2038
|
+
else if (row.status === "hydrating") summary.hydrating += 1;
|
|
2039
|
+
else if (row.status === "error") summary.error += 1;
|
|
2040
|
+
else summary.hydrated += 1;
|
|
2041
|
+
}
|
|
2042
|
+
return summary;
|
|
1101
2043
|
}
|
|
1102
2044
|
|
|
1103
2045
|
// Pass a stored federation block through only when it has the shape the join
|
|
@@ -1714,6 +2656,11 @@ export async function resyncMockSourceSnapshot(args: {
|
|
|
1714
2656
|
await db
|
|
1715
2657
|
.delete(schema.contentDatabaseSourceFields)
|
|
1716
2658
|
.where(eq(schema.contentDatabaseSourceFields.sourceId, args.source.id));
|
|
2659
|
+
await db
|
|
2660
|
+
.delete(schema.contentDatabaseBodyHydrationQueue)
|
|
2661
|
+
.where(
|
|
2662
|
+
eq(schema.contentDatabaseBodyHydrationQueue.sourceId, args.source.id),
|
|
2663
|
+
);
|
|
1717
2664
|
await db
|
|
1718
2665
|
.delete(schema.contentDatabaseSourceRows)
|
|
1719
2666
|
.where(eq(schema.contentDatabaseSourceRows.sourceId, args.source.id));
|
|
@@ -1987,6 +2934,8 @@ export async function importBuilderCmsEntriesAsDatabaseItems(args: {
|
|
|
1987
2934
|
databaseId: args.database.id,
|
|
1988
2935
|
documentId,
|
|
1989
2936
|
position: nextItemPosition++,
|
|
2937
|
+
bodyHydrationStatus: entry.rawEntry ? "pending" : "hydrated",
|
|
2938
|
+
bodyHydrationError: null,
|
|
1990
2939
|
createdAt: args.now,
|
|
1991
2940
|
updatedAt: args.now,
|
|
1992
2941
|
});
|
|
@@ -2006,6 +2955,8 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
2006
2955
|
const builderRead = await readBuilderCmsContentEntries({
|
|
2007
2956
|
model: args.source.sourceTable,
|
|
2008
2957
|
});
|
|
2958
|
+
const builderEntries =
|
|
2959
|
+
builderRead.state === "live" ? builderRead.entries : [];
|
|
2009
2960
|
let existingRows = await db
|
|
2010
2961
|
.select()
|
|
2011
2962
|
.from(schema.contentDatabaseSourceRows)
|
|
@@ -2013,7 +2964,7 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
2013
2964
|
if (builderRead.state === "live") {
|
|
2014
2965
|
const imported = await importBuilderCmsEntriesAsDatabaseItems({
|
|
2015
2966
|
database: args.database,
|
|
2016
|
-
entries:
|
|
2967
|
+
entries: builderEntries,
|
|
2017
2968
|
now: args.now,
|
|
2018
2969
|
sourceTable: args.source.sourceTable,
|
|
2019
2970
|
existingSourceRows: existingRows,
|
|
@@ -2032,7 +2983,7 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
2032
2983
|
const builderEntriesByDocumentId =
|
|
2033
2984
|
builderRead.state === "live"
|
|
2034
2985
|
? mapBuilderCmsEntriesToLocalItems({
|
|
2035
|
-
entries:
|
|
2986
|
+
entries: builderEntries,
|
|
2036
2987
|
items: response.items,
|
|
2037
2988
|
sourceTable: args.source.sourceTable,
|
|
2038
2989
|
now: args.now,
|
|
@@ -2052,7 +3003,6 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
2052
3003
|
},
|
|
2053
3004
|
]),
|
|
2054
3005
|
);
|
|
2055
|
-
|
|
2056
3006
|
await db
|
|
2057
3007
|
.delete(schema.contentDatabaseSourceFields)
|
|
2058
3008
|
.where(eq(schema.contentDatabaseSourceFields.sourceId, args.source.id));
|
|
@@ -2066,8 +3016,7 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
2066
3016
|
sourceType: "builder-cms",
|
|
2067
3017
|
properties,
|
|
2068
3018
|
builderModelFields,
|
|
2069
|
-
builderSampleEntries:
|
|
2070
|
-
builderRead.state === "live" ? builderRead.entries : [],
|
|
3019
|
+
builderSampleEntries: builderEntries,
|
|
2071
3020
|
now: args.now,
|
|
2072
3021
|
});
|
|
2073
3022
|
// Row-union: a resync must only (re)link items that BELONG to this source —
|
|
@@ -2102,6 +3051,17 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
2102
3051
|
existingBuilderRows,
|
|
2103
3052
|
builderEntriesByDocumentId,
|
|
2104
3053
|
});
|
|
3054
|
+
if (builderRead.state === "live") {
|
|
3055
|
+
await enqueueBuilderBodyHydrationForItems({
|
|
3056
|
+
sourceId: args.source.id,
|
|
3057
|
+
ownerEmail: args.database.ownerEmail,
|
|
3058
|
+
orgId: args.database.orgId,
|
|
3059
|
+
sourceTable: args.source.sourceTable,
|
|
3060
|
+
items: itemsToLink,
|
|
3061
|
+
builderEntriesByDocumentId,
|
|
3062
|
+
now: args.now,
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
2105
3065
|
|
|
2106
3066
|
const currentDocumentIds = new Set(
|
|
2107
3067
|
response.items.map((item) => item.document.id),
|
|
@@ -2202,6 +3162,11 @@ export async function replaceSourceMetadata(args: {
|
|
|
2202
3162
|
|
|
2203
3163
|
if (args.source) {
|
|
2204
3164
|
await deleteSourceChangeSetRecords({ sourceId: args.source.id });
|
|
3165
|
+
await db
|
|
3166
|
+
.delete(schema.contentDatabaseBodyHydrationQueue)
|
|
3167
|
+
.where(
|
|
3168
|
+
eq(schema.contentDatabaseBodyHydrationQueue.sourceId, args.source.id),
|
|
3169
|
+
);
|
|
2205
3170
|
await db
|
|
2206
3171
|
.delete(schema.contentDatabaseSourceFields)
|
|
2207
3172
|
.where(eq(schema.contentDatabaseSourceFields.sourceId, args.source.id));
|