@agent-native/core 0.133.2 → 0.133.3
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/use-action.ts +6 -0
- package/corpus/templates/content/actions/_batch-utils.ts +33 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -81
- package/corpus/templates/content/actions/_content-files.ts +31 -21
- package/corpus/templates/content/actions/_database-source-utils.ts +653 -155
- package/corpus/templates/content/actions/_database-utils.ts +535 -41
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +291 -240
- package/corpus/templates/content/actions/add-database-item.ts +15 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +156 -58
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +2 -2
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +4 -1
- package/corpus/templates/content/actions/delete-database-items.ts +8 -2
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +2 -2
- package/corpus/templates/content/actions/duplicate-database-item.ts +15 -1
- package/corpus/templates/content/actions/duplicate-database-items.ts +19 -1
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +2 -1
- package/corpus/templates/content/actions/get-content-database.ts +12 -64
- package/corpus/templates/content/actions/move-database-item.ts +4 -1
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +1 -1
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +4 -8
- package/corpus/templates/content/actions/preview-content-database-source-attach.ts +92 -0
- package/corpus/templates/content/actions/process-builder-body-hydration.ts +2 -1
- package/corpus/templates/content/actions/query-content-database-items.ts +64 -0
- package/corpus/templates/content/actions/refresh-content-database-source.ts +7 -0
- package/corpus/templates/content/actions/review-content-database-source-change-set.ts +1 -1
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +1 -1
- package/corpus/templates/content/actions/stage-builder-revision.ts +1 -1
- package/corpus/templates/content/actions/update-content-database-personal-view.ts +29 -3
- package/corpus/templates/content/actions/update-content-database-view.ts +1 -1
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +1 -1
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +3 -22
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +267 -105
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +82 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +285 -24
- package/corpus/templates/content/app/hooks/use-document-properties.ts +7 -6
- package/corpus/templates/content/app/i18n-data.ts +10 -0
- package/corpus/templates/content/changelog/2026-07-29-large-databases-keep-useful-rows-visible.md +6 -0
- package/corpus/templates/content/changelog/2026-07-30-builder-source-columns-now-appear-immediately-when-connected.md +6 -0
- package/corpus/templates/content/changelog/2026-07-30-large-builder-backed-tables-show-useful-rows-sooner-and-fini.md +6 -0
- package/corpus/templates/content/changelog/2026-08-01-large-builder-databases-now-show-rows-immediately-and-finish.md +6 -0
- package/corpus/templates/content/parity/matrix.md +2 -1
- package/corpus/templates/content/parity/matrix.ts +25 -0
- package/corpus/templates/content/shared/api.ts +54 -4
- package/corpus/templates/content/shared/database-query.ts +359 -0
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +6 -0
- package/dist/client/use-action.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
- package/src/client/use-action.ts +6 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { getDialect, type Dialect } from "@agent-native/core/db";
|
|
4
|
-
import { and, asc, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
|
4
|
+
import { and, asc, eq, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
7
|
import type {
|
|
@@ -55,7 +55,12 @@ import {
|
|
|
55
55
|
type DocumentPropertyOptionColor,
|
|
56
56
|
} from "../shared/properties.js";
|
|
57
57
|
import { sanitizeNormalizationFormula } from "../shared/properties.js";
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
bulkChunkSizeForColumnCount,
|
|
60
|
+
chunks,
|
|
61
|
+
processWithConcurrency,
|
|
62
|
+
} from "./_batch-utils.js";
|
|
63
|
+
export { bulkChunkSizeForColumnCount } from "./_batch-utils.js";
|
|
59
64
|
import {
|
|
60
65
|
readBuilderCmsContentEntry,
|
|
61
66
|
readBuilderCmsContentEntries,
|
|
@@ -123,6 +128,24 @@ function stableBuilderImportId(
|
|
|
123
128
|
.slice(0, 24)}`;
|
|
124
129
|
}
|
|
125
130
|
|
|
131
|
+
export function builderCmsImportIds(args: {
|
|
132
|
+
ownerEmail: string;
|
|
133
|
+
databaseId: string;
|
|
134
|
+
sourceTable: string;
|
|
135
|
+
entryId: string;
|
|
136
|
+
}) {
|
|
137
|
+
const identity = [
|
|
138
|
+
args.ownerEmail,
|
|
139
|
+
args.databaseId,
|
|
140
|
+
args.sourceTable,
|
|
141
|
+
args.entryId,
|
|
142
|
+
];
|
|
143
|
+
return {
|
|
144
|
+
documentId: stableBuilderImportId("builder-doc", identity),
|
|
145
|
+
itemId: stableBuilderImportId("builder-item", identity),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
126
149
|
const DEFAULT_SOURCE_CAPABILITIES: ContentDatabaseSourceCapabilities = {
|
|
127
150
|
canRefresh: true,
|
|
128
151
|
canCreateChangeSets: true,
|
|
@@ -958,38 +981,22 @@ function builderBodyUsesCurrentMediaConverter(
|
|
|
958
981
|
|
|
959
982
|
const BUILDER_BODY_HYDRATION_BACKGROUND_PRIORITY = 10;
|
|
960
983
|
const BUILDER_BODY_HYDRATION_OPEN_PRIORITY = 0;
|
|
961
|
-
const BUILDER_BODY_HYDRATION_BATCH_LIMIT =
|
|
962
|
-
const BUILDER_BODY_HYDRATION_PROCESS_CONCURRENCY =
|
|
984
|
+
const BUILDER_BODY_HYDRATION_BATCH_LIMIT = 600;
|
|
985
|
+
const BUILDER_BODY_HYDRATION_PROCESS_CONCURRENCY = 72;
|
|
986
|
+
const BUILDER_BODY_HYDRATION_BULK_PRELOAD_MIN_JOBS = 20;
|
|
963
987
|
const BUILDER_BODY_HYDRATION_MAX_ATTEMPTS = 5;
|
|
988
|
+
const BUILDER_BODY_HYDRATION_CLAIM_LEASE_MS = 2 * 60 * 1000;
|
|
989
|
+
const BUILDER_BODY_HYDRATION_POSTGRES_BULK_LIMIT = 200;
|
|
990
|
+
const BUILDER_BODY_HYDRATION_MAX_BOUND_PARAMS_PER_ROW = 8;
|
|
964
991
|
const BUILDER_BODY_HYDRATION_CODEC_VERSION =
|
|
965
992
|
"readable-native-images-authoritative-raw-baseline-v9";
|
|
966
993
|
const BUILDER_CMS_REFRESH_INITIAL_PAGES = 1;
|
|
967
994
|
const BUILDER_BODY_NOT_AVAILABLE_ERROR = "body not yet available from Builder";
|
|
968
995
|
|
|
969
|
-
export function bulkChunkSizeForColumnCount(
|
|
970
|
-
columnCount: number,
|
|
971
|
-
dialect: Dialect = getDialect(),
|
|
972
|
-
) {
|
|
973
|
-
// D1 rejects statements with more than 100 bound params, so derive every
|
|
974
|
-
// bulk chunk from the statement's column count instead of a fixed row count.
|
|
975
|
-
const budget = dialect === "d1" ? 90 : 900;
|
|
976
|
-
return Math.max(1, Math.floor(budget / Math.max(1, columnCount)));
|
|
977
|
-
}
|
|
978
|
-
|
|
979
996
|
function idChunkSize() {
|
|
980
997
|
return bulkChunkSizeForColumnCount(1);
|
|
981
998
|
}
|
|
982
999
|
|
|
983
|
-
async function processInBatches<T>(
|
|
984
|
-
items: T[],
|
|
985
|
-
concurrency: number,
|
|
986
|
-
worker: (item: T) => Promise<void>,
|
|
987
|
-
) {
|
|
988
|
-
for (const batch of chunks(items, concurrency)) {
|
|
989
|
-
await Promise.all(batch.map((item) => worker(item)));
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
1000
|
function builderBodyHydrationDelayMs() {
|
|
994
1001
|
if (
|
|
995
1002
|
process.env.NODE_ENV === "production" ||
|
|
@@ -1081,7 +1088,10 @@ function normalizeHydrationLimit(limit: number | null | undefined) {
|
|
|
1081
1088
|
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
1082
1089
|
return BUILDER_BODY_HYDRATION_BATCH_LIMIT;
|
|
1083
1090
|
}
|
|
1084
|
-
return Math.max(
|
|
1091
|
+
return Math.max(
|
|
1092
|
+
1,
|
|
1093
|
+
Math.min(Math.floor(limit), BUILDER_BODY_HYDRATION_BATCH_LIMIT),
|
|
1094
|
+
);
|
|
1085
1095
|
}
|
|
1086
1096
|
|
|
1087
1097
|
function builderBodyIsRawPlaceholderOnly(content: string | null | undefined) {
|
|
@@ -1531,6 +1541,7 @@ export async function enqueueBuilderBodyHydrationForItems(args: {
|
|
|
1531
1541
|
items: ContentDatabaseItem[];
|
|
1532
1542
|
builderEntriesByDocumentId: Map<string, BuilderCmsSourceEntry> | undefined;
|
|
1533
1543
|
now: string;
|
|
1544
|
+
processInBackground?: boolean;
|
|
1534
1545
|
}) {
|
|
1535
1546
|
if (!args.builderEntriesByDocumentId?.size) return;
|
|
1536
1547
|
const persistedStateByDocumentId = new Map<
|
|
@@ -1626,10 +1637,12 @@ export async function enqueueBuilderBodyHydrationForItems(args: {
|
|
|
1626
1637
|
});
|
|
1627
1638
|
}
|
|
1628
1639
|
const queuedJobs = await enqueueBuilderBodyHydrations(requests);
|
|
1640
|
+
if (args.processInBackground === false) return;
|
|
1629
1641
|
void processBuilderBodyHydrationQueue({
|
|
1630
1642
|
sourceId: args.sourceId,
|
|
1631
1643
|
limit: BUILDER_BODY_HYDRATION_BATCH_LIMIT,
|
|
1632
1644
|
preloadedJobs: queuedJobs,
|
|
1645
|
+
preloadBodies: true,
|
|
1633
1646
|
}).catch((error) => {
|
|
1634
1647
|
console.error("Builder body hydration background kick failed", error);
|
|
1635
1648
|
});
|
|
@@ -1732,6 +1745,7 @@ async function processBuilderBodyHydrationJob(
|
|
|
1732
1745
|
sourceRow?: ContentDatabaseSourceRecordRowDb | null;
|
|
1733
1746
|
documentContent?: string | null;
|
|
1734
1747
|
bodyHydrationVersion?: string | null;
|
|
1748
|
+
bodyEntry?: BuilderCmsSourceEntry | null;
|
|
1735
1749
|
},
|
|
1736
1750
|
) {
|
|
1737
1751
|
const db = getDb();
|
|
@@ -1741,9 +1755,19 @@ async function processBuilderBodyHydrationJob(
|
|
|
1741
1755
|
entry.sourceValues,
|
|
1742
1756
|
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
1743
1757
|
);
|
|
1758
|
+
const bodyEntry =
|
|
1759
|
+
preloaded?.bodyEntry?.id === entry.id
|
|
1760
|
+
? {
|
|
1761
|
+
...preloaded.bodyEntry,
|
|
1762
|
+
sourceValues: {
|
|
1763
|
+
...entry.sourceValues,
|
|
1764
|
+
...preloaded.bodyEntry.sourceValues,
|
|
1765
|
+
},
|
|
1766
|
+
}
|
|
1767
|
+
: entry;
|
|
1744
1768
|
let activeSourceEntryJson = row.sourceEntryJson;
|
|
1745
1769
|
let entryWithBody = await refreshBuilderBodySourceValuesFromStoredLossless(
|
|
1746
|
-
await withBuilderBodySourceValues(
|
|
1770
|
+
await withBuilderBodySourceValues(bodyEntry),
|
|
1747
1771
|
);
|
|
1748
1772
|
const sourceRow =
|
|
1749
1773
|
preloaded?.sourceRow != null
|
|
@@ -1873,6 +1897,10 @@ async function processBuilderBodyHydrationJob(
|
|
|
1873
1897
|
schema.contentDatabaseBodyHydrationQueue.sourceEntryJson,
|
|
1874
1898
|
activeSourceEntryJson,
|
|
1875
1899
|
),
|
|
1900
|
+
eq(
|
|
1901
|
+
schema.contentDatabaseBodyHydrationQueue.attempts,
|
|
1902
|
+
row.attempts,
|
|
1903
|
+
),
|
|
1876
1904
|
),
|
|
1877
1905
|
)
|
|
1878
1906
|
.returning({ id: schema.contentDatabaseBodyHydrationQueue.id });
|
|
@@ -1912,6 +1940,10 @@ async function processBuilderBodyHydrationJob(
|
|
|
1912
1940
|
schema.contentDatabaseBodyHydrationQueue.sourceEntryJson,
|
|
1913
1941
|
activeSourceEntryJson,
|
|
1914
1942
|
),
|
|
1943
|
+
eq(
|
|
1944
|
+
schema.contentDatabaseBodyHydrationQueue.attempts,
|
|
1945
|
+
row.attempts,
|
|
1946
|
+
),
|
|
1915
1947
|
),
|
|
1916
1948
|
)
|
|
1917
1949
|
.returning({ id: schema.contentDatabaseBodyHydrationQueue.id });
|
|
@@ -1926,12 +1958,9 @@ async function processBuilderBodyHydrationJob(
|
|
|
1926
1958
|
if (!nextContent.trim()) {
|
|
1927
1959
|
const attempts = row.attempts;
|
|
1928
1960
|
await db.transaction(async (tx) => {
|
|
1929
|
-
const queueRowCas =
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
schema.contentDatabaseBodyHydrationQueue.sourceEntryJson,
|
|
1933
|
-
activeSourceEntryJson,
|
|
1934
|
-
),
|
|
1961
|
+
const queueRowCas = builderBodyHydrationQueueOwnershipFilter(
|
|
1962
|
+
row,
|
|
1963
|
+
activeSourceEntryJson,
|
|
1935
1964
|
);
|
|
1936
1965
|
const markPendingIfReplaced = async () => {
|
|
1937
1966
|
const [replacedByNewerJob] = await tx
|
|
@@ -1974,6 +2003,7 @@ async function processBuilderBodyHydrationJob(
|
|
|
1974
2003
|
const [stillOwnsQueueRow] = await tx
|
|
1975
2004
|
.update(schema.contentDatabaseBodyHydrationQueue)
|
|
1976
2005
|
.set({
|
|
2006
|
+
lastAttemptedAt: null,
|
|
1977
2007
|
lastError: BUILDER_BODY_NOT_AVAILABLE_ERROR,
|
|
1978
2008
|
updatedAt: now,
|
|
1979
2009
|
})
|
|
@@ -2021,12 +2051,9 @@ async function processBuilderBodyHydrationJob(
|
|
|
2021
2051
|
});
|
|
2022
2052
|
let wroteBody = false;
|
|
2023
2053
|
await db.transaction(async (tx) => {
|
|
2024
|
-
const queueRowCas =
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
schema.contentDatabaseBodyHydrationQueue.sourceEntryJson,
|
|
2028
|
-
activeSourceEntryJson,
|
|
2029
|
-
),
|
|
2054
|
+
const queueRowCas = builderBodyHydrationQueueOwnershipFilter(
|
|
2055
|
+
row,
|
|
2056
|
+
activeSourceEntryJson,
|
|
2030
2057
|
);
|
|
2031
2058
|
const markPendingIfReplaced = async () => {
|
|
2032
2059
|
const [replacedByNewerJob] = await tx
|
|
@@ -2185,6 +2212,276 @@ async function processBuilderBodyHydrationJob(
|
|
|
2185
2212
|
// repersist the empty state over the newly hydrated Builder body.
|
|
2186
2213
|
}
|
|
2187
2214
|
|
|
2215
|
+
type PreparedPristineBuilderBodyHydration = {
|
|
2216
|
+
job: ContentDatabaseBodyHydrationQueueRowDb;
|
|
2217
|
+
sourceRow: ContentDatabaseSourceRecordRowDb;
|
|
2218
|
+
documentContent: string;
|
|
2219
|
+
content: string;
|
|
2220
|
+
sourceValuesJson: string;
|
|
2221
|
+
lastSourceUpdatedAt: string;
|
|
2222
|
+
bodyHydrationVersion: string;
|
|
2223
|
+
};
|
|
2224
|
+
|
|
2225
|
+
class PristineBuilderBodyHydrationCasMiss extends Error {}
|
|
2226
|
+
|
|
2227
|
+
function hydrationCaseSql<T>(
|
|
2228
|
+
idColumn: unknown,
|
|
2229
|
+
fallbackColumn: unknown,
|
|
2230
|
+
values: Array<{ id: string; value: T }>,
|
|
2231
|
+
) {
|
|
2232
|
+
const cases = values.map(({ id, value }) => sql`WHEN ${id} THEN ${value}`);
|
|
2233
|
+
return sql<T>`CASE ${idColumn} ${sql.join(cases, sql` `)} ELSE ${fallbackColumn} END`;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
function builderBodyHydrationQueueOwnershipFilter(
|
|
2237
|
+
job: ContentDatabaseBodyHydrationQueueRowDb,
|
|
2238
|
+
sourceEntryJson = job.sourceEntryJson,
|
|
2239
|
+
) {
|
|
2240
|
+
return and(
|
|
2241
|
+
eq(schema.contentDatabaseBodyHydrationQueue.id, job.id),
|
|
2242
|
+
eq(
|
|
2243
|
+
schema.contentDatabaseBodyHydrationQueue.sourceEntryJson,
|
|
2244
|
+
sourceEntryJson,
|
|
2245
|
+
),
|
|
2246
|
+
eq(schema.contentDatabaseBodyHydrationQueue.attempts, job.attempts),
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
async function preparePristineBuilderBodyHydration(args: {
|
|
2251
|
+
job: ContentDatabaseBodyHydrationQueueRowDb;
|
|
2252
|
+
sourceRow: ContentDatabaseSourceRecordRowDb | null;
|
|
2253
|
+
documentContent: string | null | undefined;
|
|
2254
|
+
bodyHydrationVersion: string | null;
|
|
2255
|
+
bodyEntry: BuilderCmsSourceEntry | null;
|
|
2256
|
+
}): Promise<PreparedPristineBuilderBodyHydration | null> {
|
|
2257
|
+
const { job, sourceRow, documentContent, bodyHydrationVersion, bodyEntry } =
|
|
2258
|
+
args;
|
|
2259
|
+
if (
|
|
2260
|
+
job.attempts !== 1 ||
|
|
2261
|
+
!sourceRow ||
|
|
2262
|
+
!bodyEntry ||
|
|
2263
|
+
bodyHydrationVersion !== null ||
|
|
2264
|
+
!isEffectivelyEmptyDocumentContent(documentContent ?? "") ||
|
|
2265
|
+
sourceRow.sourceRowId !== job.sourceRowId ||
|
|
2266
|
+
bodyEntry.id !== job.sourceRowId
|
|
2267
|
+
) {
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
const queuedEntry = parseHydrationEntry(job);
|
|
2271
|
+
if (!queuedEntry || queuedEntry.id !== bodyEntry.id) return null;
|
|
2272
|
+
const sourceValues =
|
|
2273
|
+
parseObject<Record<string, DocumentPropertyValue>>(
|
|
2274
|
+
sourceRow.sourceValuesJson,
|
|
2275
|
+
) ?? {};
|
|
2276
|
+
if (
|
|
2277
|
+
stringSourceValue(sourceValues, BUILDER_CMS_BODY_CONTENT_KEY)?.trim() ||
|
|
2278
|
+
stringSourceValue(sourceValues, BUILDER_CMS_BODY_BLOCKS_HASH_KEY)
|
|
2279
|
+
) {
|
|
2280
|
+
return null;
|
|
2281
|
+
}
|
|
2282
|
+
const queuedLastUpdated =
|
|
2283
|
+
stringSourceValue(queuedEntry.sourceValues, "lastUpdated") ??
|
|
2284
|
+
queuedEntry.updatedAt;
|
|
2285
|
+
const bodyLastUpdated =
|
|
2286
|
+
stringSourceValue(bodyEntry.sourceValues, "lastUpdated") ??
|
|
2287
|
+
bodyEntry.updatedAt;
|
|
2288
|
+
if (
|
|
2289
|
+
queuedLastUpdated &&
|
|
2290
|
+
bodyLastUpdated &&
|
|
2291
|
+
queuedLastUpdated !== bodyLastUpdated
|
|
2292
|
+
) {
|
|
2293
|
+
return null;
|
|
2294
|
+
}
|
|
2295
|
+
const entryWithBody = await refreshBuilderBodySourceValuesFromStoredLossless(
|
|
2296
|
+
await withBuilderBodySourceValues({
|
|
2297
|
+
...bodyEntry,
|
|
2298
|
+
sourceValues: {
|
|
2299
|
+
...queuedEntry.sourceValues,
|
|
2300
|
+
...bodyEntry.sourceValues,
|
|
2301
|
+
},
|
|
2302
|
+
}),
|
|
2303
|
+
);
|
|
2304
|
+
const nextValues = {
|
|
2305
|
+
...sourceValues,
|
|
2306
|
+
...entryWithBody.sourceValues,
|
|
2307
|
+
};
|
|
2308
|
+
const content =
|
|
2309
|
+
stringSourceValue(nextValues, BUILDER_CMS_BODY_CONTENT_KEY) ?? "";
|
|
2310
|
+
if (!content.trim()) return null;
|
|
2311
|
+
return {
|
|
2312
|
+
job,
|
|
2313
|
+
sourceRow,
|
|
2314
|
+
documentContent: documentContent ?? "",
|
|
2315
|
+
content,
|
|
2316
|
+
sourceValuesJson: JSON.stringify(nextValues),
|
|
2317
|
+
lastSourceUpdatedAt: entryWithBody.updatedAt ?? new Date().toISOString(),
|
|
2318
|
+
bodyHydrationVersion: builderBodyHydrationVersion(entryWithBody),
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
async function persistPristineBuilderBodyHydrationsInBulk(
|
|
2323
|
+
prepared: PreparedPristineBuilderBodyHydration[],
|
|
2324
|
+
now: string,
|
|
2325
|
+
) {
|
|
2326
|
+
const db = getDb();
|
|
2327
|
+
const persistedJobIds = new Set<string>();
|
|
2328
|
+
const chunkLimit = builderBodyHydrationBulkChunkLimit();
|
|
2329
|
+
for (const batch of chunks(prepared, chunkLimit)) {
|
|
2330
|
+
try {
|
|
2331
|
+
await db.transaction(async (tx) => {
|
|
2332
|
+
const queueOwnership = batch.map(({ job }) =>
|
|
2333
|
+
builderBodyHydrationQueueOwnershipFilter(job),
|
|
2334
|
+
);
|
|
2335
|
+
|
|
2336
|
+
const updatedDocuments = await tx
|
|
2337
|
+
.update(schema.documents)
|
|
2338
|
+
.set({
|
|
2339
|
+
content: hydrationCaseSql(
|
|
2340
|
+
schema.documents.id,
|
|
2341
|
+
schema.documents.content,
|
|
2342
|
+
batch.map((row) => ({
|
|
2343
|
+
id: row.job.documentId,
|
|
2344
|
+
value: row.content,
|
|
2345
|
+
})),
|
|
2346
|
+
),
|
|
2347
|
+
updatedAt: now,
|
|
2348
|
+
})
|
|
2349
|
+
.where(
|
|
2350
|
+
or(
|
|
2351
|
+
...batch.map((row) =>
|
|
2352
|
+
and(
|
|
2353
|
+
eq(schema.documents.id, row.job.documentId),
|
|
2354
|
+
eq(schema.documents.content, row.documentContent),
|
|
2355
|
+
),
|
|
2356
|
+
),
|
|
2357
|
+
),
|
|
2358
|
+
)
|
|
2359
|
+
.returning({ id: schema.documents.id });
|
|
2360
|
+
if (updatedDocuments.length !== batch.length) {
|
|
2361
|
+
throw new PristineBuilderBodyHydrationCasMiss(
|
|
2362
|
+
"Builder body hydration document changed.",
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
const updatedSourceRows = await tx
|
|
2367
|
+
.update(schema.contentDatabaseSourceRows)
|
|
2368
|
+
.set({
|
|
2369
|
+
sourceValuesJson: hydrationCaseSql(
|
|
2370
|
+
schema.contentDatabaseSourceRows.id,
|
|
2371
|
+
schema.contentDatabaseSourceRows.sourceValuesJson,
|
|
2372
|
+
batch.map((row) => ({
|
|
2373
|
+
id: row.sourceRow.id,
|
|
2374
|
+
value: row.sourceValuesJson,
|
|
2375
|
+
})),
|
|
2376
|
+
),
|
|
2377
|
+
lastSyncedAt: now,
|
|
2378
|
+
lastSourceUpdatedAt: hydrationCaseSql(
|
|
2379
|
+
schema.contentDatabaseSourceRows.id,
|
|
2380
|
+
schema.contentDatabaseSourceRows.lastSourceUpdatedAt,
|
|
2381
|
+
batch.map((row) => ({
|
|
2382
|
+
id: row.sourceRow.id,
|
|
2383
|
+
value: row.lastSourceUpdatedAt,
|
|
2384
|
+
})),
|
|
2385
|
+
),
|
|
2386
|
+
updatedAt: now,
|
|
2387
|
+
})
|
|
2388
|
+
.where(
|
|
2389
|
+
or(
|
|
2390
|
+
...batch.map((row) =>
|
|
2391
|
+
and(
|
|
2392
|
+
eq(schema.contentDatabaseSourceRows.id, row.sourceRow.id),
|
|
2393
|
+
eq(
|
|
2394
|
+
schema.contentDatabaseSourceRows.sourceId,
|
|
2395
|
+
row.job.sourceId,
|
|
2396
|
+
),
|
|
2397
|
+
eq(
|
|
2398
|
+
schema.contentDatabaseSourceRows.databaseItemId,
|
|
2399
|
+
row.job.databaseItemId,
|
|
2400
|
+
),
|
|
2401
|
+
eq(
|
|
2402
|
+
schema.contentDatabaseSourceRows.sourceValuesJson,
|
|
2403
|
+
row.sourceRow.sourceValuesJson,
|
|
2404
|
+
),
|
|
2405
|
+
),
|
|
2406
|
+
),
|
|
2407
|
+
),
|
|
2408
|
+
)
|
|
2409
|
+
.returning({ id: schema.contentDatabaseSourceRows.id });
|
|
2410
|
+
if (updatedSourceRows.length !== batch.length) {
|
|
2411
|
+
throw new PristineBuilderBodyHydrationCasMiss(
|
|
2412
|
+
"Builder body hydration source row changed.",
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
const deletedQueueRows = await tx
|
|
2417
|
+
.delete(schema.contentDatabaseBodyHydrationQueue)
|
|
2418
|
+
.where(or(...queueOwnership))
|
|
2419
|
+
.returning({
|
|
2420
|
+
id: schema.contentDatabaseBodyHydrationQueue.id,
|
|
2421
|
+
});
|
|
2422
|
+
if (deletedQueueRows.length !== batch.length) {
|
|
2423
|
+
// This guarded delete is the queue-ownership CAS for the whole
|
|
2424
|
+
// transaction; a miss rolls back the preceding document/source writes.
|
|
2425
|
+
throw new PristineBuilderBodyHydrationCasMiss(
|
|
2426
|
+
"Builder body hydration queue changed.",
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
const updatedItems = await tx
|
|
2431
|
+
.update(schema.contentDatabaseItems)
|
|
2432
|
+
.set({
|
|
2433
|
+
bodyHydrationStatus: "hydrated",
|
|
2434
|
+
bodyHydrationAttemptedAt: now,
|
|
2435
|
+
bodyHydrationError: null,
|
|
2436
|
+
bodyHydrationVersion: hydrationCaseSql(
|
|
2437
|
+
schema.contentDatabaseItems.id,
|
|
2438
|
+
schema.contentDatabaseItems.bodyHydrationVersion,
|
|
2439
|
+
batch.map((row) => ({
|
|
2440
|
+
id: row.job.databaseItemId,
|
|
2441
|
+
value: row.bodyHydrationVersion,
|
|
2442
|
+
})),
|
|
2443
|
+
),
|
|
2444
|
+
updatedAt: now,
|
|
2445
|
+
})
|
|
2446
|
+
.where(
|
|
2447
|
+
and(
|
|
2448
|
+
inArray(
|
|
2449
|
+
schema.contentDatabaseItems.id,
|
|
2450
|
+
batch.map((row) => row.job.databaseItemId),
|
|
2451
|
+
),
|
|
2452
|
+
eq(schema.contentDatabaseItems.bodyHydrationStatus, "hydrating"),
|
|
2453
|
+
),
|
|
2454
|
+
)
|
|
2455
|
+
.returning({ id: schema.contentDatabaseItems.id });
|
|
2456
|
+
if (updatedItems.length !== batch.length) {
|
|
2457
|
+
throw new PristineBuilderBodyHydrationCasMiss(
|
|
2458
|
+
"Builder body hydration item changed.",
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
});
|
|
2462
|
+
for (const row of batch) persistedJobIds.add(row.job.id);
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
if (!(error instanceof PristineBuilderBodyHydrationCasMiss)) throw error;
|
|
2465
|
+
// The transaction rolled the whole chunk back. The existing per-row path
|
|
2466
|
+
// below reloads its conflict-sensitive state and preserves the safer
|
|
2467
|
+
// edit, migration, retry, and unavailable-body semantics.
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
return persistedJobIds;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
export function builderBodyHydrationBulkChunkLimit(
|
|
2474
|
+
dialect: Dialect = getDialect(),
|
|
2475
|
+
) {
|
|
2476
|
+
const portableLimit = bulkChunkSizeForColumnCount(
|
|
2477
|
+
BUILDER_BODY_HYDRATION_MAX_BOUND_PARAMS_PER_ROW,
|
|
2478
|
+
dialect,
|
|
2479
|
+
);
|
|
2480
|
+
return dialect === "postgres"
|
|
2481
|
+
? Math.max(portableLimit, BUILDER_BODY_HYDRATION_POSTGRES_BULK_LIMIT)
|
|
2482
|
+
: portableLimit;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2188
2485
|
async function enqueueStaleBuilderBodyHydrationForOpenDocument(args: {
|
|
2189
2486
|
sourceId: string;
|
|
2190
2487
|
documentId: string;
|
|
@@ -2263,6 +2560,7 @@ export async function processBuilderBodyHydrationQueue(args: {
|
|
|
2263
2560
|
documentId?: string | null;
|
|
2264
2561
|
limit?: number | null;
|
|
2265
2562
|
preloadedJobs?: ContentDatabaseBodyHydrationQueueRowDb[];
|
|
2563
|
+
preloadBodies?: boolean;
|
|
2266
2564
|
}) {
|
|
2267
2565
|
const db = getDb();
|
|
2268
2566
|
const limit = normalizeHydrationLimit(args.limit);
|
|
@@ -2329,11 +2627,17 @@ export async function processBuilderBodyHydrationQueue(args: {
|
|
|
2329
2627
|
});
|
|
2330
2628
|
})()
|
|
2331
2629
|
: persistedJobs(limit));
|
|
2630
|
+
const claimLeaseCutoff = new Date(
|
|
2631
|
+
Date.now() - BUILDER_BODY_HYDRATION_CLAIM_LEASE_MS,
|
|
2632
|
+
).toISOString();
|
|
2332
2633
|
|
|
2333
2634
|
let succeeded = 0;
|
|
2334
2635
|
let failed = 0;
|
|
2335
2636
|
const claimedJobs: ContentDatabaseBodyHydrationQueueRowDb[] = [];
|
|
2336
|
-
for (const jobChunk of chunks(
|
|
2637
|
+
for (const jobChunk of chunks(
|
|
2638
|
+
jobs,
|
|
2639
|
+
bulkChunkSizeForColumnCount(args.preloadBodies === true ? 5 : 3),
|
|
2640
|
+
)) {
|
|
2337
2641
|
const claimFilters = jobChunk.map((job) =>
|
|
2338
2642
|
and(
|
|
2339
2643
|
eq(schema.contentDatabaseBodyHydrationQueue.id, job.id),
|
|
@@ -2341,6 +2645,28 @@ export async function processBuilderBodyHydrationQueue(args: {
|
|
|
2341
2645
|
schema.contentDatabaseBodyHydrationQueue.sourceEntryJson,
|
|
2342
2646
|
job.sourceEntryJson,
|
|
2343
2647
|
),
|
|
2648
|
+
eq(schema.contentDatabaseBodyHydrationQueue.attempts, job.attempts),
|
|
2649
|
+
args.preloadBodies === true
|
|
2650
|
+
? and(
|
|
2651
|
+
job.lastAttemptedAt
|
|
2652
|
+
? eq(
|
|
2653
|
+
schema.contentDatabaseBodyHydrationQueue.lastAttemptedAt,
|
|
2654
|
+
job.lastAttemptedAt,
|
|
2655
|
+
)
|
|
2656
|
+
: isNull(
|
|
2657
|
+
schema.contentDatabaseBodyHydrationQueue.lastAttemptedAt,
|
|
2658
|
+
),
|
|
2659
|
+
or(
|
|
2660
|
+
isNull(
|
|
2661
|
+
schema.contentDatabaseBodyHydrationQueue.lastAttemptedAt,
|
|
2662
|
+
),
|
|
2663
|
+
lt(
|
|
2664
|
+
schema.contentDatabaseBodyHydrationQueue.lastAttemptedAt,
|
|
2665
|
+
claimLeaseCutoff,
|
|
2666
|
+
),
|
|
2667
|
+
),
|
|
2668
|
+
)
|
|
2669
|
+
: undefined,
|
|
2344
2670
|
),
|
|
2345
2671
|
);
|
|
2346
2672
|
if (claimFilters.length === 0) continue;
|
|
@@ -2446,8 +2772,64 @@ export async function processBuilderBodyHydrationQueue(args: {
|
|
|
2446
2772
|
const documentContentById = new Map(
|
|
2447
2773
|
documents.map((document) => [document.id, document.content]),
|
|
2448
2774
|
);
|
|
2449
|
-
|
|
2450
|
-
claimedJobs,
|
|
2775
|
+
const claimedSourceRowIds = new Set(
|
|
2776
|
+
claimedJobs.map((job) => job.sourceRowId),
|
|
2777
|
+
);
|
|
2778
|
+
const bodyEntryById = new Map<string, BuilderCmsSourceEntry>();
|
|
2779
|
+
const bulkPreloadBodies =
|
|
2780
|
+
args.preloadBodies === true &&
|
|
2781
|
+
claimedJobs.length >= BUILDER_BODY_HYDRATION_BULK_PRELOAD_MIN_JOBS;
|
|
2782
|
+
if (!args.documentId && bulkPreloadBodies && claimedJobs.length > 0) {
|
|
2783
|
+
const sourceTables = Array.from(
|
|
2784
|
+
new Set(claimedJobs.map((job) => job.sourceTable)),
|
|
2785
|
+
);
|
|
2786
|
+
const bodyReads = await Promise.all(
|
|
2787
|
+
sourceTables.map((model) =>
|
|
2788
|
+
readBuilderCmsContentEntries({
|
|
2789
|
+
model,
|
|
2790
|
+
includeBodies: true,
|
|
2791
|
+
limit: 10_000,
|
|
2792
|
+
}),
|
|
2793
|
+
),
|
|
2794
|
+
);
|
|
2795
|
+
for (const read of bodyReads) {
|
|
2796
|
+
if (read.state !== "live") continue;
|
|
2797
|
+
for (const entry of read.entries) {
|
|
2798
|
+
if (claimedSourceRowIds.has(entry.id)) {
|
|
2799
|
+
bodyEntryById.set(entry.id, entry);
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
const preparedPristineHydrations: PreparedPristineBuilderBodyHydration[] = [];
|
|
2805
|
+
if (!args.documentId && bulkPreloadBodies) {
|
|
2806
|
+
await processWithConcurrency(
|
|
2807
|
+
claimedJobs,
|
|
2808
|
+
BUILDER_BODY_HYDRATION_PROCESS_CONCURRENCY,
|
|
2809
|
+
async (job) => {
|
|
2810
|
+
try {
|
|
2811
|
+
const prepared = await preparePristineBuilderBodyHydration({
|
|
2812
|
+
job,
|
|
2813
|
+
sourceRow: sourceRowsByItemId.get(job.databaseItemId) ?? null,
|
|
2814
|
+
documentContent: documentContentById.get(job.documentId),
|
|
2815
|
+
bodyHydrationVersion:
|
|
2816
|
+
bodyHydrationVersionByItemId.get(job.databaseItemId) ?? null,
|
|
2817
|
+
bodyEntry: bodyEntryById.get(job.sourceRowId) ?? null,
|
|
2818
|
+
});
|
|
2819
|
+
if (prepared) preparedPristineHydrations.push(prepared);
|
|
2820
|
+
} catch {
|
|
2821
|
+
// The established per-row path below owns conversion and retry errors.
|
|
2822
|
+
}
|
|
2823
|
+
},
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
const bulkPersistedJobIds = await persistPristineBuilderBodyHydrationsInBulk(
|
|
2827
|
+
preparedPristineHydrations,
|
|
2828
|
+
now,
|
|
2829
|
+
);
|
|
2830
|
+
succeeded += bulkPersistedJobIds.size;
|
|
2831
|
+
await processWithConcurrency(
|
|
2832
|
+
claimedJobs.filter((job) => !bulkPersistedJobIds.has(job.id)),
|
|
2451
2833
|
BUILDER_BODY_HYDRATION_PROCESS_CONCURRENCY,
|
|
2452
2834
|
async (job) => {
|
|
2453
2835
|
const attemptNow = new Date().toISOString();
|
|
@@ -2458,6 +2840,7 @@ export async function processBuilderBodyHydrationQueue(args: {
|
|
|
2458
2840
|
sourceRow: sourceRowsByItemId.get(job.databaseItemId) ?? null,
|
|
2459
2841
|
bodyHydrationVersion:
|
|
2460
2842
|
bodyHydrationVersionByItemId.get(job.databaseItemId) ?? null,
|
|
2843
|
+
bodyEntry: bodyEntryById.get(job.sourceRowId) ?? null,
|
|
2461
2844
|
documentContent: documentContentById.has(job.documentId)
|
|
2462
2845
|
? (documentContentById.get(job.documentId) ?? null)
|
|
2463
2846
|
: undefined,
|
|
@@ -2467,13 +2850,7 @@ export async function processBuilderBodyHydrationQueue(args: {
|
|
|
2467
2850
|
failed += 1;
|
|
2468
2851
|
const message = error instanceof Error ? error.message : String(error);
|
|
2469
2852
|
const attempts = job.attempts;
|
|
2470
|
-
const queueRowCas =
|
|
2471
|
-
eq(schema.contentDatabaseBodyHydrationQueue.id, job.id),
|
|
2472
|
-
eq(
|
|
2473
|
-
schema.contentDatabaseBodyHydrationQueue.sourceEntryJson,
|
|
2474
|
-
job.sourceEntryJson,
|
|
2475
|
-
),
|
|
2476
|
-
);
|
|
2853
|
+
const queueRowCas = builderBodyHydrationQueueOwnershipFilter(job);
|
|
2477
2854
|
const markPendingIfReplaced = async () => {
|
|
2478
2855
|
const [replacedByNewerJob] = await db
|
|
2479
2856
|
.select({ id: schema.contentDatabaseBodyHydrationQueue.id })
|
|
@@ -2514,7 +2891,7 @@ export async function processBuilderBodyHydrationQueue(args: {
|
|
|
2514
2891
|
.update(schema.contentDatabaseBodyHydrationQueue)
|
|
2515
2892
|
.set({
|
|
2516
2893
|
attempts,
|
|
2517
|
-
lastAttemptedAt:
|
|
2894
|
+
lastAttemptedAt: null,
|
|
2518
2895
|
lastError: message,
|
|
2519
2896
|
priority: job.priority + 10,
|
|
2520
2897
|
updatedAt: attemptNow,
|
|
@@ -3514,6 +3891,7 @@ export async function getContentDatabaseSourceSnapshotForReview(
|
|
|
3514
3891
|
*/
|
|
3515
3892
|
export async function getAllContentDatabaseSourceSnapshots(
|
|
3516
3893
|
database: ContentDatabaseRow | ContentDatabase,
|
|
3894
|
+
options: { documentIds?: string[] } = {},
|
|
3517
3895
|
): Promise<ContentDatabaseSource[]> {
|
|
3518
3896
|
if ("deletedAt" in database && database.deletedAt) {
|
|
3519
3897
|
throw new Error(`Database "${database.id}" not found`);
|
|
@@ -3527,15 +3905,31 @@ export async function getAllContentDatabaseSourceSnapshots(
|
|
|
3527
3905
|
asc(schema.contentDatabaseSources.createdAt),
|
|
3528
3906
|
asc(schema.contentDatabaseSources.id),
|
|
3529
3907
|
);
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
await loadSourceSnapshot(source, database, {
|
|
3908
|
+
return Promise.all(
|
|
3909
|
+
sources.map((source) => {
|
|
3910
|
+
return loadSourceSnapshot(source, database, {
|
|
3534
3911
|
includeHeavyBuilderBodyValues: false,
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3912
|
+
documentIds: sourceSnapshotPageDocumentIds({
|
|
3913
|
+
sourceType: source.sourceType,
|
|
3914
|
+
metadataJson: source.metadataJson,
|
|
3915
|
+
documentIds: options.documentIds,
|
|
3916
|
+
}),
|
|
3917
|
+
});
|
|
3918
|
+
}),
|
|
3919
|
+
);
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
export function sourceSnapshotPageDocumentIds(args: {
|
|
3923
|
+
sourceType: string;
|
|
3924
|
+
metadataJson: string | null;
|
|
3925
|
+
documentIds?: string[];
|
|
3926
|
+
}) {
|
|
3927
|
+
const metadata = parseObject<SourceMetadataRecord>(args.metadataJson);
|
|
3928
|
+
const federation = normalizeSourceFederation(metadata?.federation);
|
|
3929
|
+
return normalizeSourceType(args.sourceType) === "builder-cms" &&
|
|
3930
|
+
federation?.role !== "secondary"
|
|
3931
|
+
? args.documentIds
|
|
3932
|
+
: undefined;
|
|
3539
3933
|
}
|
|
3540
3934
|
|
|
3541
3935
|
async function readSourceSnapshotRowsOnce(args: {
|
|
@@ -3546,9 +3940,7 @@ async function readSourceSnapshotRowsOnce(args: {
|
|
|
3546
3940
|
documentIds?: string[];
|
|
3547
3941
|
}) {
|
|
3548
3942
|
const db = getDb();
|
|
3549
|
-
const documentScope = args.documentIds
|
|
3550
|
-
? new Set(args.documentIds)
|
|
3551
|
-
: null;
|
|
3943
|
+
const documentScope = args.documentIds ? new Set(args.documentIds) : null;
|
|
3552
3944
|
const rowRows = await db
|
|
3553
3945
|
.select(
|
|
3554
3946
|
sourceSnapshotRowSelection({
|
|
@@ -3660,9 +4052,7 @@ async function sourceSnapshotConsistencyMarker(args: {
|
|
|
3660
4052
|
documentIds?: string[];
|
|
3661
4053
|
}) {
|
|
3662
4054
|
const db = getDb();
|
|
3663
|
-
const documentScope = args.documentIds
|
|
3664
|
-
? new Set(args.documentIds)
|
|
3665
|
-
: null;
|
|
4055
|
+
const documentScope = args.documentIds ? new Set(args.documentIds) : null;
|
|
3666
4056
|
const [rows] = await db
|
|
3667
4057
|
.select({
|
|
3668
4058
|
count: sql<number>`COUNT(*)`,
|
|
@@ -3737,9 +4127,14 @@ async function loadSourceSnapshotRowsOptimistically(args: {
|
|
|
3737
4127
|
const before = await sourceSnapshotConsistencyMarker(args);
|
|
3738
4128
|
latest = await readSourceSnapshotRowsOnce(args);
|
|
3739
4129
|
const after = await sourceSnapshotConsistencyMarker(args);
|
|
3740
|
-
if (sourceSnapshotConsistencyMarkersEqual(before, after))
|
|
4130
|
+
if (sourceSnapshotConsistencyMarkersEqual(before, after)) {
|
|
4131
|
+
return { ...latest, consistencyAttempts: attempt + 1 };
|
|
4132
|
+
}
|
|
3741
4133
|
}
|
|
3742
|
-
return
|
|
4134
|
+
return {
|
|
4135
|
+
...(latest ?? (await readSourceSnapshotRowsOnce(args))),
|
|
4136
|
+
consistencyAttempts: 3,
|
|
4137
|
+
};
|
|
3743
4138
|
}
|
|
3744
4139
|
|
|
3745
4140
|
async function loadSourceSnapshot(
|
|
@@ -3748,50 +4143,76 @@ async function loadSourceSnapshot(
|
|
|
3748
4143
|
options: { includeHeavyBuilderBodyValues: boolean; documentIds?: string[] },
|
|
3749
4144
|
): Promise<ContentDatabaseSource> {
|
|
3750
4145
|
const db = getDb();
|
|
3751
|
-
const [fieldRows, changeRows,
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
.
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
4146
|
+
const [fieldRows, changeRows, propertyDefs] = await Promise.all([
|
|
4147
|
+
db
|
|
4148
|
+
.select()
|
|
4149
|
+
.from(schema.contentDatabaseSourceFields)
|
|
4150
|
+
.where(eq(schema.contentDatabaseSourceFields.sourceId, source.id))
|
|
4151
|
+
.orderBy(asc(schema.contentDatabaseSourceFields.createdAt)),
|
|
4152
|
+
db
|
|
4153
|
+
.select()
|
|
4154
|
+
.from(schema.contentDatabaseSourceChangeSets)
|
|
4155
|
+
.where(
|
|
4156
|
+
options.documentIds !== undefined
|
|
4157
|
+
? and(
|
|
4158
|
+
eq(schema.contentDatabaseSourceChangeSets.sourceId, source.id),
|
|
4159
|
+
inArray(
|
|
4160
|
+
schema.contentDatabaseSourceChangeSets.documentId,
|
|
4161
|
+
options.documentIds,
|
|
4162
|
+
),
|
|
4163
|
+
)
|
|
4164
|
+
: eq(schema.contentDatabaseSourceChangeSets.sourceId, source.id),
|
|
4165
|
+
)
|
|
4166
|
+
.orderBy(asc(schema.contentDatabaseSourceChangeSets.createdAt)),
|
|
4167
|
+
db
|
|
4168
|
+
.select({
|
|
4169
|
+
id: schema.documentPropertyDefinitions.id,
|
|
4170
|
+
name: schema.documentPropertyDefinitions.name,
|
|
4171
|
+
type: schema.documentPropertyDefinitions.type,
|
|
4172
|
+
optionsJson: schema.documentPropertyDefinitions.optionsJson,
|
|
4173
|
+
})
|
|
4174
|
+
.from(schema.documentPropertyDefinitions)
|
|
4175
|
+
.where(eq(schema.documentPropertyDefinitions.databaseId, database.id)),
|
|
4176
|
+
]);
|
|
4177
|
+
const scopedChangeSetIds = changeRows.map((changeSet) => changeSet.id);
|
|
4178
|
+
const [reviewRows, executionRows] =
|
|
4179
|
+
options.documentIds !== undefined && scopedChangeSetIds.length === 0
|
|
4180
|
+
? [[], []]
|
|
4181
|
+
: await Promise.all([
|
|
4182
|
+
db
|
|
4183
|
+
.select()
|
|
4184
|
+
.from(schema.contentDatabaseSourceChangeReviews)
|
|
4185
|
+
.where(
|
|
4186
|
+
and(
|
|
4187
|
+
eq(
|
|
4188
|
+
schema.contentDatabaseSourceChangeReviews.sourceId,
|
|
4189
|
+
source.id,
|
|
3768
4190
|
),
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
]);
|
|
4191
|
+
options.documentIds !== undefined
|
|
4192
|
+
? inArray(
|
|
4193
|
+
schema.contentDatabaseSourceChangeReviews.changeSetId,
|
|
4194
|
+
scopedChangeSetIds,
|
|
4195
|
+
)
|
|
4196
|
+
: undefined,
|
|
4197
|
+
),
|
|
4198
|
+
)
|
|
4199
|
+
.orderBy(asc(schema.contentDatabaseSourceChangeReviews.createdAt)),
|
|
4200
|
+
db
|
|
4201
|
+
.select()
|
|
4202
|
+
.from(schema.contentDatabaseSourceExecutions)
|
|
4203
|
+
.where(
|
|
4204
|
+
and(
|
|
4205
|
+
eq(schema.contentDatabaseSourceExecutions.sourceId, source.id),
|
|
4206
|
+
options.documentIds !== undefined
|
|
4207
|
+
? inArray(
|
|
4208
|
+
schema.contentDatabaseSourceExecutions.changeSetId,
|
|
4209
|
+
scopedChangeSetIds,
|
|
4210
|
+
)
|
|
4211
|
+
: undefined,
|
|
4212
|
+
),
|
|
4213
|
+
)
|
|
4214
|
+
.orderBy(asc(schema.contentDatabaseSourceExecutions.createdAt)),
|
|
4215
|
+
]);
|
|
3795
4216
|
|
|
3796
4217
|
const propertyNameById = new Map(
|
|
3797
4218
|
propertyDefs.map((row) => [row.id, row.name]),
|
|
@@ -3844,6 +4265,7 @@ async function loadSourceSnapshot(
|
|
|
3844
4265
|
allDocumentIds,
|
|
3845
4266
|
rowDocuments,
|
|
3846
4267
|
propertyValueRows,
|
|
4268
|
+
consistencyAttempts,
|
|
3847
4269
|
} = await loadSourceSnapshotRowsOptimistically({
|
|
3848
4270
|
source,
|
|
3849
4271
|
database,
|
|
@@ -4224,6 +4646,10 @@ async function loadSourceSnapshot(
|
|
|
4224
4646
|
fields,
|
|
4225
4647
|
rows,
|
|
4226
4648
|
changeSets,
|
|
4649
|
+
projection:
|
|
4650
|
+
options.documentIds !== undefined
|
|
4651
|
+
? { rows: "page", changeSets: "page" }
|
|
4652
|
+
: { rows: "complete", changeSets: "complete" },
|
|
4227
4653
|
bodyHydration,
|
|
4228
4654
|
};
|
|
4229
4655
|
}
|
|
@@ -5640,10 +6066,11 @@ export async function importBuilderCmsEntriesAsDatabaseItems(args: {
|
|
|
5640
6066
|
}): Promise<{
|
|
5641
6067
|
imported: number;
|
|
5642
6068
|
importedEntriesByDocumentId: Map<string, BuilderCmsSourceEntry>;
|
|
6069
|
+
importedItems: ContentDatabaseItem[];
|
|
5643
6070
|
}> {
|
|
5644
6071
|
const importedEntriesByDocumentId = new Map<string, BuilderCmsSourceEntry>();
|
|
5645
6072
|
if (args.entries.length === 0) {
|
|
5646
|
-
return { imported: 0, importedEntriesByDocumentId };
|
|
6073
|
+
return { imported: 0, importedEntriesByDocumentId, importedItems: [] };
|
|
5647
6074
|
}
|
|
5648
6075
|
const db = getDb();
|
|
5649
6076
|
const [databaseDocument] = await db
|
|
@@ -5732,6 +6159,9 @@ export async function importBuilderCmsEntriesAsDatabaseItems(args: {
|
|
|
5732
6159
|
.filter((row) => !representedDocumentIds.has(row.document.id))
|
|
5733
6160
|
.map((row) => row.document.title.trim().toLowerCase()),
|
|
5734
6161
|
);
|
|
6162
|
+
const currentItemByDocumentId = new Map(
|
|
6163
|
+
currentItems.map((row) => [row.document.id, row.item]),
|
|
6164
|
+
);
|
|
5735
6165
|
|
|
5736
6166
|
// Reads MAX(position) for both `documents` and `content_database_items`
|
|
5737
6167
|
// then batch-inserts at MAX+1.. — serialize the whole read-through-write
|
|
@@ -5777,26 +6207,29 @@ export async function importBuilderCmsEntriesAsDatabaseItems(args: {
|
|
|
5777
6207
|
continue;
|
|
5778
6208
|
}
|
|
5779
6209
|
|
|
6210
|
+
const { documentId, itemId } = builderCmsImportIds({
|
|
6211
|
+
ownerEmail: args.database.ownerEmail,
|
|
6212
|
+
databaseId: args.database.id,
|
|
6213
|
+
sourceTable: args.sourceTable,
|
|
6214
|
+
entryId: entry.id,
|
|
6215
|
+
});
|
|
6216
|
+
const existingDeterministicItem =
|
|
6217
|
+
currentItemByDocumentId.get(documentId);
|
|
6218
|
+
if (existingDeterministicItem?.id === itemId) {
|
|
6219
|
+
// A prior attach can commit the deterministic document/item and
|
|
6220
|
+
// fail before linking its source row. Treat that pair as the
|
|
6221
|
+
// same Builder identity so refresh repairs the missing link
|
|
6222
|
+
// without synthesizing a duplicate response item.
|
|
6223
|
+
importedEntriesByDocumentId.set(documentId, entry);
|
|
6224
|
+
continue;
|
|
6225
|
+
}
|
|
6226
|
+
|
|
5780
6227
|
const title = entry.title.trim() || entry.id;
|
|
5781
6228
|
const titleKey = title.toLowerCase();
|
|
5782
6229
|
if (!args.skipTitleDedup && existingUnlinkedTitles.has(titleKey)) {
|
|
5783
6230
|
continue;
|
|
5784
6231
|
}
|
|
5785
6232
|
|
|
5786
|
-
const importIdentity = [
|
|
5787
|
-
args.database.ownerEmail,
|
|
5788
|
-
args.database.id,
|
|
5789
|
-
args.sourceTable,
|
|
5790
|
-
entry.id,
|
|
5791
|
-
];
|
|
5792
|
-
const documentId = stableBuilderImportId(
|
|
5793
|
-
"builder-doc",
|
|
5794
|
-
importIdentity,
|
|
5795
|
-
);
|
|
5796
|
-
const itemId = stableBuilderImportId(
|
|
5797
|
-
"builder-item",
|
|
5798
|
-
importIdentity,
|
|
5799
|
-
);
|
|
5800
6233
|
documentRows.push({
|
|
5801
6234
|
id: documentId,
|
|
5802
6235
|
spaceId: databaseSpaceId,
|
|
@@ -5827,6 +6260,7 @@ export async function importBuilderCmsEntriesAsDatabaseItems(args: {
|
|
|
5827
6260
|
});
|
|
5828
6261
|
importedEntriesByDocumentId.set(documentId, entry);
|
|
5829
6262
|
}
|
|
6263
|
+
const insertedItemIds = new Set<string>();
|
|
5830
6264
|
await db.transaction(async (tx) => {
|
|
5831
6265
|
for (const chunk of chunks(
|
|
5832
6266
|
documentRows,
|
|
@@ -5841,10 +6275,14 @@ export async function importBuilderCmsEntriesAsDatabaseItems(args: {
|
|
|
5841
6275
|
itemRows,
|
|
5842
6276
|
bulkChunkSizeForColumnCount(10),
|
|
5843
6277
|
)) {
|
|
5844
|
-
await tx
|
|
6278
|
+
const insertedItems = await tx
|
|
5845
6279
|
.insert(schema.contentDatabaseItems)
|
|
5846
6280
|
.values(chunk)
|
|
5847
|
-
.onConflictDoNothing()
|
|
6281
|
+
.onConflictDoNothing()
|
|
6282
|
+
.returning({ id: schema.contentDatabaseItems.id });
|
|
6283
|
+
for (const item of insertedItems) {
|
|
6284
|
+
insertedItemIds.add(item.id);
|
|
6285
|
+
}
|
|
5848
6286
|
}
|
|
5849
6287
|
await ensureDocumentsFilesMembership(
|
|
5850
6288
|
tx,
|
|
@@ -5854,7 +6292,47 @@ export async function importBuilderCmsEntriesAsDatabaseItems(args: {
|
|
|
5854
6292
|
);
|
|
5855
6293
|
});
|
|
5856
6294
|
|
|
5857
|
-
|
|
6295
|
+
const candidateDocumentById = new Map(
|
|
6296
|
+
documentRows.map((document) => [document.id!, document]),
|
|
6297
|
+
);
|
|
6298
|
+
const importedItems: ContentDatabaseItem[] = itemRows
|
|
6299
|
+
.filter((item) => insertedItemIds.has(item.id!))
|
|
6300
|
+
.map((item) => {
|
|
6301
|
+
const document = candidateDocumentById.get(item.documentId)!;
|
|
6302
|
+
return {
|
|
6303
|
+
id: item.id!,
|
|
6304
|
+
databaseId: args.database.id,
|
|
6305
|
+
document: {
|
|
6306
|
+
id: document.id!,
|
|
6307
|
+
parentId: document.parentId ?? null,
|
|
6308
|
+
title: document.title ?? "Untitled",
|
|
6309
|
+
content: "",
|
|
6310
|
+
icon: document.icon ?? null,
|
|
6311
|
+
position: document.position ?? 0,
|
|
6312
|
+
isFavorite: false,
|
|
6313
|
+
hideFromSearch: Boolean(document.hideFromSearch),
|
|
6314
|
+
visibility: document.visibility ?? "private",
|
|
6315
|
+
accessRole: "owner",
|
|
6316
|
+
canEdit: true,
|
|
6317
|
+
canManage: true,
|
|
6318
|
+
createdAt: document.createdAt!,
|
|
6319
|
+
updatedAt: document.updatedAt!,
|
|
6320
|
+
},
|
|
6321
|
+
position: item.position ?? 0,
|
|
6322
|
+
properties: [],
|
|
6323
|
+
bodyHydration: {
|
|
6324
|
+
status: "pending",
|
|
6325
|
+
attemptedAt: null,
|
|
6326
|
+
error: null,
|
|
6327
|
+
version: null,
|
|
6328
|
+
},
|
|
6329
|
+
};
|
|
6330
|
+
});
|
|
6331
|
+
return {
|
|
6332
|
+
imported: insertedItemIds.size,
|
|
6333
|
+
importedEntriesByDocumentId,
|
|
6334
|
+
importedItems,
|
|
6335
|
+
};
|
|
5858
6336
|
},
|
|
5859
6337
|
),
|
|
5860
6338
|
);
|
|
@@ -5865,9 +6343,10 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
5865
6343
|
source: ContentDatabaseSourceRowDb;
|
|
5866
6344
|
now: string;
|
|
5867
6345
|
runFullRefresh?: boolean;
|
|
6346
|
+
finishPagination?: boolean;
|
|
5868
6347
|
refreshClaimId?: string;
|
|
5869
6348
|
}) {
|
|
5870
|
-
|
|
6349
|
+
const setupPromise = sourceSetupPayload(args.database.id);
|
|
5871
6350
|
const db = getDb();
|
|
5872
6351
|
const sourceMetadata =
|
|
5873
6352
|
parseObject<SourceMetadataRecord>(args.source.metadataJson) ?? {};
|
|
@@ -5877,7 +6356,7 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
5877
6356
|
? sourceMetadata.activeReadSourceRowIds
|
|
5878
6357
|
: [];
|
|
5879
6358
|
const continueOffset =
|
|
5880
|
-
!args.runFullRefresh &&
|
|
6359
|
+
(!args.runFullRefresh || args.finishPagination) &&
|
|
5881
6360
|
sourceMetadata.sourceFetchState === "fetching" &&
|
|
5882
6361
|
sourceMetadata.lastReadHasMore === true &&
|
|
5883
6362
|
activeReadSourceRowIds.length > 0 &&
|
|
@@ -5891,25 +6370,33 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
5891
6370
|
.where(eq(schema.contentDatabaseSourceFields.sourceId, args.source.id));
|
|
5892
6371
|
let builderModelFields: BuilderCmsModelFieldSummary[] | undefined;
|
|
5893
6372
|
let builderModelFieldsReadFailed = false;
|
|
5894
|
-
|
|
5895
|
-
builderModelFields =
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
6373
|
+
if (continueOffset > 0 && sourceMetadata.builderModelFields?.length) {
|
|
6374
|
+
builderModelFields = sourceMetadata.builderModelFields;
|
|
6375
|
+
} else {
|
|
6376
|
+
try {
|
|
6377
|
+
builderModelFields = await readBuilderCmsModelFields({
|
|
6378
|
+
model: args.source.sourceTable,
|
|
6379
|
+
});
|
|
6380
|
+
builderModelFields = mergeBuilderCmsModelFieldsPreservingReferenceModels({
|
|
6381
|
+
existing: sourceMetadata.builderModelFields,
|
|
6382
|
+
refreshed: builderModelFields,
|
|
6383
|
+
});
|
|
6384
|
+
} catch (error) {
|
|
6385
|
+
builderModelFieldsReadFailed = true;
|
|
6386
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6387
|
+
console.warn(
|
|
6388
|
+
`[content] Builder model field read failed for ${args.source.sourceTable}; continuing source row sync without model field metadata. ${message}`,
|
|
6389
|
+
);
|
|
6390
|
+
}
|
|
5908
6391
|
}
|
|
5909
6392
|
const projectionModelFields =
|
|
5910
6393
|
builderModelFields && builderModelFields.length > 0
|
|
5911
6394
|
? builderModelFields
|
|
5912
6395
|
: (sourceMetadata.builderModelFields ?? []);
|
|
6396
|
+
const existingRowsPromise = db
|
|
6397
|
+
.select()
|
|
6398
|
+
.from(schema.contentDatabaseSourceRows)
|
|
6399
|
+
.where(eq(schema.contentDatabaseSourceRows.sourceId, args.source.id));
|
|
5913
6400
|
const builderRead = await readBuilderCmsContentEntries({
|
|
5914
6401
|
model: args.source.sourceTable,
|
|
5915
6402
|
fieldPaths: [
|
|
@@ -5945,10 +6432,9 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
5945
6432
|
"Builder source refresh claim was lost before snapshot mutation.",
|
|
5946
6433
|
);
|
|
5947
6434
|
}
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
.where(eq(schema.contentDatabaseSourceRows.sourceId, args.source.id));
|
|
6435
|
+
const { properties, response: initialResponse } = await setupPromise;
|
|
6436
|
+
let response = initialResponse;
|
|
6437
|
+
const existingRows = await existingRowsPromise;
|
|
5952
6438
|
const readStartOffset = builderRead.progress?.startOffset ?? 0;
|
|
5953
6439
|
const activeReadSourceRowIdSet = new Set(activeReadSourceRowIds);
|
|
5954
6440
|
const suspiciousEmptyRead =
|
|
@@ -5995,11 +6481,14 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
|
|
|
5995
6481
|
});
|
|
5996
6482
|
importedEntriesByDocumentId = importResult.importedEntriesByDocumentId;
|
|
5997
6483
|
if (importResult.imported > 0) {
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
.
|
|
6001
|
-
|
|
6002
|
-
|
|
6484
|
+
const existingItemIds = new Set(response.items.map((item) => item.id));
|
|
6485
|
+
const importedItems = importResult.importedItems.filter(
|
|
6486
|
+
(item) => !existingItemIds.has(item.id),
|
|
6487
|
+
);
|
|
6488
|
+
response = {
|
|
6489
|
+
...response,
|
|
6490
|
+
items: [...response.items, ...importedItems],
|
|
6491
|
+
};
|
|
6003
6492
|
}
|
|
6004
6493
|
}
|
|
6005
6494
|
const builderEntriesByDocumentId =
|
|
@@ -6971,15 +7460,24 @@ export async function getSourceRows(sourceId: string) {
|
|
|
6971
7460
|
.where(eq(schema.contentDatabaseSourceRows.sourceId, sourceId));
|
|
6972
7461
|
}
|
|
6973
7462
|
|
|
6974
|
-
export async function listDatabasePropertiesAndItems(
|
|
7463
|
+
export async function listDatabasePropertiesAndItems(
|
|
7464
|
+
databaseId: string,
|
|
7465
|
+
options: { limit?: number; offset?: number; documentIds?: string[] } = {},
|
|
7466
|
+
) {
|
|
6975
7467
|
const { getContentDatabaseResponse } = await import("./_database-utils.js");
|
|
6976
|
-
return getContentDatabaseResponse(databaseId
|
|
7468
|
+
return getContentDatabaseResponse(databaseId, {
|
|
7469
|
+
...options,
|
|
7470
|
+
includeSources: false,
|
|
7471
|
+
});
|
|
6977
7472
|
}
|
|
6978
7473
|
|
|
6979
|
-
export async function sourceSetupPayload(
|
|
7474
|
+
export async function sourceSetupPayload(
|
|
7475
|
+
databaseId: string,
|
|
7476
|
+
options: { limit?: number; offset?: number; documentIds?: string[] } = {},
|
|
7477
|
+
) {
|
|
6980
7478
|
const [properties, response] = await Promise.all([
|
|
6981
7479
|
listPropertiesForDatabase(databaseId),
|
|
6982
|
-
listDatabasePropertiesAndItems(databaseId),
|
|
7480
|
+
listDatabasePropertiesAndItems(databaseId, options),
|
|
6983
7481
|
]);
|
|
6984
7482
|
return { properties, response };
|
|
6985
7483
|
}
|