@opengeni/documents 0.2.70 → 0.3.2
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/dist/index.d.ts +36 -7
- package/dist/index.js +224 -59
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +326 -86
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
AddDocumentRequest,
|
|
4
4
|
CreateDocumentBaseRequest,
|
|
5
5
|
Document,
|
|
6
|
+
DocumentAuthorityKind,
|
|
6
7
|
DocumentBase,
|
|
7
8
|
DocumentCuration,
|
|
8
9
|
DocumentCurationStatus,
|
|
@@ -13,7 +14,15 @@ import type {
|
|
|
13
14
|
FileAsset,
|
|
14
15
|
KnowledgeSourceKind,
|
|
15
16
|
} from "@opengeni/contracts";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
requireFile,
|
|
19
|
+
rlsContextForWorkspace,
|
|
20
|
+
setSubjectRlsContext,
|
|
21
|
+
withRlsContext,
|
|
22
|
+
withWorkspaceRls,
|
|
23
|
+
withWorkspaceSubjectRls,
|
|
24
|
+
type Database,
|
|
25
|
+
} from "@opengeni/db";
|
|
17
26
|
import * as schema from "@opengeni/db/schema";
|
|
18
27
|
import type { ObjectStorage } from "@opengeni/storage";
|
|
19
28
|
import { LiteParse } from "@llamaindex/liteparse";
|
|
@@ -29,6 +38,7 @@ export const DEFAULT_DOCUMENT_CURATION_MODEL = "gpt-4o-mini";
|
|
|
29
38
|
// Curator input is a preview, not the whole document — enough to name and
|
|
30
39
|
// classify without paying for a full-document prompt on every drop.
|
|
31
40
|
export const DOCUMENT_CURATION_MAX_INPUT_CHARS = 24_000;
|
|
41
|
+
export const DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES = 1024;
|
|
32
42
|
// A base move is applied automatically only at or above this curator
|
|
33
43
|
// confidence; below it the suggestion is surfaced for human review instead.
|
|
34
44
|
export const DOCUMENT_CURATION_AUTO_FILE_CONFIDENCE = 0.75;
|
|
@@ -118,6 +128,12 @@ export type DocumentAccessFilter = {
|
|
|
118
128
|
agentOnly?: boolean | undefined;
|
|
119
129
|
};
|
|
120
130
|
|
|
131
|
+
export type DocumentAuthority = {
|
|
132
|
+
kind: DocumentAuthorityKind;
|
|
133
|
+
workspaceId: string | null;
|
|
134
|
+
subjectId: string | null;
|
|
135
|
+
};
|
|
136
|
+
|
|
121
137
|
export type DocumentInventoryStatusCounts = Record<DocumentStatus, number>;
|
|
122
138
|
export type DocumentInventorySourceKindCounts = Record<KnowledgeSourceKind, number>;
|
|
123
139
|
|
|
@@ -150,6 +166,7 @@ const DOCUMENT_INVENTORY_MAX_TOPIC_LIMIT = 100;
|
|
|
150
166
|
const DOCUMENT_INVENTORY_MAX_TOPIC_CHARS = 256;
|
|
151
167
|
|
|
152
168
|
export type DocumentSearchInput = {
|
|
169
|
+
accountId: string;
|
|
153
170
|
workspaceId: string;
|
|
154
171
|
query: string;
|
|
155
172
|
baseIds?: string[] | undefined;
|
|
@@ -160,6 +177,13 @@ export type DocumentSearchInput = {
|
|
|
160
177
|
access?: DocumentAccessFilter | undefined;
|
|
161
178
|
};
|
|
162
179
|
|
|
180
|
+
export type EffectiveDocumentSearchInput = Omit<DocumentSearchInput, "access"> & {
|
|
181
|
+
/** Immutable human subject accepted for the logical request/turn. */
|
|
182
|
+
initiatingSubjectId: string;
|
|
183
|
+
/** Agent retrieval additionally enforces documents.agent_access. */
|
|
184
|
+
surface: "human" | "agent";
|
|
185
|
+
};
|
|
186
|
+
|
|
163
187
|
export type DocumentIndexHooks = {
|
|
164
188
|
beforeEmbed?: (input: {
|
|
165
189
|
accountId: string;
|
|
@@ -637,7 +661,7 @@ export async function getDocumentInventory(
|
|
|
637
661
|
"topicMaxChars",
|
|
638
662
|
);
|
|
639
663
|
|
|
640
|
-
return await
|
|
664
|
+
return await withDocumentRls(db, workspaceId, input.access, async (scopedDb) => {
|
|
641
665
|
const [baseTotal] = await scopedDb
|
|
642
666
|
.select({ count: sql<number>`count(*)::int` })
|
|
643
667
|
.from(schema.documentBases)
|
|
@@ -646,7 +670,7 @@ export async function getDocumentInventory(
|
|
|
646
670
|
const baseJoin = and(
|
|
647
671
|
eq(schema.documents.workspaceId, workspaceId),
|
|
648
672
|
eq(schema.documents.baseId, schema.documentBases.id),
|
|
649
|
-
...documentAccessConditions(input.access),
|
|
673
|
+
...documentAccessConditions(workspaceId, input.access),
|
|
650
674
|
);
|
|
651
675
|
const baseRows = await scopedDb
|
|
652
676
|
.select({
|
|
@@ -668,7 +692,7 @@ export async function getDocumentInventory(
|
|
|
668
692
|
|
|
669
693
|
const documentWhere = and(
|
|
670
694
|
eq(schema.documents.workspaceId, workspaceId),
|
|
671
|
-
...documentAccessConditions(input.access),
|
|
695
|
+
...documentAccessConditions(workspaceId, input.access),
|
|
672
696
|
);
|
|
673
697
|
const [summary] = await scopedDb
|
|
674
698
|
.select({
|
|
@@ -859,6 +883,8 @@ export async function addDocumentToBase(
|
|
|
859
883
|
workspaceId: string;
|
|
860
884
|
baseId: string;
|
|
861
885
|
createdBy?: string | null | undefined;
|
|
886
|
+
initiatingSubjectId?: string | null | undefined;
|
|
887
|
+
organizationAuthorityGranted?: boolean | undefined;
|
|
862
888
|
curationStatus?: DocumentCurationStatus | undefined;
|
|
863
889
|
access?: DocumentAccessFilter | undefined;
|
|
864
890
|
},
|
|
@@ -867,12 +893,27 @@ export async function addDocumentToBase(
|
|
|
867
893
|
db,
|
|
868
894
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
869
895
|
async (scopedDb) => {
|
|
896
|
+
const viewerSubjectId = cleanString(input.access?.viewerSubjectId ?? null);
|
|
897
|
+
const authority = resolveDocumentAuthority({
|
|
898
|
+
kind: input.authorityKind,
|
|
899
|
+
legacyVisibility: input.visibility,
|
|
900
|
+
workspaceId: input.workspaceId,
|
|
901
|
+
initiatingSubjectId: input.initiatingSubjectId,
|
|
902
|
+
});
|
|
903
|
+
if (authority.kind === "organization" && input.organizationAuthorityGranted !== true) {
|
|
904
|
+
throw new Error("organization document writes require exact account authority");
|
|
905
|
+
}
|
|
906
|
+
if (
|
|
907
|
+
authority.kind === "personal" &&
|
|
908
|
+
(viewerSubjectId !== authority.subjectId ||
|
|
909
|
+
cleanString(input.createdBy ?? null) !== authority.subjectId)
|
|
910
|
+
) {
|
|
911
|
+
throw new Error("personal document writes require the exact initiating subject");
|
|
912
|
+
}
|
|
913
|
+
if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
|
|
870
914
|
const base = await getDocumentBase(scopedDb, input.workspaceId, input.baseId);
|
|
871
915
|
if (!base) throw new Error(`Document base not found: ${input.baseId}`);
|
|
872
916
|
const file = await requireReadyFile(scopedDb, input.workspaceId, input.fileId);
|
|
873
|
-
if (input.visibility === "private" && !cleanString(input.createdBy ?? null)) {
|
|
874
|
-
throw new Error("private documents require a creating subject");
|
|
875
|
-
}
|
|
876
917
|
const now = new Date();
|
|
877
918
|
const [existing] = await scopedDb
|
|
878
919
|
.select()
|
|
@@ -889,6 +930,10 @@ export async function addDocumentToBase(
|
|
|
889
930
|
if (!documentMatchesAccess(existing, input.access)) {
|
|
890
931
|
throw new Error(`Document not found: ${existing.id}`);
|
|
891
932
|
}
|
|
933
|
+
assertOrganizationDocumentAuthority(
|
|
934
|
+
existing.authorityKind,
|
|
935
|
+
input.organizationAuthorityGranted,
|
|
936
|
+
);
|
|
892
937
|
// Idempotent re-add: refresh caller-supplied source metadata on the
|
|
893
938
|
// existing row instead of silently discarding it (aclTags especially —
|
|
894
939
|
// a re-add that tightens tags must not be a no-op). Access policy is
|
|
@@ -913,7 +958,7 @@ export async function addDocumentToBase(
|
|
|
913
958
|
and(
|
|
914
959
|
eq(schema.documents.workspaceId, input.workspaceId),
|
|
915
960
|
eq(schema.documents.id, existing.id),
|
|
916
|
-
...documentAccessConditions(input.access),
|
|
961
|
+
...documentAccessConditions(input.workspaceId, input.access),
|
|
917
962
|
),
|
|
918
963
|
)
|
|
919
964
|
.returning();
|
|
@@ -938,7 +983,10 @@ export async function addDocumentToBase(
|
|
|
938
983
|
sourceUpdatedAt: parseOptionalDate(input.sourceUpdatedAt),
|
|
939
984
|
sourceVersion: cleanString(input.sourceVersion) ?? null,
|
|
940
985
|
aclTags: cleanStringArray(input.aclTags),
|
|
941
|
-
|
|
986
|
+
authorityKind: authority.kind,
|
|
987
|
+
authorityWorkspaceId: authority.workspaceId,
|
|
988
|
+
authoritySubjectId: authority.subjectId,
|
|
989
|
+
visibility: authority.kind === "personal" ? "private" : "workspace",
|
|
942
990
|
agentAccess: input.agentAccess ?? true,
|
|
943
991
|
createdBy: input.createdBy ?? null,
|
|
944
992
|
curationStatus: input.curationStatus ?? "none",
|
|
@@ -963,6 +1011,7 @@ export async function moveDocumentToBase(
|
|
|
963
1011
|
workspaceId: string;
|
|
964
1012
|
documentId: string;
|
|
965
1013
|
targetBaseId?: string | null | undefined;
|
|
1014
|
+
organizationAuthorityGranted?: boolean | undefined;
|
|
966
1015
|
access?: DocumentAccessFilter | undefined;
|
|
967
1016
|
},
|
|
968
1017
|
): Promise<Document> {
|
|
@@ -970,6 +1019,8 @@ export async function moveDocumentToBase(
|
|
|
970
1019
|
db,
|
|
971
1020
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
972
1021
|
async (scopedDb) => {
|
|
1022
|
+
const viewerSubjectId = cleanString(input.access?.viewerSubjectId ?? null);
|
|
1023
|
+
if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
|
|
973
1024
|
const [row] = await scopedDb
|
|
974
1025
|
.select()
|
|
975
1026
|
.from(schema.documents)
|
|
@@ -977,11 +1028,12 @@ export async function moveDocumentToBase(
|
|
|
977
1028
|
and(
|
|
978
1029
|
eq(schema.documents.workspaceId, input.workspaceId),
|
|
979
1030
|
eq(schema.documents.id, input.documentId),
|
|
980
|
-
...documentAccessConditions(input.access),
|
|
1031
|
+
...documentAccessConditions(input.workspaceId, input.access),
|
|
981
1032
|
),
|
|
982
1033
|
)
|
|
983
1034
|
.limit(1);
|
|
984
1035
|
if (!row) throw new Error(`Document not found: ${input.documentId}`);
|
|
1036
|
+
assertOrganizationDocumentAuthority(row.authorityKind, input.organizationAuthorityGranted);
|
|
985
1037
|
const suggestion = (row.curation as { suggestedBaseId?: string | null } | null)
|
|
986
1038
|
?.suggestedBaseId;
|
|
987
1039
|
const targetBaseId = input.targetBaseId ?? suggestion;
|
|
@@ -1020,7 +1072,7 @@ export async function moveDocumentToBase(
|
|
|
1020
1072
|
and(
|
|
1021
1073
|
eq(schema.documents.workspaceId, input.workspaceId),
|
|
1022
1074
|
eq(schema.documents.id, input.documentId),
|
|
1023
|
-
...documentAccessConditions(input.access),
|
|
1075
|
+
...documentAccessConditions(input.workspaceId, input.access),
|
|
1024
1076
|
),
|
|
1025
1077
|
)
|
|
1026
1078
|
.returning();
|
|
@@ -1050,6 +1102,7 @@ export async function deleteDocumentFromBase(
|
|
|
1050
1102
|
workspaceId: string;
|
|
1051
1103
|
baseId: string;
|
|
1052
1104
|
documentId: string;
|
|
1105
|
+
organizationAuthorityGranted?: boolean | undefined;
|
|
1053
1106
|
access?: DocumentAccessFilter | undefined;
|
|
1054
1107
|
},
|
|
1055
1108
|
): Promise<void> {
|
|
@@ -1057,6 +1110,8 @@ export async function deleteDocumentFromBase(
|
|
|
1057
1110
|
db,
|
|
1058
1111
|
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
1059
1112
|
async (scopedDb) => {
|
|
1113
|
+
const viewerSubjectId = cleanString(input.access?.viewerSubjectId ?? null);
|
|
1114
|
+
if (viewerSubjectId) await setSubjectRlsContext(scopedDb, viewerSubjectId);
|
|
1060
1115
|
const [document] = await scopedDb
|
|
1061
1116
|
.select()
|
|
1062
1117
|
.from(schema.documents)
|
|
@@ -1064,13 +1119,17 @@ export async function deleteDocumentFromBase(
|
|
|
1064
1119
|
and(
|
|
1065
1120
|
eq(schema.documents.workspaceId, input.workspaceId),
|
|
1066
1121
|
eq(schema.documents.id, input.documentId),
|
|
1067
|
-
...documentAccessConditions(input.access),
|
|
1122
|
+
...documentAccessConditions(input.workspaceId, input.access),
|
|
1068
1123
|
),
|
|
1069
1124
|
)
|
|
1070
1125
|
.limit(1);
|
|
1071
1126
|
if (!document) {
|
|
1072
1127
|
throw new Error(`Document not found: ${input.documentId}`);
|
|
1073
1128
|
}
|
|
1129
|
+
assertOrganizationDocumentAuthority(
|
|
1130
|
+
document.authorityKind,
|
|
1131
|
+
input.organizationAuthorityGranted,
|
|
1132
|
+
);
|
|
1074
1133
|
if (document.baseId !== input.baseId) {
|
|
1075
1134
|
throw new Error(`Document not found: ${input.documentId}`);
|
|
1076
1135
|
}
|
|
@@ -1080,7 +1139,7 @@ export async function deleteDocumentFromBase(
|
|
|
1080
1139
|
and(
|
|
1081
1140
|
eq(schema.documents.workspaceId, input.workspaceId),
|
|
1082
1141
|
eq(schema.documents.id, input.documentId),
|
|
1083
|
-
...documentAccessConditions(input.access),
|
|
1142
|
+
...documentAccessConditions(input.workspaceId, input.access),
|
|
1084
1143
|
),
|
|
1085
1144
|
);
|
|
1086
1145
|
},
|
|
@@ -1093,7 +1152,7 @@ export async function listDocuments(
|
|
|
1093
1152
|
baseId: string,
|
|
1094
1153
|
access?: DocumentAccessFilter,
|
|
1095
1154
|
): Promise<Document[]> {
|
|
1096
|
-
return await
|
|
1155
|
+
return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
|
|
1097
1156
|
const rows = await scopedDb
|
|
1098
1157
|
.select()
|
|
1099
1158
|
.from(schema.documents)
|
|
@@ -1101,7 +1160,7 @@ export async function listDocuments(
|
|
|
1101
1160
|
and(
|
|
1102
1161
|
eq(schema.documents.workspaceId, workspaceId),
|
|
1103
1162
|
eq(schema.documents.baseId, baseId),
|
|
1104
|
-
...documentAccessConditions(access),
|
|
1163
|
+
...documentAccessConditions(workspaceId, access),
|
|
1105
1164
|
),
|
|
1106
1165
|
)
|
|
1107
1166
|
.orderBy(asc(schema.documents.createdAt));
|
|
@@ -1115,7 +1174,7 @@ export async function getDocument(
|
|
|
1115
1174
|
documentId: string,
|
|
1116
1175
|
access?: DocumentAccessFilter,
|
|
1117
1176
|
): Promise<Document | null> {
|
|
1118
|
-
return await
|
|
1177
|
+
return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
|
|
1119
1178
|
const [row] = await scopedDb
|
|
1120
1179
|
.select()
|
|
1121
1180
|
.from(schema.documents)
|
|
@@ -1123,7 +1182,7 @@ export async function getDocument(
|
|
|
1123
1182
|
and(
|
|
1124
1183
|
eq(schema.documents.workspaceId, workspaceId),
|
|
1125
1184
|
eq(schema.documents.id, documentId),
|
|
1126
|
-
...documentAccessConditions(access),
|
|
1185
|
+
...documentAccessConditions(workspaceId, access),
|
|
1127
1186
|
),
|
|
1128
1187
|
)
|
|
1129
1188
|
.limit(1);
|
|
@@ -1136,8 +1195,22 @@ export async function queueDocumentForReindex(
|
|
|
1136
1195
|
workspaceId: string,
|
|
1137
1196
|
documentId: string,
|
|
1138
1197
|
access?: DocumentAccessFilter,
|
|
1198
|
+
organizationAuthorityGranted?: boolean,
|
|
1139
1199
|
): Promise<Document> {
|
|
1140
|
-
return await
|
|
1200
|
+
return await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
|
|
1201
|
+
const [document] = await scopedDb
|
|
1202
|
+
.select({ authorityKind: schema.documents.authorityKind })
|
|
1203
|
+
.from(schema.documents)
|
|
1204
|
+
.where(
|
|
1205
|
+
and(
|
|
1206
|
+
eq(schema.documents.workspaceId, workspaceId),
|
|
1207
|
+
eq(schema.documents.id, documentId),
|
|
1208
|
+
...documentAccessConditions(workspaceId, access),
|
|
1209
|
+
),
|
|
1210
|
+
)
|
|
1211
|
+
.limit(1);
|
|
1212
|
+
if (!document) throw new Error(`Document not found: ${documentId}`);
|
|
1213
|
+
assertOrganizationDocumentAuthority(document.authorityKind, organizationAuthorityGranted);
|
|
1141
1214
|
const [row] = await scopedDb
|
|
1142
1215
|
.update(schema.documents)
|
|
1143
1216
|
.set({
|
|
@@ -1149,7 +1222,7 @@ export async function queueDocumentForReindex(
|
|
|
1149
1222
|
and(
|
|
1150
1223
|
eq(schema.documents.workspaceId, workspaceId),
|
|
1151
1224
|
eq(schema.documents.id, documentId),
|
|
1152
|
-
...documentAccessConditions(access),
|
|
1225
|
+
...documentAccessConditions(workspaceId, access),
|
|
1153
1226
|
),
|
|
1154
1227
|
)
|
|
1155
1228
|
.returning();
|
|
@@ -1158,6 +1231,15 @@ export async function queueDocumentForReindex(
|
|
|
1158
1231
|
});
|
|
1159
1232
|
}
|
|
1160
1233
|
|
|
1234
|
+
function assertOrganizationDocumentAuthority(
|
|
1235
|
+
authorityKind: string,
|
|
1236
|
+
organizationAuthorityGranted: boolean | undefined,
|
|
1237
|
+
): void {
|
|
1238
|
+
if (authorityKind === "organization" && organizationAuthorityGranted !== true) {
|
|
1239
|
+
throw new Error("organization document mutations require exact account authority");
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1161
1243
|
export async function indexDocumentNow(
|
|
1162
1244
|
db: Database,
|
|
1163
1245
|
objectStorage: ObjectStorage,
|
|
@@ -1165,10 +1247,12 @@ export async function indexDocumentNow(
|
|
|
1165
1247
|
documentId: string,
|
|
1166
1248
|
services: DocumentServices = createDocumentServices(),
|
|
1167
1249
|
hooks: DocumentIndexHooks = {},
|
|
1250
|
+
access?: DocumentAccessFilter,
|
|
1168
1251
|
): Promise<Document> {
|
|
1169
|
-
const [loadedDocument] = await
|
|
1252
|
+
const [loadedDocument] = await withDocumentRls(
|
|
1170
1253
|
db,
|
|
1171
1254
|
workspaceId,
|
|
1255
|
+
access,
|
|
1172
1256
|
async (scopedDb) =>
|
|
1173
1257
|
await scopedDb
|
|
1174
1258
|
.select()
|
|
@@ -1181,7 +1265,7 @@ export async function indexDocumentNow(
|
|
|
1181
1265
|
if (!loadedDocument) throw new Error(`Document not found: ${documentId}`);
|
|
1182
1266
|
let document: DocumentRow = loadedDocument;
|
|
1183
1267
|
const file = await requireReadyFile(db, workspaceId, document.fileId);
|
|
1184
|
-
await
|
|
1268
|
+
await withDocumentRls(db, workspaceId, access, async (scopedDb) => {
|
|
1185
1269
|
await scopedDb
|
|
1186
1270
|
.update(schema.documents)
|
|
1187
1271
|
.set({
|
|
@@ -1217,9 +1301,10 @@ export async function indexDocumentNow(
|
|
|
1217
1301
|
`Embedding provider returned ${embeddings.length} embeddings for ${chunks.length} chunks`,
|
|
1218
1302
|
);
|
|
1219
1303
|
}
|
|
1220
|
-
await
|
|
1304
|
+
await withDocumentRls(
|
|
1221
1305
|
db,
|
|
1222
1306
|
workspaceId,
|
|
1307
|
+
access,
|
|
1223
1308
|
async (scopedDb) =>
|
|
1224
1309
|
await scopedDb.transaction(async (tx) => {
|
|
1225
1310
|
await tx
|
|
@@ -1238,6 +1323,9 @@ export async function indexDocumentNow(
|
|
|
1238
1323
|
documentId,
|
|
1239
1324
|
baseId: document.baseId,
|
|
1240
1325
|
fileId: file.id,
|
|
1326
|
+
authorityKind: document.authorityKind,
|
|
1327
|
+
authorityWorkspaceId: document.authorityWorkspaceId,
|
|
1328
|
+
authoritySubjectId: document.authoritySubjectId,
|
|
1241
1329
|
chunkIndex: index,
|
|
1242
1330
|
text: chunk.text,
|
|
1243
1331
|
metadata: {
|
|
@@ -1280,9 +1368,10 @@ export async function indexDocumentNow(
|
|
|
1280
1368
|
}),
|
|
1281
1369
|
);
|
|
1282
1370
|
} catch (error) {
|
|
1283
|
-
const [failed] = await
|
|
1371
|
+
const [failed] = await withDocumentRls(
|
|
1284
1372
|
db,
|
|
1285
1373
|
workspaceId,
|
|
1374
|
+
access,
|
|
1286
1375
|
async (scopedDb) =>
|
|
1287
1376
|
await scopedDb
|
|
1288
1377
|
.update(schema.documents)
|
|
@@ -1303,7 +1392,7 @@ export async function indexDocumentNow(
|
|
|
1303
1392
|
// that created/queued it. Public reads remain fail-closed when no subject is
|
|
1304
1393
|
// supplied; the creator subject is the document's frozen access principal.
|
|
1305
1394
|
const updated = await getDocument(db, workspaceId, documentId, {
|
|
1306
|
-
viewerSubjectId: document.
|
|
1395
|
+
viewerSubjectId: document.authoritySubjectId,
|
|
1307
1396
|
});
|
|
1308
1397
|
if (!updated) throw new Error(`Document disappeared after indexing: ${documentId}`);
|
|
1309
1398
|
return updated;
|
|
@@ -1322,9 +1411,10 @@ async function curateDroppedDocument(
|
|
|
1322
1411
|
// it with heuristics: indexing still proceeds, but the drop remains an
|
|
1323
1412
|
// ordinary uncured document with its caller-supplied title and metadata.
|
|
1324
1413
|
if (!services.curator) {
|
|
1325
|
-
const [updated] = await
|
|
1414
|
+
const [updated] = await withDocumentRls(
|
|
1326
1415
|
db,
|
|
1327
1416
|
document.workspaceId,
|
|
1417
|
+
{ viewerSubjectId: document.authoritySubjectId },
|
|
1328
1418
|
async (scopedDb) =>
|
|
1329
1419
|
await scopedDb
|
|
1330
1420
|
.update(schema.documents)
|
|
@@ -1380,9 +1470,10 @@ async function curateDroppedDocument(
|
|
|
1380
1470
|
) {
|
|
1381
1471
|
// The (workspace, base, file) unique index means a same-file twin already
|
|
1382
1472
|
// in the target base blocks the move; keep it as a suggestion instead.
|
|
1383
|
-
const conflict = await
|
|
1473
|
+
const conflict = await withDocumentRls(
|
|
1384
1474
|
db,
|
|
1385
1475
|
document.workspaceId,
|
|
1476
|
+
{ viewerSubjectId: document.authoritySubjectId },
|
|
1386
1477
|
async (scopedDb) =>
|
|
1387
1478
|
await scopedDb
|
|
1388
1479
|
.select({ id: schema.documents.id })
|
|
@@ -1413,9 +1504,10 @@ async function curateDroppedDocument(
|
|
|
1413
1504
|
: moveToBaseId
|
|
1414
1505
|
? "auto_filed"
|
|
1415
1506
|
: "suggested";
|
|
1416
|
-
const [updated] = await
|
|
1507
|
+
const [updated] = await withDocumentRls(
|
|
1417
1508
|
db,
|
|
1418
1509
|
document.workspaceId,
|
|
1510
|
+
{ viewerSubjectId: document.authoritySubjectId },
|
|
1419
1511
|
async (scopedDb) =>
|
|
1420
1512
|
await scopedDb
|
|
1421
1513
|
.update(schema.documents)
|
|
@@ -1445,6 +1537,7 @@ export async function searchDocuments(
|
|
|
1445
1537
|
input: DocumentSearchInput,
|
|
1446
1538
|
services: Pick<DocumentServices, "embedder"> = createDocumentServices(),
|
|
1447
1539
|
): Promise<DocumentSearchResult[]> {
|
|
1540
|
+
await assertDocumentAccountWorkspace(db, input.accountId, input.workspaceId);
|
|
1448
1541
|
const mode = input.mode ?? "hybrid";
|
|
1449
1542
|
const limit = Math.min(Math.max(input.limit ?? 5, 1), 50);
|
|
1450
1543
|
const candidateLimit = mode === "hybrid" ? Math.min(limit * 4, 100) : limit;
|
|
@@ -1471,6 +1564,42 @@ export async function searchDocuments(
|
|
|
1471
1564
|
return mergeDocumentSearchRows(rows, mode).slice(0, limit);
|
|
1472
1565
|
}
|
|
1473
1566
|
|
|
1567
|
+
/**
|
|
1568
|
+
* Canonical effective retrieval composition for API, SDK, and MCP surfaces.
|
|
1569
|
+
* Callers supply the already-authorized immutable initiating subject; no
|
|
1570
|
+
* request/tool input can replace it with another user's personal authority.
|
|
1571
|
+
*/
|
|
1572
|
+
export async function searchEffectiveDocuments(
|
|
1573
|
+
db: Database,
|
|
1574
|
+
input: EffectiveDocumentSearchInput,
|
|
1575
|
+
services: Pick<DocumentServices, "embedder"> = createDocumentServices(),
|
|
1576
|
+
): Promise<DocumentSearchResult[]> {
|
|
1577
|
+
const initiatingSubjectId = cleanString(input.initiatingSubjectId);
|
|
1578
|
+
if (!initiatingSubjectId) {
|
|
1579
|
+
throw new Error("effective document retrieval requires an initiating subject");
|
|
1580
|
+
}
|
|
1581
|
+
return await searchDocuments(
|
|
1582
|
+
db,
|
|
1583
|
+
{
|
|
1584
|
+
accountId: input.accountId,
|
|
1585
|
+
workspaceId: input.workspaceId,
|
|
1586
|
+
query: input.query,
|
|
1587
|
+
baseIds: input.baseIds,
|
|
1588
|
+
limit: input.limit,
|
|
1589
|
+
mode: input.mode,
|
|
1590
|
+
sourceKinds: input.sourceKinds,
|
|
1591
|
+
aclTags: input.aclTags,
|
|
1592
|
+
// Construct the lower-level access filter here instead of spreading the
|
|
1593
|
+
// caller input, so an untyped/legacy access override is always ignored.
|
|
1594
|
+
access: {
|
|
1595
|
+
viewerSubjectId: initiatingSubjectId,
|
|
1596
|
+
...(input.surface === "agent" ? { agentOnly: true } : {}),
|
|
1597
|
+
},
|
|
1598
|
+
},
|
|
1599
|
+
services,
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1474
1603
|
async function vectorSearchDocuments(
|
|
1475
1604
|
db: Database,
|
|
1476
1605
|
input: DocumentSearchInput,
|
|
@@ -1480,13 +1609,16 @@ async function vectorSearchDocuments(
|
|
|
1480
1609
|
const queryEmbedding = await services.embedder.embedQuery(input.query);
|
|
1481
1610
|
validateEmbedding(queryEmbedding, services.embedder.dimensions, services.embedder.model);
|
|
1482
1611
|
const distance = sql<number>`${schema.documentChunks.embedding} <=> ${vectorLiteral(queryEmbedding)}::vector`;
|
|
1483
|
-
const rows = await
|
|
1612
|
+
const rows = await withDocumentAccountRls(
|
|
1484
1613
|
db,
|
|
1614
|
+
input.accountId,
|
|
1485
1615
|
input.workspaceId,
|
|
1616
|
+
input.access,
|
|
1486
1617
|
async (scopedDb) =>
|
|
1487
1618
|
await scopedDb
|
|
1488
1619
|
.select({
|
|
1489
1620
|
chunkId: schema.documentChunks.id,
|
|
1621
|
+
workspaceId: schema.documentChunks.workspaceId,
|
|
1490
1622
|
documentId: schema.documentChunks.documentId,
|
|
1491
1623
|
baseId: schema.documentChunks.baseId,
|
|
1492
1624
|
fileId: schema.documentChunks.fileId,
|
|
@@ -1503,6 +1635,9 @@ async function vectorSearchDocuments(
|
|
|
1503
1635
|
sourceUpdatedAt: schema.documents.sourceUpdatedAt,
|
|
1504
1636
|
sourceVersion: schema.documents.sourceVersion,
|
|
1505
1637
|
aclTags: schema.documents.aclTags,
|
|
1638
|
+
authorityKind: schema.documents.authorityKind,
|
|
1639
|
+
authorityWorkspaceId: schema.documents.authorityWorkspaceId,
|
|
1640
|
+
authoritySubjectId: schema.documents.authoritySubjectId,
|
|
1506
1641
|
distance,
|
|
1507
1642
|
})
|
|
1508
1643
|
.from(schema.documentChunks)
|
|
@@ -1512,7 +1647,7 @@ async function vectorSearchDocuments(
|
|
|
1512
1647
|
.limit(limit),
|
|
1513
1648
|
);
|
|
1514
1649
|
return rows.map((row) => ({
|
|
1515
|
-
...mapSearchRowBase(row
|
|
1650
|
+
...mapSearchRowBase(row),
|
|
1516
1651
|
vectorScore: 1 / (1 + Number(row.distance)),
|
|
1517
1652
|
keywordScore: null,
|
|
1518
1653
|
}));
|
|
@@ -1524,13 +1659,16 @@ async function keywordSearchDocuments(
|
|
|
1524
1659
|
limit: number,
|
|
1525
1660
|
): Promise<CombinedSearchRow[]> {
|
|
1526
1661
|
const rank = sql<number>`ts_rank_cd(to_tsvector('simple', ${schema.documentChunks.text}), plainto_tsquery('simple', ${input.query}))`;
|
|
1527
|
-
const rows = await
|
|
1662
|
+
const rows = await withDocumentAccountRls(
|
|
1528
1663
|
db,
|
|
1664
|
+
input.accountId,
|
|
1529
1665
|
input.workspaceId,
|
|
1666
|
+
input.access,
|
|
1530
1667
|
async (scopedDb) =>
|
|
1531
1668
|
await scopedDb
|
|
1532
1669
|
.select({
|
|
1533
1670
|
chunkId: schema.documentChunks.id,
|
|
1671
|
+
workspaceId: schema.documentChunks.workspaceId,
|
|
1534
1672
|
documentId: schema.documentChunks.documentId,
|
|
1535
1673
|
baseId: schema.documentChunks.baseId,
|
|
1536
1674
|
fileId: schema.documentChunks.fileId,
|
|
@@ -1547,6 +1685,9 @@ async function keywordSearchDocuments(
|
|
|
1547
1685
|
sourceUpdatedAt: schema.documents.sourceUpdatedAt,
|
|
1548
1686
|
sourceVersion: schema.documents.sourceVersion,
|
|
1549
1687
|
aclTags: schema.documents.aclTags,
|
|
1688
|
+
authorityKind: schema.documents.authorityKind,
|
|
1689
|
+
authorityWorkspaceId: schema.documents.authorityWorkspaceId,
|
|
1690
|
+
authoritySubjectId: schema.documents.authoritySubjectId,
|
|
1550
1691
|
rank,
|
|
1551
1692
|
})
|
|
1552
1693
|
.from(schema.documentChunks)
|
|
@@ -1561,7 +1702,7 @@ async function keywordSearchDocuments(
|
|
|
1561
1702
|
.limit(limit),
|
|
1562
1703
|
);
|
|
1563
1704
|
return rows.map((row) => ({
|
|
1564
|
-
...mapSearchRowBase(row
|
|
1705
|
+
...mapSearchRowBase(row),
|
|
1565
1706
|
vectorScore: null,
|
|
1566
1707
|
keywordScore: normalizeKeywordScore(Number(row.rank)),
|
|
1567
1708
|
}));
|
|
@@ -1569,17 +1710,21 @@ async function keywordSearchDocuments(
|
|
|
1569
1710
|
|
|
1570
1711
|
export async function getDocumentChunk(
|
|
1571
1712
|
db: Database,
|
|
1713
|
+
accountId: string,
|
|
1572
1714
|
workspaceId: string,
|
|
1573
1715
|
chunkId: string,
|
|
1574
1716
|
access?: DocumentAccessFilter,
|
|
1575
1717
|
): Promise<DocumentSearchResult | null> {
|
|
1576
|
-
const [row] = await
|
|
1718
|
+
const [row] = await withDocumentAccountRls(
|
|
1577
1719
|
db,
|
|
1720
|
+
accountId,
|
|
1578
1721
|
workspaceId,
|
|
1722
|
+
access,
|
|
1579
1723
|
async (scopedDb) =>
|
|
1580
1724
|
await scopedDb
|
|
1581
1725
|
.select({
|
|
1582
1726
|
chunkId: schema.documentChunks.id,
|
|
1727
|
+
workspaceId: schema.documentChunks.workspaceId,
|
|
1583
1728
|
documentId: schema.documentChunks.documentId,
|
|
1584
1729
|
baseId: schema.documentChunks.baseId,
|
|
1585
1730
|
fileId: schema.documentChunks.fileId,
|
|
@@ -1596,22 +1741,26 @@ export async function getDocumentChunk(
|
|
|
1596
1741
|
sourceUpdatedAt: schema.documents.sourceUpdatedAt,
|
|
1597
1742
|
sourceVersion: schema.documents.sourceVersion,
|
|
1598
1743
|
aclTags: schema.documents.aclTags,
|
|
1744
|
+
authorityKind: schema.documents.authorityKind,
|
|
1745
|
+
authorityWorkspaceId: schema.documents.authorityWorkspaceId,
|
|
1746
|
+
authoritySubjectId: schema.documents.authoritySubjectId,
|
|
1599
1747
|
})
|
|
1600
1748
|
.from(schema.documentChunks)
|
|
1601
1749
|
.innerJoin(schema.documents, eq(schema.documentChunks.documentId, schema.documents.id))
|
|
1602
1750
|
.where(
|
|
1603
1751
|
and(
|
|
1604
|
-
eq(schema.
|
|
1752
|
+
eq(schema.documents.accountId, accountId),
|
|
1753
|
+
eq(schema.documentChunks.accountId, accountId),
|
|
1605
1754
|
eq(schema.documentChunks.id, chunkId),
|
|
1606
1755
|
eq(schema.documents.status, "ready"),
|
|
1607
|
-
...documentAccessConditions(access),
|
|
1756
|
+
...documentAccessConditions(workspaceId, access),
|
|
1608
1757
|
),
|
|
1609
1758
|
)
|
|
1610
1759
|
.limit(1),
|
|
1611
1760
|
);
|
|
1612
1761
|
if (!row) return null;
|
|
1613
1762
|
return {
|
|
1614
|
-
...mapSearchRowBase(row
|
|
1763
|
+
...mapSearchRowBase(row),
|
|
1615
1764
|
score: 1,
|
|
1616
1765
|
matchType: "hybrid",
|
|
1617
1766
|
vectorScore: null,
|
|
@@ -1628,65 +1777,139 @@ type CombinedSearchRow = SearchRowBase & {
|
|
|
1628
1777
|
keywordScore: number | null;
|
|
1629
1778
|
};
|
|
1630
1779
|
|
|
1780
|
+
export function resolveDocumentAuthority(input: {
|
|
1781
|
+
kind?: DocumentAuthorityKind | undefined;
|
|
1782
|
+
legacyVisibility?: DocumentVisibility | undefined;
|
|
1783
|
+
workspaceId: string;
|
|
1784
|
+
initiatingSubjectId?: string | null | undefined;
|
|
1785
|
+
}): DocumentAuthority {
|
|
1786
|
+
const legacyKind = input.legacyVisibility === "private" ? "personal" : "workspace";
|
|
1787
|
+
const kind = input.kind ?? legacyKind;
|
|
1788
|
+
if (
|
|
1789
|
+
input.kind &&
|
|
1790
|
+
input.legacyVisibility &&
|
|
1791
|
+
(input.legacyVisibility === "private") !== (input.kind === "personal")
|
|
1792
|
+
) {
|
|
1793
|
+
throw new Error("document authorityKind conflicts with legacy visibility");
|
|
1794
|
+
}
|
|
1795
|
+
if (kind === "organization") {
|
|
1796
|
+
return { kind, workspaceId: null, subjectId: null };
|
|
1797
|
+
}
|
|
1798
|
+
if (kind === "workspace") {
|
|
1799
|
+
return { kind, workspaceId: input.workspaceId, subjectId: null };
|
|
1800
|
+
}
|
|
1801
|
+
const subjectId = cleanString(input.initiatingSubjectId ?? null);
|
|
1802
|
+
if (!subjectId) throw new Error("personal documents require an initiating subject");
|
|
1803
|
+
if (new TextEncoder().encode(subjectId).byteLength > DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES) {
|
|
1804
|
+
throw new Error(
|
|
1805
|
+
`personal document initiating subject exceeds ${DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES} UTF-8 bytes`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
return { kind, workspaceId: input.workspaceId, subjectId };
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
async function withDocumentRls<T>(
|
|
1812
|
+
db: Database,
|
|
1813
|
+
workspaceId: string,
|
|
1814
|
+
access: DocumentAccessFilter | undefined,
|
|
1815
|
+
fn: (db: Database) => Promise<T>,
|
|
1816
|
+
): Promise<T> {
|
|
1817
|
+
const subjectId = cleanString(access?.viewerSubjectId ?? null);
|
|
1818
|
+
return subjectId
|
|
1819
|
+
? await withWorkspaceSubjectRls(db, workspaceId, subjectId, fn)
|
|
1820
|
+
: await withWorkspaceRls(db, workspaceId, fn);
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
async function withDocumentAccountRls<T>(
|
|
1824
|
+
db: Database,
|
|
1825
|
+
accountId: string,
|
|
1826
|
+
workspaceId: string,
|
|
1827
|
+
access: DocumentAccessFilter | undefined,
|
|
1828
|
+
fn: (db: Database) => Promise<T>,
|
|
1829
|
+
): Promise<T> {
|
|
1830
|
+
const context = await assertDocumentAccountWorkspace(db, accountId, workspaceId);
|
|
1831
|
+
const subjectId = cleanString(access?.viewerSubjectId ?? null);
|
|
1832
|
+
return await withRlsContext(db, context, async (scopedDb) => {
|
|
1833
|
+
if (subjectId) await setSubjectRlsContext(scopedDb, subjectId);
|
|
1834
|
+
return await fn(scopedDb);
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
async function assertDocumentAccountWorkspace(
|
|
1839
|
+
db: Database,
|
|
1840
|
+
accountId: string,
|
|
1841
|
+
workspaceId: string,
|
|
1842
|
+
) {
|
|
1843
|
+
const context = await rlsContextForWorkspace(db, workspaceId);
|
|
1844
|
+
if (context.accountId !== accountId) {
|
|
1845
|
+
throw new Error("document account/workspace authority mismatch");
|
|
1846
|
+
}
|
|
1847
|
+
return context;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1631
1850
|
/**
|
|
1632
1851
|
* Visibility/agent scoping shared by every document read path. Fail-closed:
|
|
1633
1852
|
* with no filter supplied, private documents are invisible.
|
|
1634
1853
|
*/
|
|
1635
|
-
function documentAccessConditions(
|
|
1854
|
+
function documentAccessConditions(
|
|
1855
|
+
workspaceId: string,
|
|
1856
|
+
access: DocumentAccessFilter | undefined,
|
|
1857
|
+
): SQL[] {
|
|
1858
|
+
const organization = eq(schema.documents.authorityKind, "organization");
|
|
1859
|
+
const workspace = and(
|
|
1860
|
+
eq(schema.documents.authorityKind, "workspace"),
|
|
1861
|
+
eq(schema.documents.authorityWorkspaceId, workspaceId),
|
|
1862
|
+
);
|
|
1863
|
+
const viewer = cleanString(access?.viewerSubjectId ?? null);
|
|
1864
|
+
const personal = viewer
|
|
1865
|
+
? and(
|
|
1866
|
+
eq(schema.documents.authorityKind, "personal"),
|
|
1867
|
+
eq(schema.documents.authorityWorkspaceId, workspaceId),
|
|
1868
|
+
eq(schema.documents.authoritySubjectId, viewer),
|
|
1869
|
+
)
|
|
1870
|
+
: undefined;
|
|
1871
|
+
const authority = viewer
|
|
1872
|
+
? (or(organization, workspace, personal) ?? organization)
|
|
1873
|
+
: (or(organization, workspace) ?? organization);
|
|
1636
1874
|
if (access?.agentOnly) {
|
|
1637
|
-
|
|
1638
|
-
const visibility = viewer
|
|
1639
|
-
? or(eq(schema.documents.visibility, "workspace"), eq(schema.documents.createdBy, viewer))
|
|
1640
|
-
: eq(schema.documents.visibility, "workspace");
|
|
1641
|
-
return [eq(schema.documents.agentAccess, true), ...(visibility ? [visibility] : [])];
|
|
1875
|
+
return [eq(schema.documents.agentAccess, true), authority];
|
|
1642
1876
|
}
|
|
1643
|
-
|
|
1644
|
-
if (viewer) {
|
|
1645
|
-
const condition = or(
|
|
1646
|
-
eq(schema.documents.visibility, "workspace"),
|
|
1647
|
-
eq(schema.documents.createdBy, viewer),
|
|
1648
|
-
);
|
|
1649
|
-
return condition ? [condition] : [];
|
|
1650
|
-
}
|
|
1651
|
-
return [eq(schema.documents.visibility, "workspace")];
|
|
1877
|
+
return [authority];
|
|
1652
1878
|
}
|
|
1653
1879
|
|
|
1654
1880
|
function documentMatchesAccess(
|
|
1655
|
-
document: Pick<DocumentAccessRecord, "
|
|
1881
|
+
document: Pick<DocumentAccessRecord, "authorityKind" | "authoritySubjectId" | "agentAccess">,
|
|
1656
1882
|
access: DocumentAccessFilter | undefined,
|
|
1657
1883
|
): boolean {
|
|
1658
1884
|
if (access?.agentOnly) {
|
|
1659
|
-
return (
|
|
1660
|
-
document.agentAccess &&
|
|
1661
|
-
(document.visibility !== "private" ||
|
|
1662
|
-
(!!access.viewerSubjectId && document.createdBy === access.viewerSubjectId))
|
|
1663
|
-
);
|
|
1885
|
+
return document.agentAccess && canViewDocument(document, access.viewerSubjectId);
|
|
1664
1886
|
}
|
|
1665
1887
|
return canViewDocument(document, access?.viewerSubjectId);
|
|
1666
1888
|
}
|
|
1667
1889
|
|
|
1668
1890
|
/** Whether a single already-fetched document is readable by this human viewer. */
|
|
1669
1891
|
export function canViewDocument(
|
|
1670
|
-
document: Pick<DocumentAccessRecord, "
|
|
1892
|
+
document: Pick<DocumentAccessRecord, "authorityKind" | "authoritySubjectId">,
|
|
1671
1893
|
viewerSubjectId: string | null | undefined,
|
|
1672
1894
|
): boolean {
|
|
1673
1895
|
return (
|
|
1674
|
-
document.
|
|
1675
|
-
(!!viewerSubjectId && document.
|
|
1896
|
+
document.authorityKind !== "personal" ||
|
|
1897
|
+
(!!viewerSubjectId && document.authoritySubjectId === viewerSubjectId)
|
|
1676
1898
|
);
|
|
1677
1899
|
}
|
|
1678
1900
|
|
|
1679
1901
|
type DocumentAccessRecord = {
|
|
1680
|
-
|
|
1681
|
-
|
|
1902
|
+
authorityKind: string;
|
|
1903
|
+
authoritySubjectId: string | null;
|
|
1682
1904
|
agentAccess: boolean;
|
|
1683
1905
|
};
|
|
1684
1906
|
|
|
1685
1907
|
function documentSearchConditions(input: DocumentSearchInput, embeddingModel?: string): SQL[] {
|
|
1686
1908
|
const conditions: SQL[] = [
|
|
1687
1909
|
eq(schema.documents.status, "ready"),
|
|
1688
|
-
eq(schema.
|
|
1689
|
-
|
|
1910
|
+
eq(schema.documents.accountId, input.accountId),
|
|
1911
|
+
eq(schema.documentChunks.accountId, input.accountId),
|
|
1912
|
+
...documentAccessConditions(input.workspaceId, input.access),
|
|
1690
1913
|
];
|
|
1691
1914
|
if (embeddingModel) {
|
|
1692
1915
|
conditions.push(eq(schema.documentChunks.embeddingModel, embeddingModel));
|
|
@@ -1704,31 +1927,32 @@ function documentSearchConditions(input: DocumentSearchInput, embeddingModel?: s
|
|
|
1704
1927
|
return conditions;
|
|
1705
1928
|
}
|
|
1706
1929
|
|
|
1707
|
-
function mapSearchRowBase(
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1930
|
+
function mapSearchRowBase(row: {
|
|
1931
|
+
chunkId: string;
|
|
1932
|
+
workspaceId: string;
|
|
1933
|
+
documentId: string;
|
|
1934
|
+
baseId: string;
|
|
1935
|
+
fileId: string;
|
|
1936
|
+
title: string;
|
|
1937
|
+
text: string;
|
|
1938
|
+
chunkIndex: number;
|
|
1939
|
+
metadata: Record<string, unknown>;
|
|
1940
|
+
sourceKind: string;
|
|
1941
|
+
sourceUri: string | null;
|
|
1942
|
+
sourceExternalId: string | null;
|
|
1943
|
+
sourceTitle: string | null;
|
|
1944
|
+
sourceAuthor: string | null;
|
|
1945
|
+
sourceCreatedAt: Date | null;
|
|
1946
|
+
sourceUpdatedAt: Date | null;
|
|
1947
|
+
sourceVersion: string | null;
|
|
1948
|
+
aclTags: string[];
|
|
1949
|
+
authorityKind: string;
|
|
1950
|
+
authorityWorkspaceId: string | null;
|
|
1951
|
+
authoritySubjectId: string | null;
|
|
1952
|
+
}): SearchRowBase {
|
|
1729
1953
|
return {
|
|
1730
1954
|
chunkId: row.chunkId,
|
|
1731
|
-
workspaceId,
|
|
1955
|
+
workspaceId: row.workspaceId,
|
|
1732
1956
|
documentId: row.documentId,
|
|
1733
1957
|
baseId: row.baseId,
|
|
1734
1958
|
fileId: row.fileId,
|
|
@@ -1745,6 +1969,9 @@ function mapSearchRowBase(
|
|
|
1745
1969
|
sourceUpdatedAt: row.sourceUpdatedAt?.toISOString() ?? null,
|
|
1746
1970
|
sourceVersion: row.sourceVersion,
|
|
1747
1971
|
aclTags: cleanStringArray(row.aclTags),
|
|
1972
|
+
authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
|
|
1973
|
+
authorityWorkspaceId: row.authorityWorkspaceId,
|
|
1974
|
+
authoritySubjectId: row.authoritySubjectId,
|
|
1748
1975
|
};
|
|
1749
1976
|
}
|
|
1750
1977
|
|
|
@@ -2029,6 +2256,9 @@ function mapDocument(row: typeof schema.documents.$inferSelect): Document {
|
|
|
2029
2256
|
sourceUpdatedAt: row.sourceUpdatedAt?.toISOString() ?? null,
|
|
2030
2257
|
sourceVersion: row.sourceVersion,
|
|
2031
2258
|
aclTags: cleanStringArray(row.aclTags),
|
|
2259
|
+
authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
|
|
2260
|
+
authorityWorkspaceId: row.authorityWorkspaceId,
|
|
2261
|
+
authoritySubjectId: row.authoritySubjectId,
|
|
2032
2262
|
visibility: normalizeDocumentVisibility(row.visibility),
|
|
2033
2263
|
createdBy: row.createdBy,
|
|
2034
2264
|
agentAccess: row.agentAccess,
|
|
@@ -2041,6 +2271,16 @@ function mapDocument(row: typeof schema.documents.$inferSelect): Document {
|
|
|
2041
2271
|
};
|
|
2042
2272
|
}
|
|
2043
2273
|
|
|
2274
|
+
function normalizeDocumentAuthorityKind(value: string): DocumentAuthorityKind {
|
|
2275
|
+
switch (value) {
|
|
2276
|
+
case "organization":
|
|
2277
|
+
case "personal":
|
|
2278
|
+
return value;
|
|
2279
|
+
default:
|
|
2280
|
+
return "workspace";
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2044
2284
|
function normalizeDocumentVisibility(value: string): DocumentVisibility {
|
|
2045
2285
|
return value === "private" ? "private" : "workspace";
|
|
2046
2286
|
}
|