@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
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
parseDocumentHideFromSearch,
|
|
7
7
|
} from "../server/lib/documents.js";
|
|
8
8
|
import type {
|
|
9
|
+
ContentDatabaseBodyHydration,
|
|
9
10
|
ContentDatabaseMembership,
|
|
10
11
|
ContentDatabaseResponse,
|
|
11
12
|
} from "../shared/api.js";
|
|
@@ -28,8 +29,27 @@ function canManageRole(role: string) {
|
|
|
28
29
|
type DatabaseMembershipRow = {
|
|
29
30
|
item: typeof schema.contentDatabaseItems.$inferSelect;
|
|
30
31
|
database: typeof schema.contentDatabases.$inferSelect;
|
|
32
|
+
sourceId?: string | null;
|
|
31
33
|
};
|
|
32
34
|
|
|
35
|
+
export function serializeBodyHydration(
|
|
36
|
+
item: typeof schema.contentDatabaseItems.$inferSelect,
|
|
37
|
+
): ContentDatabaseBodyHydration {
|
|
38
|
+
const status = item.bodyHydrationStatus;
|
|
39
|
+
return {
|
|
40
|
+
status:
|
|
41
|
+
status === "pending" ||
|
|
42
|
+
status === "hydrating" ||
|
|
43
|
+
status === "hydrated" ||
|
|
44
|
+
status === "error"
|
|
45
|
+
? status
|
|
46
|
+
: "hydrated",
|
|
47
|
+
attemptedAt: item.bodyHydrationAttemptedAt,
|
|
48
|
+
error: item.bodyHydrationError,
|
|
49
|
+
version: item.bodyHydrationVersion,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
export function serializeDatabaseMembership(
|
|
34
54
|
row: DatabaseMembershipRow,
|
|
35
55
|
): ContentDatabaseMembership {
|
|
@@ -38,6 +58,8 @@ export function serializeDatabaseMembership(
|
|
|
38
58
|
databaseDocumentId: row.database.documentId,
|
|
39
59
|
databaseTitle: row.database.title || "Untitled database",
|
|
40
60
|
position: row.item.position,
|
|
61
|
+
sourceId: row.sourceId ?? null,
|
|
62
|
+
bodyHydration: serializeBodyHydration(row.item),
|
|
41
63
|
};
|
|
42
64
|
}
|
|
43
65
|
|
|
@@ -178,6 +200,7 @@ export async function getContentDatabaseResponse(
|
|
|
178
200
|
databaseId: item.databaseId,
|
|
179
201
|
document: serializeDocument(document, { item, database }),
|
|
180
202
|
position: item.position,
|
|
203
|
+
bodyHydration: serializeBodyHydration(item),
|
|
181
204
|
properties: await listPropertiesForDatabase(databaseId, document),
|
|
182
205
|
});
|
|
183
206
|
}
|
|
@@ -288,12 +311,20 @@ export async function getDatabaseItemByDocumentId(
|
|
|
288
311
|
.select({
|
|
289
312
|
item: schema.contentDatabaseItems,
|
|
290
313
|
database: schema.contentDatabases,
|
|
314
|
+
sourceId: schema.contentDatabaseSourceRows.sourceId,
|
|
291
315
|
})
|
|
292
316
|
.from(schema.contentDatabaseItems)
|
|
293
317
|
.innerJoin(
|
|
294
318
|
schema.contentDatabases,
|
|
295
319
|
eq(schema.contentDatabases.id, schema.contentDatabaseItems.databaseId),
|
|
296
320
|
)
|
|
321
|
+
.leftJoin(
|
|
322
|
+
schema.contentDatabaseSourceRows,
|
|
323
|
+
eq(
|
|
324
|
+
schema.contentDatabaseSourceRows.databaseItemId,
|
|
325
|
+
schema.contentDatabaseItems.id,
|
|
326
|
+
),
|
|
327
|
+
)
|
|
297
328
|
.where(and(...clauses));
|
|
298
329
|
return row ?? null;
|
|
299
330
|
}
|
|
@@ -327,6 +358,11 @@ export async function deleteDatabaseDataForDocument(
|
|
|
327
358
|
.from(schema.contentDatabaseSources)
|
|
328
359
|
.where(eq(schema.contentDatabaseSources.databaseId, database.id));
|
|
329
360
|
for (const source of sources) {
|
|
361
|
+
await db
|
|
362
|
+
.delete(schema.contentDatabaseBodyHydrationQueue)
|
|
363
|
+
.where(
|
|
364
|
+
eq(schema.contentDatabaseBodyHydrationQueue.sourceId, source.id),
|
|
365
|
+
);
|
|
330
366
|
await db
|
|
331
367
|
.delete(schema.contentDatabaseSourceExecutions)
|
|
332
368
|
.where(eq(schema.contentDatabaseSourceExecutions.sourceId, source.id));
|
|
@@ -363,6 +399,11 @@ export async function deleteDatabaseDataForDocument(
|
|
|
363
399
|
includeDeleted: true,
|
|
364
400
|
});
|
|
365
401
|
if (item) {
|
|
402
|
+
await db
|
|
403
|
+
.delete(schema.contentDatabaseBodyHydrationQueue)
|
|
404
|
+
.where(
|
|
405
|
+
eq(schema.contentDatabaseBodyHydrationQueue.documentId, documentId),
|
|
406
|
+
);
|
|
366
407
|
await db
|
|
367
408
|
.delete(schema.documentPropertyValues)
|
|
368
409
|
.where(
|
|
@@ -225,8 +225,71 @@ function folderDocumentsForFiles(files: LocalArtifactFileMeta[]) {
|
|
|
225
225
|
.map((folderPath, index) => documentFromFolder(folderPath, index));
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
let localFileDocumentsCache: {
|
|
229
|
+
scope: string;
|
|
230
|
+
signature: string;
|
|
231
|
+
documents: Document[];
|
|
232
|
+
cachedAt: number;
|
|
233
|
+
} | null = null;
|
|
234
|
+
const LOCAL_FILE_DOCUMENTS_CACHE_TTL_MS = 5_000;
|
|
235
|
+
|
|
236
|
+
function localFileDocumentsCacheScope() {
|
|
237
|
+
return JSON.stringify({
|
|
238
|
+
manifest: process.env.AGENT_NATIVE_MANIFEST,
|
|
239
|
+
manifestPath: process.env.AGENT_NATIVE_MANIFEST_PATH,
|
|
240
|
+
mode: process.env.AGENT_NATIVE_MODE,
|
|
241
|
+
dataMode: process.env.AGENT_NATIVE_DATA_MODE,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function localFileDocumentsSignature(files: LocalArtifactFileMeta[]) {
|
|
246
|
+
return JSON.stringify(
|
|
247
|
+
files.map((file) => ({
|
|
248
|
+
absolutePath: file.absolutePath,
|
|
249
|
+
contentType: file.contentType,
|
|
250
|
+
hash: file.hash,
|
|
251
|
+
path: file.path,
|
|
252
|
+
profile: file.profile,
|
|
253
|
+
rootName: file.rootName,
|
|
254
|
+
rootPath: file.rootPath,
|
|
255
|
+
sizeBytes: file.sizeBytes,
|
|
256
|
+
updatedAt: file.updatedAt,
|
|
257
|
+
})),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function cloneLocalDocument(document: Document): Document {
|
|
262
|
+
return {
|
|
263
|
+
...document,
|
|
264
|
+
source: document.source ? { ...document.source } : undefined,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function invalidateLocalFileDocumentsCache() {
|
|
269
|
+
localFileDocumentsCache = null;
|
|
270
|
+
}
|
|
271
|
+
|
|
228
272
|
export async function listLocalFileDocuments(): Promise<Document[]> {
|
|
273
|
+
const scope = localFileDocumentsCacheScope();
|
|
274
|
+
if (
|
|
275
|
+
localFileDocumentsCache &&
|
|
276
|
+
localFileDocumentsCache.scope === scope &&
|
|
277
|
+
Date.now() - localFileDocumentsCache.cachedAt <
|
|
278
|
+
LOCAL_FILE_DOCUMENTS_CACHE_TTL_MS
|
|
279
|
+
) {
|
|
280
|
+
return localFileDocumentsCache.documents.map(cloneLocalDocument);
|
|
281
|
+
}
|
|
282
|
+
|
|
229
283
|
const files = await listLocalArtifactFiles(localOptions());
|
|
284
|
+
const signature = localFileDocumentsSignature(files);
|
|
285
|
+
if (
|
|
286
|
+
localFileDocumentsCache?.scope === scope &&
|
|
287
|
+
localFileDocumentsCache.signature === signature
|
|
288
|
+
) {
|
|
289
|
+
localFileDocumentsCache.cachedAt = Date.now();
|
|
290
|
+
return localFileDocumentsCache.documents.map(cloneLocalDocument);
|
|
291
|
+
}
|
|
292
|
+
|
|
230
293
|
const folderDocuments = folderDocumentsForFiles(files);
|
|
231
294
|
const fileDocuments = await Promise.all(
|
|
232
295
|
files.map(async (meta, index) => {
|
|
@@ -237,7 +300,14 @@ export async function listLocalFileDocuments(): Promise<Document[]> {
|
|
|
237
300
|
return documentFromLocalFile(file ?? meta, file?.content ?? "", index);
|
|
238
301
|
}),
|
|
239
302
|
);
|
|
240
|
-
|
|
303
|
+
const documents = [...folderDocuments, ...fileDocuments];
|
|
304
|
+
localFileDocumentsCache = {
|
|
305
|
+
scope,
|
|
306
|
+
signature,
|
|
307
|
+
documents,
|
|
308
|
+
cachedAt: Date.now(),
|
|
309
|
+
};
|
|
310
|
+
return documents.map(cloneLocalDocument);
|
|
241
311
|
}
|
|
242
312
|
|
|
243
313
|
export async function getLocalFileDocument(id: string): Promise<Document> {
|
|
@@ -442,6 +512,7 @@ export async function updateLocalFileDocument(
|
|
|
442
512
|
content: nextSource,
|
|
443
513
|
expectedHash: file.hash,
|
|
444
514
|
});
|
|
515
|
+
invalidateLocalFileDocumentsCache();
|
|
445
516
|
return getLocalFileDocument(id);
|
|
446
517
|
}
|
|
447
518
|
|
|
@@ -505,6 +576,7 @@ export async function createLocalFileDocument(
|
|
|
505
576
|
content: source,
|
|
506
577
|
ifNotExists: true,
|
|
507
578
|
});
|
|
579
|
+
invalidateLocalFileDocumentsCache();
|
|
508
580
|
return getLocalFileDocument(localFileDocumentId(path));
|
|
509
581
|
} catch (error) {
|
|
510
582
|
if (isAlreadyExistsError(error)) continue;
|
|
@@ -521,6 +593,7 @@ export async function deleteLocalFileDocument(id: string) {
|
|
|
521
593
|
}
|
|
522
594
|
const path = localDocumentPathFromId(id);
|
|
523
595
|
await deleteLocalArtifactFile({ ...localOptions(), path });
|
|
596
|
+
invalidateLocalFileDocumentsCache();
|
|
524
597
|
return { success: true, deleted: 1 };
|
|
525
598
|
}
|
|
526
599
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import type { BuilderCmsSourceEntry } from "./_builder-cms-source-adapter.js";
|
|
17
17
|
import {
|
|
18
18
|
databaseSourceExistsForTable,
|
|
19
|
+
enqueueBuilderBodyHydrationForItems,
|
|
19
20
|
ensureDatabaseSourceProperty,
|
|
20
21
|
getExistingSource,
|
|
21
22
|
getSourceRows,
|
|
@@ -265,6 +266,8 @@ export default defineAction({
|
|
|
265
266
|
const additionalRead = await readBuilderCmsContentEntries({
|
|
266
267
|
model: sourceTable,
|
|
267
268
|
});
|
|
269
|
+
const additionalEntries =
|
|
270
|
+
additionalRead.state === "live" ? additionalRead.entries : [];
|
|
268
271
|
const additionalModelFields = await readBuilderCmsModelFields({
|
|
269
272
|
model: sourceTable,
|
|
270
273
|
});
|
|
@@ -284,7 +287,7 @@ export default defineAction({
|
|
|
284
287
|
if (additionalRead.state === "live") {
|
|
285
288
|
await importBuilderCmsEntriesAsDatabaseItems({
|
|
286
289
|
database,
|
|
287
|
-
entries:
|
|
290
|
+
entries: additionalEntries,
|
|
288
291
|
now,
|
|
289
292
|
sourceTable,
|
|
290
293
|
existingSourceRows: [],
|
|
@@ -299,7 +302,7 @@ export default defineAction({
|
|
|
299
302
|
const additionalEntriesByDocumentId =
|
|
300
303
|
additionalRead.state === "live"
|
|
301
304
|
? mapBuilderCmsEntriesToLocalItems({
|
|
302
|
-
entries:
|
|
305
|
+
entries: additionalEntries,
|
|
303
306
|
items: importedItems,
|
|
304
307
|
sourceTable,
|
|
305
308
|
now,
|
|
@@ -312,8 +315,7 @@ export default defineAction({
|
|
|
312
315
|
sourceType,
|
|
313
316
|
properties: additionalSetup.properties,
|
|
314
317
|
builderModelFields: additionalModelFields,
|
|
315
|
-
builderSampleEntries:
|
|
316
|
-
additionalRead.state === "live" ? additionalRead.entries : [],
|
|
318
|
+
builderSampleEntries: additionalEntries,
|
|
317
319
|
now,
|
|
318
320
|
});
|
|
319
321
|
await seedMockSourceRows({
|
|
@@ -325,6 +327,17 @@ export default defineAction({
|
|
|
325
327
|
now,
|
|
326
328
|
builderEntriesByDocumentId: additionalEntriesByDocumentId,
|
|
327
329
|
});
|
|
330
|
+
if (additionalRead.state === "live") {
|
|
331
|
+
await enqueueBuilderBodyHydrationForItems({
|
|
332
|
+
sourceId: additionalSourceId,
|
|
333
|
+
ownerEmail: database.ownerEmail,
|
|
334
|
+
orgId: database.orgId,
|
|
335
|
+
sourceTable,
|
|
336
|
+
items: importedItems,
|
|
337
|
+
builderEntriesByDocumentId: additionalEntriesByDocumentId,
|
|
338
|
+
now,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
328
341
|
await updateBuilderCmsSourceReadMetadata({
|
|
329
342
|
sourceId: additionalSourceId,
|
|
330
343
|
sourceTable,
|
|
@@ -365,6 +378,8 @@ export default defineAction({
|
|
|
365
378
|
model: sourceTable,
|
|
366
379
|
})
|
|
367
380
|
: null;
|
|
381
|
+
const builderEntries =
|
|
382
|
+
builderRead?.state === "live" ? builderRead.entries : [];
|
|
368
383
|
const builderModelFields =
|
|
369
384
|
sourceType === "builder-cms"
|
|
370
385
|
? await readBuilderCmsModelFields({
|
|
@@ -374,7 +389,7 @@ export default defineAction({
|
|
|
374
389
|
if (builderRead?.state === "live") {
|
|
375
390
|
await importBuilderCmsEntriesAsDatabaseItems({
|
|
376
391
|
database,
|
|
377
|
-
entries:
|
|
392
|
+
entries: builderEntries,
|
|
378
393
|
now,
|
|
379
394
|
sourceTable,
|
|
380
395
|
existingSourceRows,
|
|
@@ -385,7 +400,7 @@ export default defineAction({
|
|
|
385
400
|
const builderEntriesByDocumentId =
|
|
386
401
|
builderRead?.state === "live"
|
|
387
402
|
? mapBuilderCmsEntriesToLocalItems({
|
|
388
|
-
entries:
|
|
403
|
+
entries: builderEntries,
|
|
389
404
|
items: refreshedSetup.response.items,
|
|
390
405
|
sourceTable,
|
|
391
406
|
now,
|
|
@@ -399,8 +414,7 @@ export default defineAction({
|
|
|
399
414
|
sourceType,
|
|
400
415
|
properties: refreshedSetup.properties,
|
|
401
416
|
builderModelFields,
|
|
402
|
-
builderSampleEntries:
|
|
403
|
-
builderRead?.state === "live" ? builderRead.entries : [],
|
|
417
|
+
builderSampleEntries: builderEntries,
|
|
404
418
|
now,
|
|
405
419
|
});
|
|
406
420
|
await seedMockSourceRows({
|
|
@@ -412,6 +426,17 @@ export default defineAction({
|
|
|
412
426
|
now,
|
|
413
427
|
builderEntriesByDocumentId,
|
|
414
428
|
});
|
|
429
|
+
if (sourceType === "builder-cms" && builderRead?.state === "live") {
|
|
430
|
+
await enqueueBuilderBodyHydrationForItems({
|
|
431
|
+
sourceId,
|
|
432
|
+
ownerEmail: database.ownerEmail,
|
|
433
|
+
orgId: database.orgId,
|
|
434
|
+
sourceTable,
|
|
435
|
+
items: refreshedSetup.response.items,
|
|
436
|
+
builderEntriesByDocumentId,
|
|
437
|
+
now,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
415
440
|
if (sourceType === "builder-cms" && builderRead) {
|
|
416
441
|
await updateBuilderCmsSourceReadMetadata({
|
|
417
442
|
sourceId,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./_builder-cms-read-client.js";
|
|
18
18
|
import type { BuilderCmsSourceEntry } from "./_builder-cms-source-adapter.js";
|
|
19
19
|
import {
|
|
20
|
+
enqueueBuilderBodyHydrationForItems,
|
|
20
21
|
ensureDatabaseSourceProperty,
|
|
21
22
|
importBuilderCmsEntriesAsDatabaseItems,
|
|
22
23
|
mapBuilderCmsEntriesToLocalItems,
|
|
@@ -383,6 +384,19 @@ export default defineAction({
|
|
|
383
384
|
now,
|
|
384
385
|
builderEntriesByDocumentId,
|
|
385
386
|
});
|
|
387
|
+
if (read.state === "live") {
|
|
388
|
+
await enqueueBuilderBodyHydrationForItems({
|
|
389
|
+
sourceId: source.id,
|
|
390
|
+
ownerEmail: database.ownerEmail,
|
|
391
|
+
orgId: database.orgId,
|
|
392
|
+
sourceTable: source.sourceTable,
|
|
393
|
+
items: refreshedSetup.response.items.filter((item) =>
|
|
394
|
+
builderEntriesByDocumentId.has(item.document.id),
|
|
395
|
+
),
|
|
396
|
+
builderEntriesByDocumentId,
|
|
397
|
+
now,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
386
400
|
await updateBuilderCmsSourceReadMetadata({
|
|
387
401
|
sourceId: source.id,
|
|
388
402
|
sourceTable: source.sourceTable,
|
|
@@ -16,6 +16,9 @@ import { getContentDatabaseResponse } from "./_database-utils.js";
|
|
|
16
16
|
|
|
17
17
|
async function deleteSourceRecords(sourceId: string) {
|
|
18
18
|
const db = getDb();
|
|
19
|
+
await db
|
|
20
|
+
.delete(schema.contentDatabaseBodyHydrationQueue)
|
|
21
|
+
.where(eq(schema.contentDatabaseBodyHydrationQueue.sourceId, sourceId));
|
|
19
22
|
await db
|
|
20
23
|
.delete(schema.contentDatabaseSourceExecutions)
|
|
21
24
|
.where(eq(schema.contentDatabaseSourceExecutions.sourceId, sourceId));
|
|
@@ -11,13 +11,19 @@ import {
|
|
|
11
11
|
type ContentDatabaseSourceChangeSet,
|
|
12
12
|
type ContentDatabaseSourceExecutionState,
|
|
13
13
|
type ContentDatabaseSourcePushMode,
|
|
14
|
+
type DocumentPropertyValue,
|
|
14
15
|
type ExecuteBuilderSourceExecutionRequest,
|
|
15
16
|
} from "../shared/api.js";
|
|
16
17
|
import {
|
|
17
18
|
type BuilderCmsEntryLiveState,
|
|
18
19
|
readBuilderCmsEntryLiveState,
|
|
19
20
|
} from "./_builder-cms-read-client.js";
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
BUILDER_CMS_BODY_BLOCKS_HASH_KEY,
|
|
23
|
+
BUILDER_CMS_BODY_CONTENT_KEY,
|
|
24
|
+
BUILDER_CMS_BODY_SIDECARS_KEY,
|
|
25
|
+
builderCmsQualifiedId,
|
|
26
|
+
} from "./_builder-cms-source-adapter.js";
|
|
21
27
|
import type {
|
|
22
28
|
BuilderCmsExecutionPayload,
|
|
23
29
|
BuilderCmsExecutionPlan,
|
|
@@ -255,6 +261,47 @@ function builderCmsAuthoritativeLastUpdated(
|
|
|
255
261
|
);
|
|
256
262
|
}
|
|
257
263
|
|
|
264
|
+
function parseSourceValues(
|
|
265
|
+
value: string | null | undefined,
|
|
266
|
+
): Record<string, DocumentPropertyValue> {
|
|
267
|
+
if (!value) return {};
|
|
268
|
+
try {
|
|
269
|
+
const parsed = JSON.parse(value) as unknown;
|
|
270
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
271
|
+
? (parsed as Record<string, DocumentPropertyValue>)
|
|
272
|
+
: {};
|
|
273
|
+
} catch {
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function builderCmsReconciledSourceValuesJson(args: {
|
|
279
|
+
existingSourceValuesJson: string | null | undefined;
|
|
280
|
+
snapshotSourceValues: Record<string, DocumentPropertyValue> | undefined;
|
|
281
|
+
changeSet: ContentDatabaseSourceChangeSet;
|
|
282
|
+
}) {
|
|
283
|
+
const next = {
|
|
284
|
+
...(args.snapshotSourceValues ?? {}),
|
|
285
|
+
...parseSourceValues(args.existingSourceValuesJson),
|
|
286
|
+
};
|
|
287
|
+
for (const field of args.changeSet.fieldChanges) {
|
|
288
|
+
next[field.sourceFieldKey] = field.proposedValue;
|
|
289
|
+
}
|
|
290
|
+
const bodyChange = args.changeSet.bodyChange;
|
|
291
|
+
if (bodyChange) {
|
|
292
|
+
if (bodyChange.proposedHash !== undefined) {
|
|
293
|
+
next[BUILDER_CMS_BODY_BLOCKS_HASH_KEY] = bodyChange.proposedHash;
|
|
294
|
+
}
|
|
295
|
+
if (bodyChange.proposedContent !== undefined) {
|
|
296
|
+
next[BUILDER_CMS_BODY_CONTENT_KEY] = bodyChange.proposedContent;
|
|
297
|
+
}
|
|
298
|
+
if (bodyChange.sidecarsJson !== undefined) {
|
|
299
|
+
next[BUILDER_CMS_BODY_SIDECARS_KEY] = bodyChange.sidecarsJson;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return JSON.stringify(next);
|
|
303
|
+
}
|
|
304
|
+
|
|
258
305
|
function sourceRowForChangeSet(
|
|
259
306
|
source: ContentDatabaseSource,
|
|
260
307
|
changeSet: ContentDatabaseSourceChangeSet,
|
|
@@ -270,6 +317,7 @@ function sourceRowForChangeSet(
|
|
|
270
317
|
|
|
271
318
|
function requiresLivePreflight(effect: BuilderCmsExecutionPayload["effect"]) {
|
|
272
319
|
return (
|
|
320
|
+
effect === "autosave" ||
|
|
273
321
|
effect === "update_in_place" ||
|
|
274
322
|
effect === "publish" ||
|
|
275
323
|
effect === "unpublish"
|
|
@@ -314,6 +362,7 @@ function liveTimestampsDiffer(args: {
|
|
|
314
362
|
function livePreflightBlockMessage(args: {
|
|
315
363
|
liveState: BuilderCmsEntryLiveState;
|
|
316
364
|
baselineLastUpdated: string | null;
|
|
365
|
+
baselineBlocksHash: string | null;
|
|
317
366
|
effect: BuilderCmsExecutionPayload["effect"];
|
|
318
367
|
}) {
|
|
319
368
|
if (!args.liveState.exists) {
|
|
@@ -327,6 +376,13 @@ function livePreflightBlockMessage(args: {
|
|
|
327
376
|
) {
|
|
328
377
|
return "Builder entry changed since this diff was approved; refresh and re-review.";
|
|
329
378
|
}
|
|
379
|
+
if (
|
|
380
|
+
args.baselineBlocksHash &&
|
|
381
|
+
args.liveState.blocksHash &&
|
|
382
|
+
args.baselineBlocksHash !== args.liveState.blocksHash
|
|
383
|
+
) {
|
|
384
|
+
return "Builder body changed since this diff was approved; refresh and re-review.";
|
|
385
|
+
}
|
|
330
386
|
if (args.effect === "publish" && args.liveState.published !== "draft") {
|
|
331
387
|
return "Entry is already published.";
|
|
332
388
|
}
|
|
@@ -415,10 +471,20 @@ async function reconcileBuilderCmsWrite(args: {
|
|
|
415
471
|
: [];
|
|
416
472
|
|
|
417
473
|
const [row] = existingRow;
|
|
474
|
+
const snapshotRow = sourceRowForChangeSet(args.source, args.changeSet);
|
|
475
|
+
const sourceValuesJson = builderCmsReconciledSourceValuesJson({
|
|
476
|
+
existingSourceValuesJson: row?.sourceValuesJson,
|
|
477
|
+
snapshotSourceValues: snapshotRow?.sourceValues,
|
|
478
|
+
changeSet: args.changeSet,
|
|
479
|
+
});
|
|
480
|
+
const patchWithValues = {
|
|
481
|
+
...patch,
|
|
482
|
+
sourceValuesJson,
|
|
483
|
+
};
|
|
418
484
|
if (row) {
|
|
419
485
|
await db
|
|
420
486
|
.update(schema.contentDatabaseSourceRows)
|
|
421
|
-
.set(
|
|
487
|
+
.set(patchWithValues)
|
|
422
488
|
.where(eq(schema.contentDatabaseSourceRows.id, row.id));
|
|
423
489
|
} else if (args.changeSet.documentId && args.changeSet.databaseItemId) {
|
|
424
490
|
await db.insert(schema.contentDatabaseSourceRows).values({
|
|
@@ -428,7 +494,7 @@ async function reconcileBuilderCmsWrite(args: {
|
|
|
428
494
|
databaseItemId: args.changeSet.databaseItemId,
|
|
429
495
|
documentId: args.changeSet.documentId,
|
|
430
496
|
createdAt: args.now,
|
|
431
|
-
...
|
|
497
|
+
...patchWithValues,
|
|
432
498
|
});
|
|
433
499
|
} else {
|
|
434
500
|
throw new Error(
|
|
@@ -763,6 +829,7 @@ export async function executeBuilderSourceExecutionWithDeps(
|
|
|
763
829
|
const message = livePreflightBlockMessage({
|
|
764
830
|
liveState,
|
|
765
831
|
baselineLastUpdated: targetRow?.lastSourceUpdatedAt ?? null,
|
|
832
|
+
baselineBlocksHash: changeSet.bodyChange?.currentHash ?? null,
|
|
766
833
|
effect: plan.payload.effect,
|
|
767
834
|
});
|
|
768
835
|
if (message) {
|
|
@@ -777,6 +844,7 @@ export async function executeBuilderSourceExecutionWithDeps(
|
|
|
777
844
|
exists: liveState.exists,
|
|
778
845
|
published: liveState.published,
|
|
779
846
|
lastUpdated: liveState.lastUpdated,
|
|
847
|
+
blocksHash: liveState.blocksHash,
|
|
780
848
|
id: liveState.id,
|
|
781
849
|
},
|
|
782
850
|
},
|
|
@@ -1,70 +1,78 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { accessFilter } from "@agent-native/core/sharing";
|
|
3
|
-
import { and, asc,
|
|
3
|
+
import { and, asc, eq, isNull, ne, sql } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
7
|
import { documentDiscoveryFilter } from "../server/lib/documents.js";
|
|
8
8
|
import type { ListContentDatabasesResponse } from "../shared/api.js";
|
|
9
9
|
|
|
10
|
+
function escapeLike(s: string): string {
|
|
11
|
+
return s.replace(/([\\%_])/g, "\\$1");
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
export default defineAction({
|
|
11
15
|
description:
|
|
12
|
-
"List the content databases the user can access (owned, shared, or org-shared — matching the sidebar) so any of them can be used as a local-table source. Optionally excludes one database (e.g. the one being configured).",
|
|
16
|
+
"List the content databases the user can access (owned, shared, or org-shared — matching the sidebar) so any of them can be used as a local-table source. Optionally filters by title or excludes one database (e.g. the one being configured).",
|
|
13
17
|
schema: z.object({
|
|
14
18
|
excludeDatabaseId: z
|
|
15
19
|
.string()
|
|
16
20
|
.optional()
|
|
17
21
|
.describe("Database id to omit from the results."),
|
|
22
|
+
query: z.string().optional().describe("Optional title search text."),
|
|
23
|
+
limit: z.coerce
|
|
24
|
+
.number()
|
|
25
|
+
.int()
|
|
26
|
+
.min(1)
|
|
27
|
+
.max(50)
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Maximum number of databases to return."),
|
|
18
30
|
}),
|
|
19
31
|
http: { method: "GET" },
|
|
20
32
|
readOnly: true,
|
|
21
33
|
run: async (args): Promise<ListContentDatabasesResponse> => {
|
|
22
34
|
const db = getDb();
|
|
35
|
+
const query = args.query?.trim();
|
|
36
|
+
const pattern = query ? `%${escapeLike(query.toLowerCase())}%` : null;
|
|
23
37
|
// The same access + discovery filter the sidebar uses, so the picker shows
|
|
24
38
|
// owned AND shared/org databases and never a trashed/hidden one.
|
|
25
|
-
const
|
|
39
|
+
const queryBuilder = db
|
|
26
40
|
.select({
|
|
27
|
-
id: schema.
|
|
41
|
+
id: schema.contentDatabases.id,
|
|
42
|
+
documentId: schema.contentDatabases.documentId,
|
|
28
43
|
title: schema.documents.title,
|
|
29
44
|
})
|
|
30
|
-
.from(schema.
|
|
45
|
+
.from(schema.contentDatabases)
|
|
46
|
+
.innerJoin(
|
|
47
|
+
schema.documents,
|
|
48
|
+
eq(schema.contentDatabases.documentId, schema.documents.id),
|
|
49
|
+
)
|
|
31
50
|
.where(
|
|
32
51
|
and(
|
|
33
52
|
accessFilter(schema.documents, schema.documentShares),
|
|
34
53
|
documentDiscoveryFilter(),
|
|
54
|
+
isNull(schema.contentDatabases.deletedAt),
|
|
55
|
+
args.excludeDatabaseId
|
|
56
|
+
? ne(schema.contentDatabases.id, args.excludeDatabaseId)
|
|
57
|
+
: undefined,
|
|
58
|
+
pattern
|
|
59
|
+
? sql`lower(${schema.documents.title}) LIKE ${pattern} ESCAPE '\\'`
|
|
60
|
+
: undefined,
|
|
35
61
|
),
|
|
36
62
|
)
|
|
37
63
|
.orderBy(asc(schema.documents.position));
|
|
38
|
-
if (accessibleDocs.length === 0) return { databases: [] };
|
|
39
64
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const rows = await db
|
|
44
|
-
.select({
|
|
45
|
-
id: schema.contentDatabases.id,
|
|
46
|
-
documentId: schema.contentDatabases.documentId,
|
|
47
|
-
})
|
|
48
|
-
.from(schema.contentDatabases)
|
|
49
|
-
.where(
|
|
50
|
-
and(
|
|
51
|
-
inArray(
|
|
52
|
-
schema.contentDatabases.documentId,
|
|
53
|
-
accessibleDocs.map((doc) => doc.id),
|
|
54
|
-
),
|
|
55
|
-
isNull(schema.contentDatabases.deletedAt),
|
|
56
|
-
),
|
|
57
|
-
);
|
|
65
|
+
const rows = args.limit
|
|
66
|
+
? await queryBuilder.limit(args.limit)
|
|
67
|
+
: await queryBuilder;
|
|
58
68
|
|
|
59
|
-
const databases = rows
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
title: titleByDocId.get(row.documentId) ?? "Untitled database",
|
|
67
|
-
}));
|
|
69
|
+
const databases = rows.map((row) => ({
|
|
70
|
+
databaseId: row.id,
|
|
71
|
+
documentId: row.documentId,
|
|
72
|
+
// The document's live title (matches the sidebar) rather than the
|
|
73
|
+
// possibly-stale content_databases.title.
|
|
74
|
+
title: row.title ?? "Untitled database",
|
|
75
|
+
}));
|
|
68
76
|
|
|
69
77
|
return { databases };
|
|
70
78
|
},
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import type {
|
|
8
|
+
ProcessBuilderBodyHydrationRequest,
|
|
9
|
+
ProcessBuilderBodyHydrationResponse,
|
|
10
|
+
} from "../shared/api.js";
|
|
11
|
+
import { processBuilderBodyHydrationQueue } from "./_database-source-utils.js";
|
|
12
|
+
|
|
13
|
+
export default defineAction({
|
|
14
|
+
description: "Hydrate queued Builder CMS body content for a database source.",
|
|
15
|
+
schema: z.object({
|
|
16
|
+
sourceId: z.string(),
|
|
17
|
+
documentId: z.string().optional(),
|
|
18
|
+
limit: z.number().int().positive().max(50).optional(),
|
|
19
|
+
}),
|
|
20
|
+
agentTool: false,
|
|
21
|
+
run: async (
|
|
22
|
+
args: ProcessBuilderBodyHydrationRequest,
|
|
23
|
+
): Promise<ProcessBuilderBodyHydrationResponse> => {
|
|
24
|
+
const db = getDb();
|
|
25
|
+
const [source] = await db
|
|
26
|
+
.select({
|
|
27
|
+
id: schema.contentDatabaseSources.id,
|
|
28
|
+
databaseDocumentId: schema.contentDatabases.documentId,
|
|
29
|
+
})
|
|
30
|
+
.from(schema.contentDatabaseSources)
|
|
31
|
+
.innerJoin(
|
|
32
|
+
schema.contentDatabases,
|
|
33
|
+
eq(
|
|
34
|
+
schema.contentDatabases.id,
|
|
35
|
+
schema.contentDatabaseSources.databaseId,
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
.where(eq(schema.contentDatabaseSources.id, args.sourceId));
|
|
39
|
+
if (!source) throw new Error(`Source "${args.sourceId}" not found`);
|
|
40
|
+
await assertAccess("document", source.databaseDocumentId, "editor");
|
|
41
|
+
return processBuilderBodyHydrationQueue({
|
|
42
|
+
sourceId: args.sourceId,
|
|
43
|
+
documentId: args.documentId,
|
|
44
|
+
limit: args.limit,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
});
|