@agent-native/core 0.133.2 → 0.133.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/use-action.ts +6 -0
- package/corpus/templates/content/actions/_batch-utils.ts +33 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -81
- package/corpus/templates/content/actions/_content-files.ts +31 -21
- package/corpus/templates/content/actions/_database-source-utils.ts +653 -155
- package/corpus/templates/content/actions/_database-utils.ts +535 -41
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +291 -240
- package/corpus/templates/content/actions/add-database-item.ts +15 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +156 -58
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +2 -2
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +4 -1
- package/corpus/templates/content/actions/delete-database-items.ts +8 -2
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +2 -2
- package/corpus/templates/content/actions/duplicate-database-item.ts +15 -1
- package/corpus/templates/content/actions/duplicate-database-items.ts +19 -1
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +2 -1
- package/corpus/templates/content/actions/get-content-database.ts +12 -64
- package/corpus/templates/content/actions/move-database-item.ts +4 -1
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +1 -1
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +4 -8
- package/corpus/templates/content/actions/preview-content-database-source-attach.ts +92 -0
- package/corpus/templates/content/actions/process-builder-body-hydration.ts +2 -1
- package/corpus/templates/content/actions/query-content-database-items.ts +64 -0
- package/corpus/templates/content/actions/refresh-content-database-source.ts +7 -0
- package/corpus/templates/content/actions/review-content-database-source-change-set.ts +1 -1
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +1 -1
- package/corpus/templates/content/actions/stage-builder-revision.ts +1 -1
- package/corpus/templates/content/actions/update-content-database-personal-view.ts +29 -3
- package/corpus/templates/content/actions/update-content-database-view.ts +1 -1
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +1 -1
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +3 -22
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +267 -105
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +82 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +285 -24
- package/corpus/templates/content/app/hooks/use-document-properties.ts +7 -6
- package/corpus/templates/content/app/i18n-data.ts +10 -0
- package/corpus/templates/content/changelog/2026-07-29-large-databases-keep-useful-rows-visible.md +6 -0
- package/corpus/templates/content/changelog/2026-07-30-builder-source-columns-now-appear-immediately-when-connected.md +6 -0
- package/corpus/templates/content/changelog/2026-07-30-large-builder-backed-tables-show-useful-rows-sooner-and-fini.md +6 -0
- package/corpus/templates/content/changelog/2026-08-01-large-builder-databases-now-show-rows-immediately-and-finish.md +6 -0
- package/corpus/templates/content/parity/matrix.md +2 -1
- package/corpus/templates/content/parity/matrix.ts +25 -0
- package/corpus/templates/content/shared/api.ts +54 -4
- package/corpus/templates/content/shared/database-query.ts +359 -0
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +6 -0
- package/dist/client/use-action.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
- package/src/client/use-action.ts +6 -0
|
@@ -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
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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(
|
|
692
|
+
.orderBy(
|
|
693
|
+
asc(schema.contentDatabaseItems.position),
|
|
694
|
+
asc(schema.contentDatabaseItems.createdAt),
|
|
695
|
+
asc(schema.contentDatabaseItems.id),
|
|
696
|
+
)
|
|
437
697
|
.$dynamic();
|
|
438
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1119
|
+
totalItems:
|
|
1120
|
+
boundedTableQueryTotal ??
|
|
1121
|
+
(serverTableQuery ? constrainedItems.length : totalVisibleItems),
|
|
679
1122
|
returnedItems: serializedItems.length,
|
|
680
1123
|
hasMore:
|
|
681
|
-
offset + serializedItems.length <
|
|
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
|
|