@agent-native/core 0.135.0 → 0.135.1

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.
Files changed (36) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/sharing/access.ts +44 -2
  5. package/corpus/templates/content/actions/_database-utils.ts +113 -4
  6. package/corpus/templates/content/actions/get-document.ts +84 -3
  7. package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +9 -2
  8. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +29 -13
  9. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +5 -12
  10. package/corpus/templates/content/app/components/editor/body-hydration.ts +12 -6
  11. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2 -3
  12. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -12
  13. package/corpus/templates/content/app/hooks/use-content-database.ts +16 -27
  14. package/corpus/templates/content/app/hooks/use-create-page.ts +3 -6
  15. package/corpus/templates/content/app/hooks/use-document-properties.ts +9 -16
  16. package/corpus/templates/content/app/hooks/use-document-versions.ts +3 -3
  17. package/corpus/templates/content/app/hooks/use-documents.ts +85 -29
  18. package/corpus/templates/content/app/hooks/use-notion.ts +15 -13
  19. package/corpus/templates/content/app/i18n-data.ts +32 -1
  20. package/corpus/templates/content/app/lib/document-query.ts +36 -0
  21. package/corpus/templates/content/changelog/2026-08-02-pages-opened-from-a-database-now-keep-that-database-s-fields.md +6 -0
  22. package/corpus/templates/content/server/lib/document-context.ts +11 -6
  23. package/corpus/templates/content/shared/api.ts +8 -0
  24. package/dist/collab/struct-routes.d.ts +1 -1
  25. package/dist/notifications/routes.d.ts +3 -3
  26. package/dist/observability/routes.d.ts +3 -3
  27. package/dist/progress/routes.d.ts +1 -1
  28. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  29. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  30. package/dist/resources/handlers.d.ts +1 -1
  31. package/dist/server/transcribe-voice.d.ts +1 -1
  32. package/dist/sharing/access.d.ts.map +1 -1
  33. package/dist/sharing/access.js +32 -2
  34. package/dist/sharing/access.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/sharing/access.ts +44 -2
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1666
32
32
  - toolkit files: 169
33
- - template files: 7480
33
+ - template files: 7482
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.135.1
4
+
5
+ ### Patch Changes
6
+
7
+ - ed51b3d: Grant org-visibility access on shareable resources based on the caller's real organization membership, instead of only their currently active organization. Fixes real org members being denied access to org-shared resources (e.g. recordings) when a different org happened to be active in their session.
8
+
3
9
  ## 0.135.0
4
10
 
5
11
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.135.0",
3
+ "version": "0.135.1",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { and, eq, or, sql, type SQL } from "drizzle-orm";
17
17
 
18
+ import { orgMembers } from "../org/schema.js";
18
19
  import {
19
20
  getRequestAuthCapability,
20
21
  getRequestUserEmail,
@@ -87,6 +88,35 @@ function emailColumnMatches(column: any, email: string): SQL {
87
88
  return sql`lower(${column}) = ${email}`;
88
89
  }
89
90
 
91
+ /**
92
+ * Real `org_members` membership, independent of the caller's currently
93
+ * active org (`ctx.orgId`). `org`-visibility access must key off actual
94
+ * membership — a user's active-org selection is a UI convenience, not a
95
+ * statement of which orgs they belong to.
96
+ *
97
+ * Queries through the resource's own `reg.getDb()` — the same connection
98
+ * every other lookup in this file uses — not the ambient `getDbExec()`,
99
+ * since `org_members` lives in that same app database.
100
+ */
101
+ async function isOrgMember(
102
+ reg: ShareableResourceRegistration,
103
+ memberOrgId: string,
104
+ email: string,
105
+ ): Promise<boolean> {
106
+ const db = reg.getDb() as any;
107
+ const rows = await db
108
+ .select({ id: orgMembers.id })
109
+ .from(orgMembers)
110
+ .where(
111
+ and(
112
+ eq(orgMembers.orgId, memberOrgId),
113
+ emailColumnMatches(orgMembers.email, email),
114
+ ),
115
+ )
116
+ .limit(1);
117
+ return rows.length > 0;
118
+ }
119
+
90
120
  /**
91
121
  * Build a Drizzle `WHERE` clause that admits rows the current user can see.
92
122
  * Pass the ownable resource table and its shares table; optional min role
@@ -470,7 +500,7 @@ async function resolveAccessImpl(
470
500
  const resource = await loadResourceForAccess(reg, resourceId, options);
471
501
  if (!resource) return null;
472
502
 
473
- const { userEmail, orgId } = ctx;
503
+ const { userEmail } = ctx;
474
504
  const normalizedUserEmail = normalizeEmailForAccess(userEmail);
475
505
 
476
506
  if (
@@ -491,7 +521,19 @@ async function resolveAccessImpl(
491
521
  // `visibility === "public"` on an `allowPublic: false` resource is treated
492
522
  // as private: only owner + explicit shares grant access. Falls through to
493
523
  // the explicit-share lookup below.
494
- if (resource.visibility === "org" && orgId && resource.orgId === orgId) {
524
+ //
525
+ // Membership in the resource's own org, not equality with the caller's
526
+ // currently active org: a caller can be a genuine member of the
527
+ // resource's org while a *different* org is their active selection, and
528
+ // `org` visibility should still admit them. Still requires some active
529
+ // org to be set at all (`orgId`), matching the pre-existing behavior for
530
+ // a caller with no active org.
531
+ if (
532
+ resource.visibility === "org" &&
533
+ resource.orgId &&
534
+ normalizedUserEmail &&
535
+ (await isOrgMember(reg, resource.orgId, normalizedUserEmail))
536
+ ) {
495
537
  const role = await highestShareRole(reg, resourceId, ctx, resource);
496
538
  return { role: role ?? "viewer", resource };
497
539
  }
@@ -1234,10 +1234,15 @@ export async function getDatabaseByDocumentId(
1234
1234
 
1235
1235
  export async function getDatabaseItemByDocumentId(
1236
1236
  documentId: string,
1237
- options: { includeDeleted?: boolean } = {},
1237
+ options: { includeDeleted?: boolean; databaseId?: string } = {},
1238
1238
  db = getDb(),
1239
1239
  ) {
1240
1240
  const clauses = [eq(schema.contentDatabaseItems.documentId, documentId)];
1241
+ if (options.databaseId) {
1242
+ clauses.push(
1243
+ eq(schema.contentDatabaseItems.databaseId, options.databaseId),
1244
+ );
1245
+ }
1241
1246
  if (!options.includeDeleted) {
1242
1247
  clauses.push(isNull(schema.contentDatabases.deletedAt));
1243
1248
  }
@@ -1262,13 +1267,33 @@ export async function getDatabaseItemByDocumentId(
1262
1267
  )
1263
1268
  .leftJoin(
1264
1269
  schema.contentDatabaseBodyHydrationQueue,
1265
- eq(
1266
- schema.contentDatabaseBodyHydrationQueue.databaseItemId,
1267
- schema.contentDatabaseItems.id,
1270
+ and(
1271
+ eq(
1272
+ schema.contentDatabaseBodyHydrationQueue.databaseItemId,
1273
+ schema.contentDatabaseItems.id,
1274
+ ),
1275
+ eq(
1276
+ schema.contentDatabaseBodyHydrationQueue.sourceId,
1277
+ schema.contentDatabaseSourceRows.sourceId,
1278
+ ),
1279
+ eq(
1280
+ schema.contentDatabaseBodyHydrationQueue.sourceRowId,
1281
+ schema.contentDatabaseSourceRows.sourceRowId,
1282
+ ),
1268
1283
  ),
1269
1284
  )
1270
1285
  .where(and(...clauses))
1271
1286
  .orderBy(
1287
+ sql`CASE
1288
+ WHEN ${schema.contentDatabaseSourceRows.sourceId} IS NOT NULL
1289
+ AND (
1290
+ ${schema.contentDatabaseItems.bodyHydrationStatus} IN ('pending', 'hydrating')
1291
+ OR ${schema.contentDatabaseBodyHydrationQueue.id} IS NOT NULL
1292
+ ) THEN 0
1293
+ WHEN ${schema.contentDatabaseSourceRows.sourceId} IS NOT NULL
1294
+ AND ${schema.contentDatabaseItems.bodyHydrationStatus} = 'error' THEN 1
1295
+ ELSE 2
1296
+ END`,
1272
1297
  sql`CASE WHEN ${schema.contentDatabaseSourceRows.sourceId} IS NOT NULL THEN 0 ELSE 1 END`,
1273
1298
  sql`CASE WHEN ${schema.contentDatabases.systemRole} IS NULL THEN 0 ELSE 1 END`,
1274
1299
  sql`CASE WHEN ${schema.contentDatabases.systemRole} = 'files' THEN 0 ELSE 1 END`,
@@ -1277,6 +1302,90 @@ export async function getDatabaseItemByDocumentId(
1277
1302
  return row ?? null;
1278
1303
  }
1279
1304
 
1305
+ export async function getBuilderBodyHydrationMembershipByDocumentId(
1306
+ documentId: string,
1307
+ db = getDb(),
1308
+ ) {
1309
+ const rows = await db
1310
+ .select({
1311
+ item: schema.contentDatabaseItems,
1312
+ database: schema.contentDatabases,
1313
+ sourceId: schema.contentDatabaseSourceRows.sourceId,
1314
+ sourceRowId: schema.contentDatabaseSourceRows.sourceRowId,
1315
+ bodyHydrationQueueId: schema.contentDatabaseBodyHydrationQueue.id,
1316
+ queueSourceId: schema.contentDatabaseBodyHydrationQueue.sourceId,
1317
+ queueSourceRowId: schema.contentDatabaseBodyHydrationQueue.sourceRowId,
1318
+ })
1319
+ .from(schema.contentDatabaseSourceRows)
1320
+ .innerJoin(
1321
+ schema.contentDatabaseSources,
1322
+ eq(
1323
+ schema.contentDatabaseSources.id,
1324
+ schema.contentDatabaseSourceRows.sourceId,
1325
+ ),
1326
+ )
1327
+ .innerJoin(
1328
+ schema.contentDatabaseItems,
1329
+ eq(
1330
+ schema.contentDatabaseItems.id,
1331
+ schema.contentDatabaseSourceRows.databaseItemId,
1332
+ ),
1333
+ )
1334
+ .innerJoin(
1335
+ schema.contentDatabases,
1336
+ eq(schema.contentDatabases.id, schema.contentDatabaseItems.databaseId),
1337
+ )
1338
+ .leftJoin(
1339
+ schema.contentDatabaseBodyHydrationQueue,
1340
+ eq(
1341
+ schema.contentDatabaseBodyHydrationQueue.databaseItemId,
1342
+ schema.contentDatabaseItems.id,
1343
+ ),
1344
+ )
1345
+ .where(
1346
+ and(
1347
+ eq(schema.contentDatabaseSourceRows.documentId, documentId),
1348
+ eq(schema.contentDatabaseItems.documentId, documentId),
1349
+ eq(schema.contentDatabaseSources.sourceType, "builder-cms"),
1350
+ isNull(schema.contentDatabases.deletedAt),
1351
+ ),
1352
+ );
1353
+
1354
+ const boundRows = rows.filter(
1355
+ (row) =>
1356
+ !row.bodyHydrationQueueId ||
1357
+ (row.queueSourceId === row.sourceId &&
1358
+ row.queueSourceRowId === row.sourceRowId),
1359
+ );
1360
+ if (boundRows.length === 0) return null;
1361
+
1362
+ const priority = (row: (typeof boundRows)[number]) => {
1363
+ if (
1364
+ row.bodyHydrationQueueId ||
1365
+ row.item.bodyHydrationStatus === "pending" ||
1366
+ row.item.bodyHydrationStatus === "hydrating"
1367
+ ) {
1368
+ return 0;
1369
+ }
1370
+ if (row.item.bodyHydrationStatus === "error") return 1;
1371
+ return 2;
1372
+ };
1373
+ const topPriority = Math.min(...boundRows.map(priority));
1374
+ const candidates = boundRows
1375
+ .filter((row) => priority(row) === topPriority)
1376
+ .sort((left, right) =>
1377
+ `${left.database.id}:${left.sourceId}:${left.sourceRowId}`.localeCompare(
1378
+ `${right.database.id}:${right.sourceId}:${right.sourceRowId}`,
1379
+ ),
1380
+ );
1381
+ const sourceIds = new Set(candidates.map((row) => row.sourceId));
1382
+
1383
+ return {
1384
+ membership: candidates[0]!,
1385
+ hydrationSourceId: sourceIds.size === 1 ? candidates[0]!.sourceId : null,
1386
+ };
1387
+ }
1388
+
1280
1389
  export async function deleteDatabaseDataForDocument(
1281
1390
  documentId: string,
1282
1391
  ownerEmail: string,
@@ -11,6 +11,7 @@ import { favoriteDocumentIds } from "./_content-favorites.js";
11
11
  import { resolveContentSpaceAccess } from "./_content-space-access.js";
12
12
  import {
13
13
  getDatabaseByDocumentId,
14
+ getBuilderBodyHydrationMembershipByDocumentId,
14
15
  getDocumentContextPath,
15
16
  getDatabaseItemByDocumentId,
16
17
  isSoftDeletedDatabaseDocument,
@@ -18,7 +19,9 @@ import {
18
19
  } from "./_database-utils.js";
19
20
  import { serializeDocumentSource } from "./_document-source.js";
20
21
  import {
22
+ getDatabaseById,
21
23
  listPropertiesForDocument,
24
+ resolvePropertyDatabaseForDocument,
22
25
  serializeDatabase,
23
26
  } from "./_property-utils.js";
24
27
 
@@ -54,6 +57,14 @@ export default defineAction({
54
57
  description: "Get a single document by ID with full content.",
55
58
  schema: z.object({
56
59
  id: z.string().optional().describe("Document ID (required)"),
60
+ databaseId: z
61
+ .string()
62
+ .optional()
63
+ .describe("Exact Database context for membership-local data"),
64
+ databaseDocumentId: z
65
+ .string()
66
+ .optional()
67
+ .describe("Backing document ID for the exact Database context"),
57
68
  }),
58
69
  http: { method: "GET" },
59
70
  readOnly: true,
@@ -79,8 +90,54 @@ export default defineAction({
79
90
  });
80
91
  }
81
92
  const doc = access.resource;
93
+ if (args.databaseDocumentId && !args.databaseId) {
94
+ throw Object.assign(new Error("databaseDocumentId requires databaseId"), {
95
+ statusCode: 404,
96
+ });
97
+ }
98
+
82
99
  const database = await getDatabaseByDocumentId(doc.id);
83
- const databaseMembership = await getDatabaseItemByDocumentId(doc.id);
100
+ const databaseMembership = args.databaseId
101
+ ? await getDatabaseItemByDocumentId(doc.id, {
102
+ databaseId: args.databaseId,
103
+ })
104
+ : await getDatabaseItemByDocumentId(doc.id);
105
+ const propertyDatabase = args.databaseId
106
+ ? await getDatabaseById(args.databaseId)
107
+ : await resolvePropertyDatabaseForDocument(doc);
108
+ const propertyDatabaseAccess =
109
+ args.databaseId && propertyDatabase
110
+ ? await resolveDocumentAccess(propertyDatabase.documentId)
111
+ : null;
112
+ if (
113
+ args.databaseId &&
114
+ (!propertyDatabase ||
115
+ !propertyDatabaseAccess ||
116
+ (propertyDatabase.documentId !== doc.id && !databaseMembership))
117
+ ) {
118
+ throw Object.assign(new Error("Database context not found"), {
119
+ statusCode: 404,
120
+ });
121
+ }
122
+ if (
123
+ args.databaseDocumentId &&
124
+ propertyDatabase?.documentId !== args.databaseDocumentId
125
+ ) {
126
+ throw Object.assign(new Error("Database context not found"), {
127
+ statusCode: 404,
128
+ });
129
+ }
130
+ const bodyHydrationTarget =
131
+ await getBuilderBodyHydrationMembershipByDocumentId(doc.id);
132
+ const bodyHydrationMembership = bodyHydrationTarget?.membership;
133
+ const bodyHydrationAccess = bodyHydrationTarget?.hydrationSourceId
134
+ ? await resolveDocumentAccess(
135
+ bodyHydrationMembership!.database.documentId,
136
+ )
137
+ : null;
138
+ const bodyHydration = bodyHydrationMembership
139
+ ? serializeDatabaseMembership(bodyHydrationMembership).bodyHydration
140
+ : null;
84
141
  const userEmail = getRequestUserEmail();
85
142
  const favoriteIds = userEmail
86
143
  ? await favoriteDocumentIds(getDb(), userEmail, [doc.id])
@@ -112,10 +169,34 @@ export default defineAction({
112
169
  databaseMembership: databaseMembership
113
170
  ? serializeDatabaseMembership(databaseMembership)
114
171
  : undefined,
172
+ bodyHydration: bodyHydrationMembership
173
+ ? {
174
+ hydration: bodyHydrationAccess
175
+ ? bodyHydration!
176
+ : {
177
+ status: bodyHydration!.status,
178
+ attemptedAt: null,
179
+ error: null,
180
+ version: null,
181
+ },
182
+ ...(bodyHydrationAccess &&
183
+ canEditRole(bodyHydrationAccess.role) &&
184
+ bodyHydrationTarget?.hydrationSourceId
185
+ ? {
186
+ provider: "builder" as const,
187
+ sourceId: bodyHydrationTarget.hydrationSourceId,
188
+ databaseDocumentId:
189
+ bodyHydrationMembership.database.documentId,
190
+ }
191
+ : {}),
192
+ }
193
+ : undefined,
115
194
  createdAt: doc.createdAt,
116
195
  updatedAt: doc.updatedAt,
117
- properties: await listPropertiesForDocument(doc),
118
- contextPath: await getDocumentContextPath(doc),
196
+ properties: await listPropertiesForDocument(doc, args.databaseId),
197
+ contextPath: await getDocumentContextPath(doc, {
198
+ databaseId: args.databaseId,
199
+ }),
119
200
  };
120
201
  },
121
202
  link: ({ result }) => {
@@ -8,9 +8,16 @@ export function BuilderBodySyncingNotice({
8
8
  description: string;
9
9
  }) {
10
10
  return (
11
- <div className="rounded-lg border border-dashed border-border bg-muted/35 px-4 py-5 text-sm">
11
+ <div
12
+ className="rounded-lg border border-dashed border-border bg-muted/35 px-4 py-5 text-sm"
13
+ role="status"
14
+ aria-live="polite"
15
+ >
12
16
  <div className="flex items-center gap-2 font-medium text-foreground">
13
- <IconLoader2 className="size-4 animate-spin text-muted-foreground" />
17
+ <IconLoader2
18
+ className="size-4 animate-spin text-muted-foreground"
19
+ aria-hidden="true"
20
+ />
14
21
  {title}
15
22
  </div>
16
23
  <p className="mt-2 max-w-2xl leading-6 text-muted-foreground">
@@ -62,8 +62,10 @@ import {
62
62
  type ContentSpaceSummary,
63
63
  } from "@/hooks/use-content-spaces";
64
64
  import {
65
+ mergeDocumentIntoDocumentCache,
65
66
  isDocumentUpdateConflict,
66
67
  patchDocumentCaches,
68
+ documentQueryFilter,
67
69
  useDocument,
68
70
  useDeleteDocument,
69
71
  useDocuments,
@@ -247,7 +249,10 @@ export function DocumentEditor({
247
249
  databaseId,
248
250
  databaseDocumentId,
249
251
  }: DocumentEditorProps) {
250
- const documentQuery = useDocument(documentId);
252
+ const documentQuery = useDocument(documentId, {
253
+ databaseId,
254
+ databaseDocumentId,
255
+ });
251
256
  const {
252
257
  data: queriedDocument,
253
258
  isError,
@@ -539,7 +544,7 @@ function DocumentEditorBody({
539
544
  const deleteDocument = useDeleteDocument();
540
545
  const queryClient = useQueryClient();
541
546
  const processBuilderBodies = useProcessBuilderBodyHydration(
542
- document.databaseMembership?.databaseDocumentId ?? documentId,
547
+ document.bodyHydration?.databaseDocumentId ?? documentId,
543
548
  );
544
549
  const canEdit = document.canEdit ?? true;
545
550
  const canEditRef = useRef(canEdit);
@@ -664,24 +669,30 @@ function DocumentEditorBody({
664
669
  );
665
670
 
666
671
  useEffect(() => {
667
- const membership = document.databaseMembership;
668
- const hydration = membership?.bodyHydration;
672
+ const hydrationContext = document.bodyHydration;
673
+ const hydration = hydrationContext?.hydration;
669
674
  if (
670
- !membership?.sourceId ||
675
+ !canEdit ||
676
+ !hydrationContext?.sourceId ||
671
677
  !hydration ||
672
678
  (hydration.status !== "pending" && hydration.status !== "error")
673
679
  ) {
674
680
  return;
675
681
  }
676
- const promotionKey = `${membership.sourceId}:${documentId}:${hydration.status}:${hydration.version ?? ""}`;
682
+ const promotionKey = `${hydrationContext.sourceId}:${documentId}:${hydration.status}:${hydration.version ?? ""}`;
677
683
  if (promotedBuilderBodyRef.current === promotionKey) return;
678
684
  promotedBuilderBodyRef.current = promotionKey;
679
685
  processBuilderBodies.mutate({
680
- sourceId: membership.sourceId,
686
+ sourceId: hydrationContext.sourceId,
681
687
  documentId,
682
688
  limit: 1,
683
689
  });
684
- }, [document.databaseMembership, documentId, processBuilderBodies.mutate]);
690
+ }, [
691
+ canEdit,
692
+ document.bodyHydration,
693
+ documentId,
694
+ processBuilderBodies.mutate,
695
+ ]);
685
696
  const titleFocusedRef = useRef(false);
686
697
  const scrollContainerRef = useRef<HTMLDivElement>(null);
687
698
  const titleInputRef = useRef<HTMLTextAreaElement>(null);
@@ -976,9 +987,8 @@ function DocumentEditorBody({
976
987
  description:
977
988
  error instanceof Error ? error.message : t("empty.genericError"),
978
989
  });
979
- queryClient.setQueryData(
980
- ["action", "get-document", { id: documentId }],
981
- fileFirstDocument,
990
+ queryClient.setQueriesData(documentQueryFilter(documentId), (old) =>
991
+ mergeDocumentIntoDocumentCache(old, fileFirstDocument),
982
992
  );
983
993
  queryClient.invalidateQueries({
984
994
  queryKey: ["action", "list-documents"],
@@ -1890,9 +1900,15 @@ function DocumentEditorBody({
1890
1900
  if (bodyHydrationPending) {
1891
1901
  return (
1892
1902
  <BuilderBodySyncingNotice
1893
- title={t("editor.builderBodySyncing")}
1903
+ title={t(
1904
+ document.bodyHydration?.provider === "builder"
1905
+ ? "editor.builderBodySyncing"
1906
+ : "editor.pageBodySyncing",
1907
+ )}
1894
1908
  description={t(
1895
- "editor.builderBodySyncingDescription",
1909
+ document.bodyHydration?.provider === "builder"
1910
+ ? "editor.builderBodySyncingDescription"
1911
+ : "editor.pageBodySyncingDescription",
1896
1912
  )}
1897
1913
  />
1898
1914
  );
@@ -90,6 +90,7 @@ import {
90
90
  useSearchNotionPages,
91
91
  useCreateAndLinkNotionPage,
92
92
  } from "@/hooks/use-notion";
93
+ import { documentQueryFilter } from "@/lib/document-query";
93
94
  import {
94
95
  localSourceAbsolutePath,
95
96
  revealLinkedLocalSourceFile,
@@ -572,12 +573,8 @@ export function DocumentToolbar({
572
573
  const previous = hideFromSearch;
573
574
  setPendingHideFromSearch(next);
574
575
 
575
- queryClient.setQueryData(
576
- ["action", "get-document", { id: documentId }],
577
- (old: any) =>
578
- old && typeof old === "object"
579
- ? { ...old, hideFromSearch: next }
580
- : old,
576
+ queryClient.setQueriesData(documentQueryFilter(documentId), (old: any) =>
577
+ old && typeof old === "object" ? { ...old, hideFromSearch: next } : old,
581
578
  );
582
579
  queryClient.setQueryData(
583
580
  ["action", "list-documents", undefined],
@@ -601,9 +598,7 @@ export function DocumentToolbar({
601
598
  });
602
599
  } catch (err) {
603
600
  setPendingHideFromSearch(previous);
604
- queryClient.invalidateQueries({
605
- queryKey: ["action", "get-document", { id: documentId }],
606
- });
601
+ queryClient.invalidateQueries(documentQueryFilter(documentId));
607
602
  queryClient.invalidateQueries({
608
603
  queryKey: ["action", "list-documents"],
609
604
  });
@@ -614,9 +609,7 @@ export function DocumentToolbar({
614
609
  throw err;
615
610
  } finally {
616
611
  setPendingHideFromSearch(null);
617
- queryClient.invalidateQueries({
618
- queryKey: ["action", "get-document", { id: documentId }],
619
- });
612
+ queryClient.invalidateQueries(documentQueryFilter(documentId));
620
613
  queryClient.invalidateQueries({
621
614
  queryKey: ["action", "list-documents"],
622
615
  });
@@ -54,12 +54,12 @@ export function databaseItemBodyHydrationIsPending(
54
54
  }
55
55
 
56
56
  export function documentBodyHydrationIsPending(
57
- document: Pick<Document, "content" | "databaseMembership">,
57
+ document: Pick<Document, "content" | "bodyHydration">,
58
58
  ) {
59
- const hydration = document.databaseMembership?.bodyHydration;
59
+ const hydration = document.bodyHydration?.hydration;
60
60
  if (
61
61
  sourceBackedEmptyBodyNeedsHydration({
62
- sourceId: document.databaseMembership?.sourceId,
62
+ sourceId: document.bodyHydration ? "source-backed" : undefined,
63
63
  content: document.content,
64
64
  hydration,
65
65
  })
@@ -81,7 +81,10 @@ export function newDocumentPageChoiceIsDisabled(args: {
81
81
 
82
82
  export function previewBodyHydrationIsPending(args: {
83
83
  item: Pick<ContentDatabaseItem, "bodyHydration" | "document">;
84
- document: Pick<Document, "content" | "databaseMembership"> | null | undefined;
84
+ document:
85
+ | Pick<Document, "content" | "databaseMembership" | "bodyHydration">
86
+ | null
87
+ | undefined;
85
88
  }) {
86
89
  const membership =
87
90
  args.document?.databaseMembership ?? args.item.document.databaseMembership;
@@ -128,11 +131,14 @@ export function previewDraftConflictsWithHydratedBody(args: {
128
131
 
129
132
  export function previewBodyHydrationIsTerminalError(args: {
130
133
  item: Pick<ContentDatabaseItem, "bodyHydration" | "document">;
131
- document: Pick<Document, "databaseMembership"> | null | undefined;
134
+ document:
135
+ | Pick<Document, "databaseMembership" | "bodyHydration">
136
+ | null
137
+ | undefined;
132
138
  }) {
133
139
  return (
134
140
  builderBodyHydrationIsTerminalError(
135
- args.document?.databaseMembership?.bodyHydration,
141
+ args.document?.bodyHydration?.hydration,
136
142
  ) ||
137
143
  builderBodyHydrationIsTerminalError(
138
144
  args.item.bodyHydration ??
@@ -204,6 +204,7 @@ import {
204
204
  } from "@/hooks/use-document-properties";
205
205
  import {
206
206
  isDocumentUpdateConflict,
207
+ documentQueryFilter,
207
208
  type DocumentUpdateResult,
208
209
  useDeleteDocument,
209
210
  useDocument,
@@ -4853,9 +4854,7 @@ function DatabaseItemPreview({
4853
4854
  void queryClient.invalidateQueries({
4854
4855
  queryKey: contentDatabaseQueryKey(databaseDocumentId),
4855
4856
  });
4856
- void queryClient.invalidateQueries({
4857
- queryKey: ["action", "get-document", { id: document.id }],
4858
- });
4857
+ void queryClient.invalidateQueries(documentQueryFilter(document.id));
4859
4858
  void queryClient.invalidateQueries({
4860
4859
  queryKey: ["action", "list-documents"],
4861
4860
  });
@@ -126,6 +126,7 @@ import {
126
126
  useUpdateDocument,
127
127
  buildDocumentTree,
128
128
  filterDocumentTreeDocuments,
129
+ documentQueryFilter,
129
130
  } from "@/hooks/use-documents";
130
131
  import { useLocalStorage } from "@/hooks/use-local-storage";
131
132
  import {
@@ -1191,16 +1192,12 @@ export function DocumentSidebar({
1191
1192
  created,
1192
1193
  );
1193
1194
  if (nextId !== id) {
1194
- queryClient.removeQueries({
1195
- queryKey: ["action", "get-document", { id }],
1196
- });
1195
+ queryClient.removeQueries(documentQueryFilter(id));
1197
1196
  navigateToDocument(nextId);
1198
1197
  }
1199
1198
  // Replace optimistic doc with real server doc + clear any 404 error
1200
1199
  // state from the in-flight fetch that ran before create completed.
1201
- queryClient.invalidateQueries({
1202
- queryKey: ["action", "get-document", { id: nextId }],
1203
- });
1200
+ queryClient.invalidateQueries(documentQueryFilter(nextId));
1204
1201
  queryClient.invalidateQueries({
1205
1202
  queryKey: ["action", "list-documents"],
1206
1203
  });
@@ -1214,9 +1211,7 @@ export function DocumentSidebar({
1214
1211
  queryClient.invalidateQueries({
1215
1212
  queryKey: ["action", "list-documents"],
1216
1213
  });
1217
- queryClient.removeQueries({
1218
- queryKey: ["action", "get-document", { id }],
1219
- });
1214
+ queryClient.removeQueries(documentQueryFilter(id));
1220
1215
  if (rootFilesDatabaseId) {
1221
1216
  queryClient.setQueryData<ContentDatabaseResponse>(
1222
1217
  contentDatabaseByIdQueryKey(rootFilesDatabaseId),
@@ -1300,9 +1295,7 @@ export function DocumentSidebar({
1300
1295
  );
1301
1296
  });
1302
1297
  for (const deletedId of deletedIds) {
1303
- queryClient.removeQueries({
1304
- queryKey: ["action", "get-document", { id: deletedId }],
1305
- });
1298
+ queryClient.removeQueries(documentQueryFilter(deletedId));
1306
1299
  }
1307
1300
 
1308
1301
  if (activeDeleted) {