@agent-native/core 0.133.2 → 0.134.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.
Files changed (213) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +62 -0
  3. package/corpus/core/docs/content/observability.mdx +41 -1
  4. package/corpus/core/docs/content/tracking.mdx +25 -0
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/agent/run-manager.ts +7 -0
  7. package/corpus/core/src/client/agent-chat-adapter.ts +10 -0
  8. package/corpus/core/src/client/analytics.ts +105 -1
  9. package/corpus/core/src/client/use-action.ts +6 -0
  10. package/corpus/core/src/deploy/build.ts +38 -1
  11. package/corpus/core/src/observability/posthog-ai.ts +294 -0
  12. package/corpus/core/src/observability/routes.ts +29 -6
  13. package/corpus/core/src/observability/traces.ts +287 -12
  14. package/corpus/core/src/observability/types.ts +11 -0
  15. package/corpus/core/src/secrets/register-framework-secrets.ts +16 -0
  16. package/corpus/core/src/server/agent-run-context.ts +21 -0
  17. package/corpus/core/src/server/capture-error.ts +8 -0
  18. package/corpus/core/src/server/core-routes-plugin.ts +75 -4
  19. package/corpus/core/src/server/deploy-environment.ts +57 -0
  20. package/corpus/core/src/server/error-noise-filter.ts +268 -0
  21. package/corpus/core/src/server/posthog-config.ts +75 -0
  22. package/corpus/core/src/server/request-context.ts +6 -0
  23. package/corpus/core/src/server/sentry-config.ts +4 -8
  24. package/corpus/core/src/server/sentry-plugin.ts +13 -29
  25. package/corpus/core/src/server/sentry.ts +13 -156
  26. package/corpus/core/src/server/ssr-handler.ts +6 -1
  27. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  28. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  29. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  30. package/corpus/core/src/tracking/error-capture.ts +44 -109
  31. package/corpus/core/src/tracking/index.ts +11 -0
  32. package/corpus/core/src/tracking/posthog-exception.ts +305 -0
  33. package/corpus/core/src/tracking/providers.ts +91 -14
  34. package/corpus/core/src/tracking/redaction.ts +105 -0
  35. package/corpus/templates/content/actions/_batch-utils.ts +33 -0
  36. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -81
  37. package/corpus/templates/content/actions/_content-files.ts +31 -21
  38. package/corpus/templates/content/actions/_database-source-utils.ts +653 -155
  39. package/corpus/templates/content/actions/_database-utils.ts +535 -41
  40. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +291 -240
  41. package/corpus/templates/content/actions/add-database-item.ts +15 -1
  42. package/corpus/templates/content/actions/attach-content-database-source.ts +156 -58
  43. package/corpus/templates/content/actions/bind-content-database-source-field.ts +2 -2
  44. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +4 -1
  45. package/corpus/templates/content/actions/delete-database-items.ts +8 -2
  46. package/corpus/templates/content/actions/disconnect-content-database-source.ts +2 -2
  47. package/corpus/templates/content/actions/duplicate-database-item.ts +15 -1
  48. package/corpus/templates/content/actions/duplicate-database-items.ts +19 -1
  49. package/corpus/templates/content/actions/execute-builder-source-execution.ts +2 -1
  50. package/corpus/templates/content/actions/get-content-database.ts +12 -64
  51. package/corpus/templates/content/actions/move-database-item.ts +4 -1
  52. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +1 -1
  53. package/corpus/templates/content/actions/prepare-builder-source-review.ts +4 -8
  54. package/corpus/templates/content/actions/preview-content-database-source-attach.ts +92 -0
  55. package/corpus/templates/content/actions/process-builder-body-hydration.ts +2 -1
  56. package/corpus/templates/content/actions/query-content-database-items.ts +64 -0
  57. package/corpus/templates/content/actions/refresh-content-database-source.ts +7 -0
  58. package/corpus/templates/content/actions/review-content-database-source-change-set.ts +1 -1
  59. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +1 -1
  60. package/corpus/templates/content/actions/stage-builder-revision.ts +1 -1
  61. package/corpus/templates/content/actions/update-content-database-personal-view.ts +29 -3
  62. package/corpus/templates/content/actions/update-content-database-view.ts +1 -1
  63. package/corpus/templates/content/actions/validate-builder-source-execution.ts +1 -1
  64. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +3 -22
  65. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +267 -105
  66. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +82 -1
  67. package/corpus/templates/content/app/hooks/use-content-database.ts +285 -24
  68. package/corpus/templates/content/app/hooks/use-document-properties.ts +7 -6
  69. package/corpus/templates/content/app/i18n-data.ts +10 -0
  70. package/corpus/templates/content/changelog/2026-07-29-large-databases-keep-useful-rows-visible.md +6 -0
  71. package/corpus/templates/content/changelog/2026-07-30-builder-source-columns-now-appear-immediately-when-connected.md +6 -0
  72. package/corpus/templates/content/changelog/2026-07-30-large-builder-backed-tables-show-useful-rows-sooner-and-fini.md +6 -0
  73. package/corpus/templates/content/changelog/2026-08-01-large-builder-databases-now-show-rows-immediately-and-finish.md +6 -0
  74. package/corpus/templates/content/parity/matrix.md +2 -1
  75. package/corpus/templates/content/parity/matrix.ts +25 -0
  76. package/corpus/templates/content/shared/api.ts +54 -4
  77. package/corpus/templates/content/shared/database-query.ts +359 -0
  78. package/dist/agent/run-manager.d.ts.map +1 -1
  79. package/dist/agent/run-manager.js +7 -0
  80. package/dist/agent/run-manager.js.map +1 -1
  81. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  82. package/dist/client/agent-chat-adapter.js +12 -0
  83. package/dist/client/agent-chat-adapter.js.map +1 -1
  84. package/dist/client/analytics.d.ts +8 -0
  85. package/dist/client/analytics.d.ts.map +1 -1
  86. package/dist/client/analytics.js +95 -1
  87. package/dist/client/analytics.js.map +1 -1
  88. package/dist/client/use-action.d.ts.map +1 -1
  89. package/dist/client/use-action.js +6 -0
  90. package/dist/client/use-action.js.map +1 -1
  91. package/dist/collab/routes.d.ts +1 -1
  92. package/dist/collab/struct-routes.d.ts +1 -1
  93. package/dist/deploy/build.d.ts.map +1 -1
  94. package/dist/deploy/build.js +38 -1
  95. package/dist/deploy/build.js.map +1 -1
  96. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  97. package/dist/notifications/routes.d.ts +3 -3
  98. package/dist/observability/posthog-ai.d.ts +126 -0
  99. package/dist/observability/posthog-ai.d.ts.map +1 -0
  100. package/dist/observability/posthog-ai.js +190 -0
  101. package/dist/observability/posthog-ai.js.map +1 -0
  102. package/dist/observability/routes.d.ts +1 -1
  103. package/dist/observability/routes.d.ts.map +1 -1
  104. package/dist/observability/routes.js +26 -6
  105. package/dist/observability/routes.js.map +1 -1
  106. package/dist/observability/traces.d.ts +9 -0
  107. package/dist/observability/traces.d.ts.map +1 -1
  108. package/dist/observability/traces.js +219 -10
  109. package/dist/observability/traces.js.map +1 -1
  110. package/dist/observability/types.d.ts +10 -0
  111. package/dist/observability/types.d.ts.map +1 -1
  112. package/dist/observability/types.js +1 -0
  113. package/dist/observability/types.js.map +1 -1
  114. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  115. package/dist/secrets/register-framework-secrets.js +14 -0
  116. package/dist/secrets/register-framework-secrets.js.map +1 -1
  117. package/dist/server/agent-run-context.d.ts +8 -0
  118. package/dist/server/agent-run-context.d.ts.map +1 -1
  119. package/dist/server/agent-run-context.js +18 -0
  120. package/dist/server/agent-run-context.js.map +1 -1
  121. package/dist/server/capture-error.d.ts +8 -0
  122. package/dist/server/capture-error.d.ts.map +1 -1
  123. package/dist/server/capture-error.js.map +1 -1
  124. package/dist/server/core-routes-plugin.d.ts +0 -23
  125. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  126. package/dist/server/core-routes-plugin.js +71 -7
  127. package/dist/server/core-routes-plugin.js.map +1 -1
  128. package/dist/server/deploy-environment.d.ts +17 -0
  129. package/dist/server/deploy-environment.d.ts.map +1 -0
  130. package/dist/server/deploy-environment.js +47 -0
  131. package/dist/server/deploy-environment.js.map +1 -0
  132. package/dist/server/error-noise-filter.d.ts +80 -0
  133. package/dist/server/error-noise-filter.d.ts.map +1 -0
  134. package/dist/server/error-noise-filter.js +173 -0
  135. package/dist/server/error-noise-filter.js.map +1 -0
  136. package/dist/server/posthog-config.d.ts +28 -0
  137. package/dist/server/posthog-config.d.ts.map +1 -0
  138. package/dist/server/posthog-config.js +53 -0
  139. package/dist/server/posthog-config.js.map +1 -0
  140. package/dist/server/request-context.d.ts +6 -0
  141. package/dist/server/request-context.d.ts.map +1 -1
  142. package/dist/server/request-context.js.map +1 -1
  143. package/dist/server/sentry-config.d.ts +1 -0
  144. package/dist/server/sentry-config.d.ts.map +1 -1
  145. package/dist/server/sentry-config.js +3 -1
  146. package/dist/server/sentry-config.js.map +1 -1
  147. package/dist/server/sentry-plugin.d.ts.map +1 -1
  148. package/dist/server/sentry-plugin.js +12 -27
  149. package/dist/server/sentry-plugin.js.map +1 -1
  150. package/dist/server/sentry.d.ts.map +1 -1
  151. package/dist/server/sentry.js +8 -124
  152. package/dist/server/sentry.js.map +1 -1
  153. package/dist/server/ssr-handler.d.ts.map +1 -1
  154. package/dist/server/ssr-handler.js +6 -1
  155. package/dist/server/ssr-handler.js.map +1 -1
  156. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  157. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  158. package/dist/shared/mcp-embed-headers.js +1 -1
  159. package/dist/shared/mcp-embed-headers.js.map +1 -1
  160. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  161. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  162. package/dist/tracking/error-capture.d.ts +7 -0
  163. package/dist/tracking/error-capture.d.ts.map +1 -1
  164. package/dist/tracking/error-capture.js +10 -73
  165. package/dist/tracking/error-capture.js.map +1 -1
  166. package/dist/tracking/index.d.ts +1 -0
  167. package/dist/tracking/index.d.ts.map +1 -1
  168. package/dist/tracking/index.js +1 -0
  169. package/dist/tracking/index.js.map +1 -1
  170. package/dist/tracking/posthog-exception.d.ts +103 -0
  171. package/dist/tracking/posthog-exception.d.ts.map +1 -0
  172. package/dist/tracking/posthog-exception.js +181 -0
  173. package/dist/tracking/posthog-exception.js.map +1 -0
  174. package/dist/tracking/providers.d.ts +14 -0
  175. package/dist/tracking/providers.d.ts.map +1 -1
  176. package/dist/tracking/providers.js +63 -11
  177. package/dist/tracking/providers.js.map +1 -1
  178. package/dist/tracking/redaction.d.ts +25 -0
  179. package/dist/tracking/redaction.d.ts.map +1 -0
  180. package/dist/tracking/redaction.js +85 -0
  181. package/dist/tracking/redaction.js.map +1 -0
  182. package/docs/content/observability.mdx +41 -1
  183. package/docs/content/tracking.mdx +25 -0
  184. package/package.json +1 -1
  185. package/src/agent/run-manager.ts +7 -0
  186. package/src/client/agent-chat-adapter.ts +10 -0
  187. package/src/client/analytics.ts +105 -1
  188. package/src/client/use-action.ts +6 -0
  189. package/src/deploy/build.ts +38 -1
  190. package/src/observability/posthog-ai.ts +294 -0
  191. package/src/observability/routes.ts +29 -6
  192. package/src/observability/traces.ts +287 -12
  193. package/src/observability/types.ts +11 -0
  194. package/src/secrets/register-framework-secrets.ts +16 -0
  195. package/src/server/agent-run-context.ts +21 -0
  196. package/src/server/capture-error.ts +8 -0
  197. package/src/server/core-routes-plugin.ts +75 -4
  198. package/src/server/deploy-environment.ts +57 -0
  199. package/src/server/error-noise-filter.ts +268 -0
  200. package/src/server/posthog-config.ts +75 -0
  201. package/src/server/request-context.ts +6 -0
  202. package/src/server/sentry-config.ts +4 -8
  203. package/src/server/sentry-plugin.ts +13 -29
  204. package/src/server/sentry.ts +13 -156
  205. package/src/server/ssr-handler.ts +6 -1
  206. package/src/shared/mcp-embed-headers.ts +1 -1
  207. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  208. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  209. package/src/tracking/error-capture.ts +44 -109
  210. package/src/tracking/index.ts +11 -0
  211. package/src/tracking/posthog-exception.ts +305 -0
  212. package/src/tracking/providers.ts +91 -14
  213. package/src/tracking/redaction.ts +105 -0
@@ -5,9 +5,11 @@ import {
5
5
  import {
6
6
  accessFilter,
7
7
  ROLE_RANK,
8
+ resolveAccess,
8
9
  type ShareRole,
9
10
  } from "@agent-native/core/sharing";
10
11
  import { and, asc, eq, inArray, isNull, or, sql } from "drizzle-orm";
12
+ import { z } from "zod";
11
13
 
12
14
  import { getDb, schema } from "../server/db/index.js";
13
15
  import { getDocumentContextPath } from "../server/lib/document-context.js";
@@ -18,13 +20,29 @@ import {
18
20
  } from "../server/lib/documents.js";
19
21
  import type {
20
22
  ContentDatabaseBodyHydration,
23
+ ContentDatabaseItem,
21
24
  ContentDatabaseMembership,
22
25
  ContentDatabaseResponse,
26
+ ContentDatabaseTableQuery,
27
+ DocumentProperty,
23
28
  } from "../shared/api.js";
29
+ import {
30
+ applyContentDatabaseTableQuery,
31
+ contentDatabaseTableQueryUsesProperties,
32
+ } from "../shared/database-query.js";
33
+ import {
34
+ evaluatePropertyFormula,
35
+ isBlocksPropertyType,
36
+ isComputedPropertyType,
37
+ isPrimaryBlocksField,
38
+ parsePropertyValue,
39
+ type DocumentPropertyType,
40
+ } from "../shared/properties.js";
24
41
  import { favoriteDocumentIds } from "./_content-favorites.js";
25
42
  import {
26
43
  listContentOrganizationMemberships,
27
44
  normalizeContentSpaceEmail,
45
+ resolveContentSpaceAccess,
28
46
  } from "./_content-space-access.js";
29
47
  import { getAllContentDatabaseSourceSnapshots } from "./_database-source-utils.js";
30
48
  import {
@@ -44,6 +62,135 @@ export { getDocumentContextPath };
44
62
 
45
63
  export const CONTENT_DATABASE_MAX_READ_LIMIT = 5_000;
46
64
 
65
+ const QUERY_PROJECTION_UNSUPPORTED_PROPERTY_TYPES =
66
+ new Set<DocumentPropertyType>(["rollup"]);
67
+
68
+ function boundedTableQueryProjectionPropertyIds(
69
+ query: ContentDatabaseTableQuery,
70
+ properties: DocumentProperty[],
71
+ ) {
72
+ let propertyIds = new Set(
73
+ query.search.trim()
74
+ ? properties.map((property) => property.definition.id)
75
+ : [...query.filters, ...query.sorts]
76
+ .map((constraint) => constraint.key)
77
+ .filter((key) => key !== "name"),
78
+ );
79
+ if (
80
+ properties.some(
81
+ (property) =>
82
+ propertyIds.has(property.definition.id) &&
83
+ property.definition.type === "formula",
84
+ )
85
+ ) {
86
+ propertyIds = new Set(properties.map((property) => property.definition.id));
87
+ }
88
+ for (const property of properties) {
89
+ if (
90
+ propertyIds.has(property.definition.id) &&
91
+ QUERY_PROJECTION_UNSUPPORTED_PROPERTY_TYPES.has(property.definition.type)
92
+ ) {
93
+ return null;
94
+ }
95
+ }
96
+ return propertyIds;
97
+ }
98
+
99
+ function projectedComputedPropertyValue(
100
+ type: DocumentPropertyType,
101
+ document: {
102
+ ownerEmail: string;
103
+ createdAt: string;
104
+ updatedAt: string;
105
+ },
106
+ ) {
107
+ if (type === "created_time") return document.createdAt;
108
+ if (type === "created_by" || type === "last_edited_by") {
109
+ return document.ownerEmail;
110
+ }
111
+ if (type === "last_edited_time") return document.updatedAt;
112
+ return null;
113
+ }
114
+
115
+ export const contentDatabaseTableQuerySchema = z
116
+ .object({
117
+ search: z.string().max(500),
118
+ filters: z
119
+ .array(
120
+ z.object({
121
+ key: z.string(),
122
+ label: z.string(),
123
+ operator: z.enum([
124
+ "contains",
125
+ "equals",
126
+ "does_not_equal",
127
+ "greater_than",
128
+ "less_than",
129
+ "before",
130
+ "after",
131
+ "between",
132
+ "is_checked",
133
+ "is_unchecked",
134
+ "is_empty",
135
+ "is_not_empty",
136
+ ]),
137
+ value: z.string(),
138
+ filterGroupId: z.string().optional(),
139
+ parentFilterGroupId: z.string().optional(),
140
+ }),
141
+ )
142
+ .max(50),
143
+ sorts: z
144
+ .array(
145
+ z.object({
146
+ key: z.string(),
147
+ label: z.string(),
148
+ direction: z.enum(["asc", "desc"]),
149
+ }),
150
+ )
151
+ .max(20),
152
+ filterMode: z.enum(["and", "or"]),
153
+ })
154
+ .optional();
155
+
156
+ async function contentDatabaseTableQueryMode(
157
+ databaseId: string,
158
+ query: ContentDatabaseTableQuery | undefined,
159
+ ) {
160
+ if (!query) return undefined;
161
+ const sourceFields = await getDb()
162
+ .select({
163
+ metadataJson: schema.contentDatabaseSources.metadataJson,
164
+ propertyId: schema.contentDatabaseSourceFields.propertyId,
165
+ })
166
+ .from(schema.contentDatabaseSourceFields)
167
+ .innerJoin(
168
+ schema.contentDatabaseSources,
169
+ eq(
170
+ schema.contentDatabaseSources.id,
171
+ schema.contentDatabaseSourceFields.sourceId,
172
+ ),
173
+ )
174
+ .where(eq(schema.contentDatabaseSources.databaseId, databaseId));
175
+ const secondaryPropertyIds = new Set<string>();
176
+ for (const field of sourceFields) {
177
+ let role: unknown = null;
178
+ try {
179
+ role = JSON.parse(field.metadataJson || "{}").federation?.role;
180
+ } catch {
181
+ role = null;
182
+ }
183
+ if (role === "secondary" && field.propertyId) {
184
+ secondaryPropertyIds.add(field.propertyId);
185
+ }
186
+ }
187
+ const usesSecondaryField = contentDatabaseTableQueryUsesProperties(
188
+ query,
189
+ secondaryPropertyIds,
190
+ );
191
+ return usesSecondaryField ? "client-required" : "server";
192
+ }
193
+
47
194
  function canManageRole(role: string) {
48
195
  return role === "owner" || role === "admin";
49
196
  }
@@ -277,33 +424,122 @@ function serializeDocument(
277
424
  };
278
425
  }
279
426
 
280
- export async function getContentDatabaseResponse(
281
- databaseId: string,
282
- options: { limit?: number; offset?: number } = {},
283
- ): Promise<ContentDatabaseResponse> {
427
+ export type ContentDatabasePageResponse = Pick<
428
+ ContentDatabaseResponse,
429
+ "items" | "source" | "sources" | "pagination" | "tableQueryMode"
430
+ >;
431
+
432
+ export type ContentDatabaseReadResolution =
433
+ | {
434
+ available: true;
435
+ database: typeof schema.contentDatabases.$inferSelect;
436
+ }
437
+ | {
438
+ available: false;
439
+ reason: "not_found" | "deleted";
440
+ databaseId: string;
441
+ documentId: string | null;
442
+ deletedAt?: string | null;
443
+ message: string;
444
+ };
445
+
446
+ export async function resolveContentDatabaseRead(args: {
447
+ databaseId?: string;
448
+ documentId?: string;
449
+ }): Promise<ContentDatabaseReadResolution> {
284
450
  const db = getDb();
451
+ let databaseId = args.databaseId;
452
+ if (!databaseId && args.documentId) {
453
+ const [database] = await db
454
+ .select({ id: schema.contentDatabases.id })
455
+ .from(schema.contentDatabases)
456
+ .where(eq(schema.contentDatabases.documentId, args.documentId));
457
+ databaseId = database?.id;
458
+ }
459
+ if (!databaseId) {
460
+ throw new Error("Either databaseId or documentId is required.");
461
+ }
462
+
285
463
  const [database] = await db
286
464
  .select()
287
465
  .from(schema.contentDatabases)
288
466
  .where(eq(schema.contentDatabases.id, databaseId));
467
+ if (!database) {
468
+ return {
469
+ available: false,
470
+ reason: "not_found",
471
+ databaseId,
472
+ documentId: args.documentId ?? null,
473
+ message: `Database "${databaseId}" not found`,
474
+ };
475
+ }
476
+
477
+ let canRead = Boolean(await resolveAccess("document", database.documentId));
478
+ if (!canRead && database.systemRole === "files" && database.spaceId) {
479
+ try {
480
+ await resolveContentSpaceAccess(database.spaceId);
481
+ canRead = true;
482
+ } catch {
483
+ canRead = false;
484
+ }
485
+ }
486
+ if (!canRead) throw new Error(`Database "${databaseId}" not found`);
487
+
488
+ if (database.deletedAt) {
489
+ return {
490
+ available: false,
491
+ reason: "deleted",
492
+ databaseId: database.id,
493
+ documentId: database.documentId,
494
+ deletedAt: database.deletedAt,
495
+ message: `Database "${database.id}" has been deleted`,
496
+ };
497
+ }
498
+
499
+ return { available: true, database };
500
+ }
501
+
502
+ type ContentDatabasePageBuild = ContentDatabasePageResponse & {
503
+ databaseRecord: typeof schema.contentDatabases.$inferSelect;
504
+ properties: ContentDatabaseResponse["properties"];
505
+ hydratedItemCount: number;
506
+ };
507
+
508
+ export async function getContentDatabasePageResponse(
509
+ databaseId: string,
510
+ options: {
511
+ limit?: number;
512
+ offset?: number;
513
+ tableQuery?: ContentDatabaseTableQuery;
514
+ includeSources?: boolean;
515
+ documentIds?: string[];
516
+ database?: typeof schema.contentDatabases.$inferSelect;
517
+ } = {},
518
+ ): Promise<ContentDatabasePageBuild> {
519
+ const db = getDb();
520
+ const database =
521
+ options.database ??
522
+ (
523
+ await db
524
+ .select()
525
+ .from(schema.contentDatabases)
526
+ .where(eq(schema.contentDatabases.id, databaseId))
527
+ )[0];
289
528
 
290
529
  if (!database || database.deletedAt) {
291
530
  throw new Error(`Database "${databaseId}" not found`);
292
531
  }
293
- const [databaseDocument] = await db
294
- .select({
295
- id: schema.documents.id,
296
- parentId: schema.documents.parentId,
297
- description: schema.documents.description,
298
- })
299
- .from(schema.documents)
300
- .where(eq(schema.documents.id, database.documentId));
301
-
302
532
  // PURE read: the primary "Content" Blocks field is seeded at create time and
303
533
  // by the one-time startup repair — never here. Reading a database (including a
304
534
  // shared one a viewer is opening) must not mutate schema.
305
535
 
306
536
  const { limit, offset } = normalizeContentDatabasePageOptions(options);
537
+ const tableQuery = options.tableQuery;
538
+ const tableQueryMode = await contentDatabaseTableQueryMode(
539
+ databaseId,
540
+ tableQuery,
541
+ );
542
+ const serverTableQuery = tableQueryMode === "server" ? tableQuery : undefined;
307
543
  const userEmail = getRequestUserEmail();
308
544
  const normalizedUserEmail = userEmail
309
545
  ? normalizeContentSpaceEmail(userEmail)
@@ -401,6 +637,11 @@ export async function getContentDatabaseResponse(
401
637
  : undefined;
402
638
  const visibleItemFilter = and(
403
639
  eq(schema.contentDatabaseItems.databaseId, databaseId),
640
+ options.documentIds !== undefined
641
+ ? options.documentIds.length > 0
642
+ ? inArray(schema.contentDatabaseItems.documentId, options.documentIds)
643
+ : sql`1 = 0`
644
+ : undefined,
404
645
  sql`exists (
405
646
  select 1 from ${schema.documents}
406
647
  where ${schema.documents.id} = ${schema.contentDatabaseItems.documentId}
@@ -428,17 +669,198 @@ export async function getContentDatabaseResponse(
428
669
  .select({ count: sql<number>`COUNT(*)` })
429
670
  .from(schema.contentDatabaseItems)
430
671
  .where(visibleItemFilter);
672
+ const totalVisibleItems = Number(itemCount?.count ?? 0);
673
+ if (tableQuery && totalVisibleItems > CONTENT_DATABASE_MAX_READ_LIMIT) {
674
+ throw new Error(
675
+ `Table constraints support up to ${CONTENT_DATABASE_MAX_READ_LIMIT} rows; this database has ${totalVisibleItems}.`,
676
+ );
677
+ }
678
+
679
+ const databaseProperties = await listPropertiesForDatabase(databaseId);
680
+ const boundedProjectionPropertyIds =
681
+ serverTableQuery && !database.systemRole
682
+ ? boundedTableQueryProjectionPropertyIds(
683
+ serverTableQuery,
684
+ databaseProperties,
685
+ )
686
+ : null;
431
687
 
432
688
  let itemsQuery = db
433
689
  .select()
434
690
  .from(schema.contentDatabaseItems)
435
691
  .where(visibleItemFilter)
436
- .orderBy(asc(schema.contentDatabaseItems.position))
692
+ .orderBy(
693
+ asc(schema.contentDatabaseItems.position),
694
+ asc(schema.contentDatabaseItems.createdAt),
695
+ asc(schema.contentDatabaseItems.id),
696
+ )
437
697
  .$dynamic();
438
- if (limit !== null) {
698
+ if (serverTableQuery) {
699
+ itemsQuery = itemsQuery.limit(CONTENT_DATABASE_MAX_READ_LIMIT);
700
+ } else if (limit !== null) {
439
701
  itemsQuery = itemsQuery.limit(limit).offset(offset);
440
702
  }
441
- const items = await itemsQuery;
703
+ let items = await itemsQuery;
704
+ let boundedTableQueryTotal: number | null = null;
705
+ if (serverTableQuery && boundedProjectionPropertyIds) {
706
+ const candidateDocuments = await db
707
+ .select({
708
+ id: schema.documents.id,
709
+ title: schema.documents.title,
710
+ ownerEmail: schema.documents.ownerEmail,
711
+ createdAt: schema.documents.createdAt,
712
+ updatedAt: schema.documents.updatedAt,
713
+ })
714
+ .from(schema.documents)
715
+ .innerJoin(
716
+ schema.contentDatabaseItems,
717
+ eq(schema.contentDatabaseItems.documentId, schema.documents.id),
718
+ )
719
+ .where(
720
+ and(
721
+ visibleItemFilter,
722
+ eq(schema.documents.ownerEmail, database.ownerEmail),
723
+ ),
724
+ );
725
+ const candidateDocumentById = new Map(
726
+ candidateDocuments.map((document) => [document.id, document]),
727
+ );
728
+ const candidateValues =
729
+ boundedProjectionPropertyIds.size > 0
730
+ ? await db
731
+ .select({
732
+ documentId: schema.documentPropertyValues.documentId,
733
+ propertyId: schema.documentPropertyValues.propertyId,
734
+ valueJson: schema.documentPropertyValues.valueJson,
735
+ })
736
+ .from(schema.documentPropertyValues)
737
+ .innerJoin(
738
+ schema.contentDatabaseItems,
739
+ eq(
740
+ schema.contentDatabaseItems.documentId,
741
+ schema.documentPropertyValues.documentId,
742
+ ),
743
+ )
744
+ .where(
745
+ and(
746
+ visibleItemFilter,
747
+ inArray(schema.documentPropertyValues.propertyId, [
748
+ ...boundedProjectionPropertyIds,
749
+ ]),
750
+ ),
751
+ )
752
+ : [];
753
+ const candidateValueByDocumentAndProperty = new Map(
754
+ candidateValues.map((value) => [
755
+ `${value.documentId}\0${value.propertyId}`,
756
+ parsePropertyValue(value.valueJson),
757
+ ]),
758
+ );
759
+ const queryProperties = databaseProperties.filter((property) =>
760
+ boundedProjectionPropertyIds.has(property.definition.id),
761
+ );
762
+ const additionalBlocksPropertyIds = queryProperties.flatMap((property) =>
763
+ isBlocksPropertyType(property.definition.type) &&
764
+ !isPrimaryBlocksField(property.definition.options)
765
+ ? [property.definition.id]
766
+ : [],
767
+ );
768
+ const candidateBlockContents =
769
+ additionalBlocksPropertyIds.length > 0
770
+ ? await db
771
+ .select({
772
+ documentId: schema.documentBlockFieldContents.documentId,
773
+ propertyId: schema.documentBlockFieldContents.propertyId,
774
+ content: schema.documentBlockFieldContents.content,
775
+ })
776
+ .from(schema.documentBlockFieldContents)
777
+ .innerJoin(
778
+ schema.contentDatabaseItems,
779
+ eq(
780
+ schema.contentDatabaseItems.documentId,
781
+ schema.documentBlockFieldContents.documentId,
782
+ ),
783
+ )
784
+ .where(
785
+ and(
786
+ visibleItemFilter,
787
+ inArray(
788
+ schema.documentBlockFieldContents.propertyId,
789
+ additionalBlocksPropertyIds,
790
+ ),
791
+ ),
792
+ )
793
+ : [];
794
+ const candidateBlockContentByDocumentAndProperty = new Map(
795
+ candidateBlockContents.map((row) => [
796
+ `${row.documentId}\0${row.propertyId}`,
797
+ row.content ?? "",
798
+ ]),
799
+ );
800
+ const candidateItems = items.flatMap((item, itemIndex) => {
801
+ const document = candidateDocumentById.get(item.documentId);
802
+ if (!document) return [];
803
+ const properties = queryProperties.map((property) => {
804
+ const type = property.definition.type;
805
+ const value =
806
+ type === "id"
807
+ ? itemIndex + 1
808
+ : isBlocksPropertyType(type)
809
+ ? isPrimaryBlocksField(property.definition.options)
810
+ ? ""
811
+ : (candidateBlockContentByDocumentAndProperty.get(
812
+ `${document.id}\0${property.definition.id}`,
813
+ ) ?? "")
814
+ : isComputedPropertyType(type)
815
+ ? projectedComputedPropertyValue(type, document)
816
+ : (candidateValueByDocumentAndProperty.get(
817
+ `${document.id}\0${property.definition.id}`,
818
+ ) ?? null);
819
+ return { ...property, value };
820
+ });
821
+ const valuesByName = Object.fromEntries(
822
+ properties
823
+ .filter((property) => property.definition.type !== "formula")
824
+ .map((property) => [property.definition.name, property.value]),
825
+ );
826
+ return [
827
+ {
828
+ id: item.id,
829
+ databaseId: item.databaseId,
830
+ document: { title: document.title },
831
+ properties: properties.map((property) =>
832
+ property.definition.type === "formula"
833
+ ? {
834
+ ...property,
835
+ value: evaluatePropertyFormula(
836
+ property.definition.options.formula,
837
+ valuesByName,
838
+ ),
839
+ }
840
+ : property,
841
+ ),
842
+ } as ContentDatabaseItem,
843
+ ];
844
+ });
845
+ const constrainedCandidates = applyContentDatabaseTableQuery(
846
+ candidateItems,
847
+ databaseProperties,
848
+ serverTableQuery,
849
+ );
850
+ boundedTableQueryTotal = constrainedCandidates.length;
851
+ const pageItemIds = new Set(
852
+ limit === null
853
+ ? constrainedCandidates.map((item) => item.id)
854
+ : constrainedCandidates
855
+ .slice(offset, offset + limit)
856
+ .map((item) => item.id),
857
+ );
858
+ const itemById = new Map(items.map((item) => [item.id, item]));
859
+ items = [...pageItemIds].flatMap((itemId) => {
860
+ const item = itemById.get(itemId);
861
+ return item ? [item] : [];
862
+ });
863
+ }
442
864
 
443
865
  const documents =
444
866
  items.length > 0
@@ -542,7 +964,6 @@ export async function getContentDatabaseResponse(
542
964
  // every document field it consumes except the deliberately omitted body.
543
965
  documents as Array<typeof schema.documents.$inferSelect>,
544
966
  );
545
- const databaseProperties = await listPropertiesForDatabase(databaseId);
546
967
  const filesProjection = await filesSystemPropertyProjection({
547
968
  database,
548
969
  documents,
@@ -586,12 +1007,12 @@ export async function getContentDatabaseResponse(
586
1007
  )
587
1008
  : new Set<string>();
588
1009
 
589
- const serializedItems = [];
1010
+ const serializedCandidateItems = [];
590
1011
  for (const item of items) {
591
1012
  const document = documentById.get(item.documentId);
592
1013
  if (!document) continue;
593
1014
  const bodyHydrationQueued = queuedBodyHydrationItemIds.has(item.id);
594
- serializedItems.push({
1015
+ serializedCandidateItems.push({
595
1016
  id: item.id,
596
1017
  databaseId: item.databaseId,
597
1018
  document: serializeDocument(
@@ -612,7 +1033,30 @@ export async function getContentDatabaseResponse(
612
1033
  });
613
1034
  }
614
1035
 
615
- const sourceSnapshots = await getAllContentDatabaseSourceSnapshots(database);
1036
+ const constrainedItems =
1037
+ serverTableQuery && boundedTableQueryTotal === null
1038
+ ? applyContentDatabaseTableQuery(
1039
+ serializedCandidateItems,
1040
+ responseProperties,
1041
+ serverTableQuery,
1042
+ )
1043
+ : serializedCandidateItems;
1044
+ const serializedItems =
1045
+ boundedTableQueryTotal !== null
1046
+ ? serializedCandidateItems
1047
+ : serverTableQuery && limit !== null
1048
+ ? constrainedItems.slice(offset, offset + limit)
1049
+ : constrainedItems;
1050
+
1051
+ const serializedDocumentIds = new Set(
1052
+ serializedItems.map((item) => item.document.id),
1053
+ );
1054
+ const sourceSnapshots =
1055
+ options.includeSources === false
1056
+ ? []
1057
+ : await getAllContentDatabaseSourceSnapshots(database, {
1058
+ documentIds: limit !== null ? [...serializedDocumentIds] : undefined,
1059
+ });
616
1060
  const organizationVisibleDocumentIds = organizationFilesItemFilter
617
1061
  ? new Set(
618
1062
  (
@@ -631,25 +1075,26 @@ export async function getContentDatabaseResponse(
631
1075
  ),
632
1076
  )
633
1077
  : sourceSnapshots;
634
- const serializedDocumentIds = new Set(
635
- serializedItems.map((item) => item.document.id),
636
- );
637
- // When paginating, scope every DOCUMENT-BACKED source's rows to the visible
638
- // page, plus the small set referenced by actionable reviews. The dialog gets
639
- // change sets independently of the item page and needs those rows to retain
640
- // the linked provider target instead of misclassifying an off-page update as
641
- // a create. Federated join rows carry no document (empty documentId), so
642
- // they're kept intact — only matched ones overlay anyway.
1078
+ // Keep the returned source overlay aligned to the visible item page.
1079
+ // Secondary federation sources stay complete until their join-key lookup can
1080
+ // be bounded independently; only matched rows overlay the returned items.
643
1081
  const pagedSources =
644
1082
  limit !== null
645
- ? sources.map((source) => ({
646
- ...source,
647
- rows: filterContentDatabaseSourceRowsForPage({
1083
+ ? sources.map((source) => {
1084
+ const rows = filterContentDatabaseSourceRowsForPage({
648
1085
  rows: source.rows,
649
1086
  changeSets: source.changeSets,
650
1087
  visibleDocumentIds: serializedDocumentIds,
651
- }),
652
- }))
1088
+ });
1089
+ return {
1090
+ ...source,
1091
+ rows,
1092
+ projection: {
1093
+ rows: "page" as const,
1094
+ changeSets: source.projection?.changeSets ?? "complete",
1095
+ },
1096
+ };
1097
+ })
653
1098
  : sources;
654
1099
  const pagedPrimary = pagedSources[0] ?? null;
655
1100
 
@@ -660,12 +1105,8 @@ export async function getContentDatabaseResponse(
660
1105
  // Opt-in federated columns (a secondary field the user added via the picker)
661
1106
  // get their per-row values from the matched overlay at read time.
662
1107
  const itemsWithOverlay = applyFederatedOverlayValues(federatedItems);
663
-
664
1108
  return {
665
- database: serializeDatabase(database, databaseDocument?.description ?? ""),
666
- contextPath: databaseDocument
667
- ? await getDocumentContextPath(databaseDocument)
668
- : [],
1109
+ databaseRecord: database,
669
1110
  properties: responseProperties,
670
1111
  items: itemsWithOverlay,
671
1112
  source: pagedPrimary,
@@ -675,12 +1116,65 @@ export async function getContentDatabaseResponse(
675
1116
  ? {
676
1117
  offset,
677
1118
  limit,
678
- totalItems: Number(itemCount?.count ?? 0),
1119
+ totalItems:
1120
+ boundedTableQueryTotal ??
1121
+ (serverTableQuery ? constrainedItems.length : totalVisibleItems),
679
1122
  returnedItems: serializedItems.length,
680
1123
  hasMore:
681
- offset + serializedItems.length < Number(itemCount?.count ?? 0),
1124
+ offset + serializedItems.length <
1125
+ (boundedTableQueryTotal ??
1126
+ (serverTableQuery
1127
+ ? constrainedItems.length
1128
+ : totalVisibleItems)),
682
1129
  }
683
1130
  : undefined,
1131
+ tableQueryMode,
1132
+ hydratedItemCount: documents.length,
1133
+ };
1134
+ }
1135
+
1136
+ export async function getContentDatabaseResponse(
1137
+ databaseId: string,
1138
+ options: {
1139
+ limit?: number;
1140
+ offset?: number;
1141
+ tableQuery?: ContentDatabaseTableQuery;
1142
+ includeSources?: boolean;
1143
+ documentIds?: string[];
1144
+ database?: typeof schema.contentDatabases.$inferSelect;
1145
+ } = {},
1146
+ ): Promise<ContentDatabaseResponse> {
1147
+ const page = await getContentDatabasePageResponse(databaseId, options);
1148
+ const db = getDb();
1149
+ const [databaseDocument] = await db
1150
+ .select({
1151
+ id: schema.documents.id,
1152
+ parentId: schema.documents.parentId,
1153
+ description: schema.documents.description,
1154
+ })
1155
+ .from(schema.documents)
1156
+ .where(
1157
+ and(
1158
+ eq(schema.documents.id, page.databaseRecord.documentId),
1159
+ accessFilter(schema.documents, schema.documentShares),
1160
+ ),
1161
+ );
1162
+ const contextPath = databaseDocument
1163
+ ? await getDocumentContextPath(databaseDocument)
1164
+ : [];
1165
+
1166
+ return {
1167
+ database: serializeDatabase(
1168
+ page.databaseRecord,
1169
+ databaseDocument?.description ?? "",
1170
+ ),
1171
+ contextPath,
1172
+ properties: page.properties,
1173
+ items: page.items,
1174
+ source: page.source,
1175
+ sources: page.sources,
1176
+ pagination: page.pagination,
1177
+ tableQueryMode: page.tableQueryMode,
684
1178
  };
685
1179
  }
686
1180