@opengeni/documents 0.6.9-canary.0 → 0.8.2-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/documents",
3
- "version": "0.6.9-canary.0",
3
+ "version": "0.8.2-canary.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,10 +40,10 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@llamaindex/liteparse": "^1.5.3",
43
- "@opengeni/config": "^0.19.0-canary.0",
44
- "@opengeni/contracts": "^2.2.0-canary.0",
45
- "@opengeni/db": "^3.1.0-canary.0",
46
- "@opengeni/storage": "^0.2.105-canary.0",
43
+ "@opengeni/config": "^0.20.1-canary.0",
44
+ "@opengeni/contracts": "^2.5.0-canary.0",
45
+ "@opengeni/db": "^3.4.0-canary.0",
46
+ "@opengeni/storage": "^0.2.108-canary.0",
47
47
  "drizzle-orm": "^0.45.2",
48
48
  "openai": "6.47.0"
49
49
  }
package/src/index.ts CHANGED
@@ -26,6 +26,13 @@ import {
26
26
  type ReclassifyDocumentAuthorityRequest,
27
27
  type RunDocumentDefaultCollectionBackfillRequest,
28
28
  DocumentDefaultCollectionBackfill,
29
+ DocumentDefaultCollectionBackfillAudit,
30
+ DocumentDefaultCollectionBackfillOperationAudit,
31
+ DocumentDefaultCollectionBackfillReceiptAudit,
32
+ DocumentDefaultCollectionBackfillRunAudit,
33
+ ListDocumentDefaultCollectionBackfillRunsResponse,
34
+ ListOrganizationDocumentAuthorityReclassificationsResponse,
35
+ OrganizationDocumentAuthorityReclassification,
29
36
  } from "@opengeni/contracts";
30
37
  import {
31
38
  createPersonalDocumentAuthority,
@@ -197,6 +204,15 @@ export type RunDocumentDefaultCollectionBackfillInput =
197
204
  accountAdminAuthorization: DocumentAccountAdminAuthorization;
198
205
  };
199
206
 
207
+ export type DocumentMigrationAuditInput = {
208
+ accountId: string;
209
+ workspaceId: string;
210
+ actorSubjectId: string;
211
+ accountAdminAuthorization: DocumentAccountAdminAuthorization;
212
+ limit?: number | undefined;
213
+ cursor?: string | undefined;
214
+ };
215
+
200
216
  export type AgentDocumentAuthorityContext = {
201
217
  sessionId: string;
202
218
  attemptId: string;
@@ -243,6 +259,7 @@ export type DocumentSearchInput = {
243
259
  limit?: number | undefined;
244
260
  mode?: DocumentSearchMode | undefined;
245
261
  sourceKinds?: KnowledgeSourceKind[] | undefined;
262
+ authorityKinds?: DocumentAuthorityKind[] | undefined;
246
263
  aclTags?: string[] | undefined;
247
264
  access?: DocumentAccessFilter | undefined;
248
265
  };
@@ -1023,6 +1040,264 @@ export async function runDocumentDefaultCollectionBackfill(
1023
1040
  );
1024
1041
  }
1025
1042
 
1043
+ export async function listDocumentDefaultCollectionBackfillRuns(
1044
+ db: Database,
1045
+ input: DocumentMigrationAuditInput,
1046
+ ) {
1047
+ const limit = validateDocumentMigrationAuditLimit(input.limit);
1048
+ const cursor = input.cursor
1049
+ ? decodeDocumentMigrationAuditCursor(input.cursor, ["backfill-runs", input.accountId], true)
1050
+ : null;
1051
+ return await withDocumentAccountRls(
1052
+ db,
1053
+ input.accountId,
1054
+ input.workspaceId,
1055
+ { viewerSubjectId: input.actorSubjectId },
1056
+ async (scopedDb) => {
1057
+ const command = {
1058
+ accountId: input.accountId,
1059
+ workspaceId: input.workspaceId,
1060
+ actorSubjectId: input.actorSubjectId,
1061
+ accountAdminAuthorization: input.accountAdminAuthorization,
1062
+ limit: limit + 1,
1063
+ beforeStartedAt: cursor?.timestamp ?? null,
1064
+ beforeRunId: cursor?.id ?? null,
1065
+ };
1066
+ const rows = (await scopedDb.execute(sql`
1067
+ SELECT list_document_default_collection_backfill_runs(
1068
+ ${JSON.stringify(command)}::jsonb
1069
+ ) AS result
1070
+ `)) as unknown as Array<{ result: unknown }>;
1071
+ const parsed = rows.map((row) => DocumentDefaultCollectionBackfillRunAudit.parse(row.result));
1072
+ const hasMore = parsed.length > limit;
1073
+ const runs = parsed.slice(0, limit);
1074
+ const tail = hasMore ? runs.at(-1) : null;
1075
+ return ListDocumentDefaultCollectionBackfillRunsResponse.parse({
1076
+ runs,
1077
+ hasMore,
1078
+ nextCursor: tail
1079
+ ? encodeDocumentMigrationAuditCursor(["backfill-runs", input.accountId], {
1080
+ timestamp: tail.startedAt,
1081
+ id: tail.runId,
1082
+ })
1083
+ : null,
1084
+ });
1085
+ },
1086
+ );
1087
+ }
1088
+
1089
+ export async function getDocumentDefaultCollectionBackfillAudit(
1090
+ db: Database,
1091
+ input: DocumentMigrationAuditInput & {
1092
+ runId: string;
1093
+ operationCursor?: string | undefined;
1094
+ receiptCursor?: string | undefined;
1095
+ },
1096
+ ) {
1097
+ const limit = validateDocumentMigrationAuditLimit(input.limit);
1098
+ const operationCursor = input.operationCursor
1099
+ ? decodeDocumentMigrationAuditCursor(
1100
+ input.operationCursor,
1101
+ ["backfill-operations", input.accountId, input.runId],
1102
+ true,
1103
+ )
1104
+ : null;
1105
+ const receiptCursor = input.receiptCursor
1106
+ ? decodeDocumentMigrationAuditCursor(
1107
+ input.receiptCursor,
1108
+ ["backfill-receipts", input.accountId, input.runId],
1109
+ false,
1110
+ )
1111
+ : null;
1112
+ return await withDocumentAccountRls(
1113
+ db,
1114
+ input.accountId,
1115
+ input.workspaceId,
1116
+ { viewerSubjectId: input.actorSubjectId },
1117
+ async (scopedDb) => {
1118
+ const command = {
1119
+ accountId: input.accountId,
1120
+ workspaceId: input.workspaceId,
1121
+ actorSubjectId: input.actorSubjectId,
1122
+ accountAdminAuthorization: input.accountAdminAuthorization,
1123
+ runId: input.runId,
1124
+ limit,
1125
+ operationBeforeCreatedAt: operationCursor?.timestamp ?? null,
1126
+ operationBeforeId: operationCursor?.id ?? null,
1127
+ receiptAfterWorkspaceId: receiptCursor?.id ?? null,
1128
+ };
1129
+ const rows = (await scopedDb.execute(sql`
1130
+ SELECT get_document_default_collection_backfill_audit(
1131
+ ${JSON.stringify(command)}::jsonb
1132
+ ) AS result
1133
+ `)) as unknown as Array<{ result: unknown }>;
1134
+ const raw = rows[0]?.result;
1135
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1136
+ throw new Error("document Default collection backfill audit returned no result");
1137
+ }
1138
+ const value = raw as Record<string, unknown>;
1139
+ const run = DocumentDefaultCollectionBackfillRunAudit.parse(value.run);
1140
+ const operations = Array.isArray(value.operations)
1141
+ ? value.operations.map((operation) =>
1142
+ DocumentDefaultCollectionBackfillOperationAudit.parse(operation),
1143
+ )
1144
+ : [];
1145
+ const receipts = Array.isArray(value.receipts)
1146
+ ? value.receipts.map((receipt) =>
1147
+ DocumentDefaultCollectionBackfillReceiptAudit.parse(receipt),
1148
+ )
1149
+ : [];
1150
+ const operationsHasMore = operations.length > limit;
1151
+ const receiptsHasMore = receipts.length > limit;
1152
+ const operationPage = operations.slice(0, limit);
1153
+ const receiptPage = receipts.slice(0, limit);
1154
+ const operationTail = operationsHasMore ? operationPage.at(-1) : null;
1155
+ const receiptTail = receiptsHasMore ? receiptPage.at(-1) : null;
1156
+ return DocumentDefaultCollectionBackfillAudit.parse({
1157
+ run,
1158
+ operations: operationPage,
1159
+ receipts: receiptPage,
1160
+ operationsHasMore,
1161
+ operationsNextCursor: operationTail
1162
+ ? encodeDocumentMigrationAuditCursor(
1163
+ ["backfill-operations", input.accountId, input.runId],
1164
+ {
1165
+ timestamp: operationTail.createdAt,
1166
+ id: operationTail.operationId,
1167
+ },
1168
+ )
1169
+ : null,
1170
+ receiptsHasMore,
1171
+ receiptsNextCursor: receiptTail
1172
+ ? encodeDocumentMigrationAuditCursor(
1173
+ ["backfill-receipts", input.accountId, input.runId],
1174
+ { timestamp: null, id: receiptTail.workspaceId },
1175
+ )
1176
+ : null,
1177
+ });
1178
+ },
1179
+ );
1180
+ }
1181
+
1182
+ export async function listOrganizationDocumentAuthorityReclassifications(
1183
+ db: Database,
1184
+ input: DocumentMigrationAuditInput,
1185
+ ) {
1186
+ const limit = validateDocumentMigrationAuditLimit(input.limit);
1187
+ const cursor = input.cursor
1188
+ ? decodeDocumentMigrationAuditCursor(
1189
+ input.cursor,
1190
+ ["organization-reclassifications", input.accountId],
1191
+ true,
1192
+ )
1193
+ : null;
1194
+ return await withDocumentAccountRls(
1195
+ db,
1196
+ input.accountId,
1197
+ input.workspaceId,
1198
+ { viewerSubjectId: input.actorSubjectId },
1199
+ async (scopedDb) => {
1200
+ const command = {
1201
+ accountId: input.accountId,
1202
+ workspaceId: input.workspaceId,
1203
+ actorSubjectId: input.actorSubjectId,
1204
+ accountAdminAuthorization: input.accountAdminAuthorization,
1205
+ limit: limit + 1,
1206
+ beforeCreatedAt: cursor?.timestamp ?? null,
1207
+ beforeOperationId: cursor?.id ?? null,
1208
+ };
1209
+ const rows = (await scopedDb.execute(sql`
1210
+ SELECT list_organization_document_authority_reclassifications(
1211
+ ${JSON.stringify(command)}::jsonb
1212
+ ) AS result
1213
+ `)) as unknown as Array<{ result: unknown }>;
1214
+ const parsed = rows.map((row) =>
1215
+ OrganizationDocumentAuthorityReclassification.parse(row.result),
1216
+ );
1217
+ const hasMore = parsed.length > limit;
1218
+ const receipts = parsed.slice(0, limit);
1219
+ const tail = hasMore ? receipts.at(-1) : null;
1220
+ return ListOrganizationDocumentAuthorityReclassificationsResponse.parse({
1221
+ receipts,
1222
+ hasMore,
1223
+ nextCursor: tail
1224
+ ? encodeDocumentMigrationAuditCursor(
1225
+ ["organization-reclassifications", input.accountId],
1226
+ { timestamp: tail.createdAt, id: tail.operationId },
1227
+ )
1228
+ : null,
1229
+ });
1230
+ },
1231
+ );
1232
+ }
1233
+
1234
+ function validateDocumentMigrationAuditLimit(value: number | undefined): number {
1235
+ const limit = value ?? 50;
1236
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
1237
+ throw new Error("document migration audit limit must be between 1 and 100");
1238
+ }
1239
+ return limit;
1240
+ }
1241
+
1242
+ type DocumentMigrationAuditCursor = { timestamp: string | null; id: string };
1243
+
1244
+ function documentMigrationAuditCursorScope(parts: string[]): string {
1245
+ return createHash("sha256")
1246
+ .update(JSON.stringify(["document-migration-audit", 1, ...parts]), "utf8")
1247
+ .digest("hex")
1248
+ .slice(0, 32);
1249
+ }
1250
+
1251
+ function encodeDocumentMigrationAuditCursor(
1252
+ parts: string[],
1253
+ cursor: DocumentMigrationAuditCursor,
1254
+ ): string {
1255
+ return Buffer.from(
1256
+ JSON.stringify({
1257
+ v: 1,
1258
+ s: documentMigrationAuditCursorScope(parts),
1259
+ t: cursor.timestamp,
1260
+ i: cursor.id,
1261
+ }),
1262
+ "utf8",
1263
+ ).toString("base64url");
1264
+ }
1265
+
1266
+ function decodeDocumentMigrationAuditCursor(
1267
+ value: string,
1268
+ parts: string[],
1269
+ requireTimestamp: boolean,
1270
+ ): DocumentMigrationAuditCursor {
1271
+ try {
1272
+ if (!value || value.length > 1_024) throw new Error("cursor length");
1273
+ const bytes = Buffer.from(value, "base64url");
1274
+ if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
1275
+ const parsed = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
1276
+ if (
1277
+ Object.keys(parsed).sort().join(",") !== "i,s,t,v" ||
1278
+ parsed.v !== 1 ||
1279
+ parsed.s !== documentMigrationAuditCursorScope(parts) ||
1280
+ typeof parsed.i !== "string" ||
1281
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
1282
+ parsed.i,
1283
+ ) ||
1284
+ (requireTimestamp
1285
+ ? typeof parsed.t !== "string" || !Number.isFinite(new Date(parsed.t).getTime())
1286
+ : parsed.t !== null)
1287
+ ) {
1288
+ throw new Error("cursor payload");
1289
+ }
1290
+ return {
1291
+ timestamp: typeof parsed.t === "string" ? parsed.t : null,
1292
+ id: parsed.i,
1293
+ };
1294
+ } catch (error) {
1295
+ throw new Error("invalid document migration audit cursor", {
1296
+ cause: error,
1297
+ });
1298
+ }
1299
+ }
1300
+
1026
1301
  export async function reclassifyDocumentAuthority(
1027
1302
  db: Database,
1028
1303
  input: ReclassifyDocumentAuthorityInput,
@@ -1170,7 +1445,9 @@ function decodeDocumentAuthorityReclassificationCursor(
1170
1445
  }
1171
1446
  return { createdAt: parsed.t, operationId: parsed.i };
1172
1447
  } catch (error) {
1173
- throw new Error("invalid document authority receipt cursor", { cause: error });
1448
+ throw new Error("invalid document authority receipt cursor", {
1449
+ cause: error,
1450
+ });
1174
1451
  }
1175
1452
  }
1176
1453
 
@@ -1622,7 +1899,11 @@ export function encodeDocumentIndexCheckpoint(input: {
1622
1899
 
1623
1900
  export function decodeDocumentIndexCheckpoint(
1624
1901
  value: string,
1625
- scope: { accountId: string; workspaceId: string; initiatingSubjectId: string },
1902
+ scope: {
1903
+ accountId: string;
1904
+ workspaceId: string;
1905
+ initiatingSubjectId: string;
1906
+ },
1626
1907
  ): bigint {
1627
1908
  try {
1628
1909
  if (!value || value.length > DOCUMENT_INDEX_CHECKPOINT_MAX_CHARS) {
@@ -1996,7 +2277,11 @@ async function curateDroppedDocument(
1996
2277
  const bases = await listDocumentBases(db, document.workspaceId);
1997
2278
  const candidates: DocumentCurationCandidateBase[] = bases
1998
2279
  .filter((base) => base.id !== document.baseId)
1999
- .map((base) => ({ id: base.id, name: base.name, description: base.description }));
2280
+ .map((base) => ({
2281
+ id: base.id,
2282
+ name: base.name,
2283
+ description: base.description,
2284
+ }));
2000
2285
  const input: DocumentCurationInput = {
2001
2286
  text: parsed.text.slice(0, DOCUMENT_CURATION_MAX_INPUT_CHARS),
2002
2287
  filename: file.filename,
@@ -2188,6 +2473,7 @@ export async function searchEffectiveDocuments(
2188
2473
  limit: input.limit,
2189
2474
  mode: input.mode,
2190
2475
  sourceKinds: input.sourceKinds,
2476
+ authorityKinds: input.authorityKinds,
2191
2477
  aclTags: input.aclTags,
2192
2478
  // Construct the lower-level access filter here instead of spreading the
2193
2479
  // caller input, so an untyped/legacy access override is always ignored.
@@ -2230,6 +2516,7 @@ export async function searchEffectiveKnowledge(
2230
2516
  ...(input.baseIds ? { baseIds: input.baseIds } : {}),
2231
2517
  ...(input.mode ? { mode: input.mode } : {}),
2232
2518
  ...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
2519
+ ...(input.authorityKinds ? { authorityKinds: input.authorityKinds } : {}),
2233
2520
  ...(input.aclTags ? { aclTags: input.aclTags } : {}),
2234
2521
  access,
2235
2522
  },
@@ -2273,6 +2560,9 @@ export async function searchEffectiveKnowledge(
2273
2560
  ranked.map((result) => result.chunkId),
2274
2561
  ),
2275
2562
  eq(schema.documents.status, "ready"),
2563
+ ...(input.authorityKinds?.length
2564
+ ? [inArray(schema.documents.authorityKind, input.authorityKinds)]
2565
+ : []),
2276
2566
  ...documentAccessConditions(input.workspaceId, access),
2277
2567
  ),
2278
2568
  ),
@@ -2808,7 +3098,10 @@ export async function browseEffectiveKnowledge(
2808
3098
  async (scopedDb) => {
2809
3099
  if (parent) {
2810
3100
  const [authorizedParent] = await scopedDb
2811
- .select({ id: schema.documents.id, indexSequence: schema.documents.indexSequence })
3101
+ .select({
3102
+ id: schema.documents.id,
3103
+ indexSequence: schema.documents.indexSequence,
3104
+ })
2812
3105
  .from(schema.documents)
2813
3106
  .where(
2814
3107
  and(
@@ -2820,7 +3113,10 @@ export async function browseEffectiveKnowledge(
2820
3113
  )
2821
3114
  .limit(1);
2822
3115
  if (!authorizedParent) {
2823
- return selectKnowledgeBrowseRecords({ entries: [], hasMoreAfterEntries: false });
3116
+ return selectKnowledgeBrowseRecords({
3117
+ entries: [],
3118
+ hasMoreAfterEntries: false,
3119
+ });
2824
3120
  }
2825
3121
  if (authorizedParent.indexSequence === null) {
2826
3122
  throw new Error("ready knowledge document is missing its index revision");
@@ -2937,7 +3233,11 @@ export function encodeKnowledgeBrowseCursor(
2937
3233
  if (position < 0n) throw new Error("knowledge browse cursor position is invalid");
2938
3234
  const version = knowledgeBrowseCursorVersion(scope);
2939
3235
  return Buffer.from(
2940
- JSON.stringify({ v: version, s: knowledgeBrowseCursorScope(scope), q: position.toString() }),
3236
+ JSON.stringify({
3237
+ v: version,
3238
+ s: knowledgeBrowseCursorScope(scope),
3239
+ q: position.toString(),
3240
+ }),
2941
3241
  "utf8",
2942
3242
  ).toString("base64url");
2943
3243
  }
@@ -3017,7 +3317,10 @@ function parseKnowledgeRecordId(value: string): {
3017
3317
  value,
3018
3318
  );
3019
3319
  if (!match) throw new Error("invalid knowledge record id");
3020
- return { kind: match[1] as "document" | "document_chunk", id: match[2]!.toLowerCase() };
3320
+ return {
3321
+ kind: match[1] as "document" | "document_chunk",
3322
+ id: match[2]!.toLowerCase(),
3323
+ };
3021
3324
  }
3022
3325
 
3023
3326
  function knowledgeDocumentRecord(
@@ -3621,6 +3924,9 @@ function documentSearchConditions(input: DocumentSearchInput, embeddingModel?: s
3621
3924
  if (input.sourceKinds?.length) {
3622
3925
  conditions.push(inArray(schema.documents.sourceKind, input.sourceKinds));
3623
3926
  }
3927
+ if (input.authorityKinds?.length) {
3928
+ conditions.push(inArray(schema.documents.authorityKind, input.authorityKinds));
3929
+ }
3624
3930
  const aclTags = cleanStringArray(input.aclTags);
3625
3931
  if (aclTags.length > 0) {
3626
3932
  conditions.push(sql`${schema.documents.aclTags} @> ${JSON.stringify(aclTags)}::jsonb`);