@opengeni/documents 0.6.9-canary.0 → 0.7.0-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.7.0-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.19.1-canary.0",
44
+ "@opengeni/contracts": "^2.3.0-canary.0",
45
+ "@opengeni/db": "^3.2.0-canary.0",
46
+ "@opengeni/storage": "^0.2.106-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;
@@ -1023,6 +1039,256 @@ export async function runDocumentDefaultCollectionBackfill(
1023
1039
  );
1024
1040
  }
1025
1041
 
1042
+ export async function listDocumentDefaultCollectionBackfillRuns(
1043
+ db: Database,
1044
+ input: DocumentMigrationAuditInput,
1045
+ ) {
1046
+ const limit = validateDocumentMigrationAuditLimit(input.limit);
1047
+ const cursor = input.cursor
1048
+ ? decodeDocumentMigrationAuditCursor(input.cursor, ["backfill-runs", input.accountId], true)
1049
+ : null;
1050
+ return await withDocumentAccountRls(
1051
+ db,
1052
+ input.accountId,
1053
+ input.workspaceId,
1054
+ { viewerSubjectId: input.actorSubjectId },
1055
+ async (scopedDb) => {
1056
+ const command = {
1057
+ accountId: input.accountId,
1058
+ workspaceId: input.workspaceId,
1059
+ actorSubjectId: input.actorSubjectId,
1060
+ accountAdminAuthorization: input.accountAdminAuthorization,
1061
+ limit: limit + 1,
1062
+ beforeStartedAt: cursor?.timestamp ?? null,
1063
+ beforeRunId: cursor?.id ?? null,
1064
+ };
1065
+ const rows = (await scopedDb.execute(sql`
1066
+ SELECT list_document_default_collection_backfill_runs(
1067
+ ${JSON.stringify(command)}::jsonb
1068
+ ) AS result
1069
+ `)) as unknown as Array<{ result: unknown }>;
1070
+ const parsed = rows.map((row) => DocumentDefaultCollectionBackfillRunAudit.parse(row.result));
1071
+ const hasMore = parsed.length > limit;
1072
+ const runs = parsed.slice(0, limit);
1073
+ const tail = hasMore ? runs.at(-1) : null;
1074
+ return ListDocumentDefaultCollectionBackfillRunsResponse.parse({
1075
+ runs,
1076
+ hasMore,
1077
+ nextCursor: tail
1078
+ ? encodeDocumentMigrationAuditCursor(["backfill-runs", input.accountId], {
1079
+ timestamp: tail.startedAt,
1080
+ id: tail.runId,
1081
+ })
1082
+ : null,
1083
+ });
1084
+ },
1085
+ );
1086
+ }
1087
+
1088
+ export async function getDocumentDefaultCollectionBackfillAudit(
1089
+ db: Database,
1090
+ input: DocumentMigrationAuditInput & {
1091
+ runId: string;
1092
+ operationCursor?: string | undefined;
1093
+ receiptCursor?: string | undefined;
1094
+ },
1095
+ ) {
1096
+ const limit = validateDocumentMigrationAuditLimit(input.limit);
1097
+ const operationCursor = input.operationCursor
1098
+ ? decodeDocumentMigrationAuditCursor(
1099
+ input.operationCursor,
1100
+ ["backfill-operations", input.accountId, input.runId],
1101
+ true,
1102
+ )
1103
+ : null;
1104
+ const receiptCursor = input.receiptCursor
1105
+ ? decodeDocumentMigrationAuditCursor(
1106
+ input.receiptCursor,
1107
+ ["backfill-receipts", input.accountId, input.runId],
1108
+ false,
1109
+ )
1110
+ : null;
1111
+ return await withDocumentAccountRls(
1112
+ db,
1113
+ input.accountId,
1114
+ input.workspaceId,
1115
+ { viewerSubjectId: input.actorSubjectId },
1116
+ async (scopedDb) => {
1117
+ const command = {
1118
+ accountId: input.accountId,
1119
+ workspaceId: input.workspaceId,
1120
+ actorSubjectId: input.actorSubjectId,
1121
+ accountAdminAuthorization: input.accountAdminAuthorization,
1122
+ runId: input.runId,
1123
+ limit,
1124
+ operationBeforeCreatedAt: operationCursor?.timestamp ?? null,
1125
+ operationBeforeId: operationCursor?.id ?? null,
1126
+ receiptAfterWorkspaceId: receiptCursor?.id ?? null,
1127
+ };
1128
+ const rows = (await scopedDb.execute(sql`
1129
+ SELECT get_document_default_collection_backfill_audit(
1130
+ ${JSON.stringify(command)}::jsonb
1131
+ ) AS result
1132
+ `)) as unknown as Array<{ result: unknown }>;
1133
+ const raw = rows[0]?.result;
1134
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1135
+ throw new Error("document Default collection backfill audit returned no result");
1136
+ }
1137
+ const value = raw as Record<string, unknown>;
1138
+ const run = DocumentDefaultCollectionBackfillRunAudit.parse(value.run);
1139
+ const operations = Array.isArray(value.operations)
1140
+ ? value.operations.map((operation) =>
1141
+ DocumentDefaultCollectionBackfillOperationAudit.parse(operation),
1142
+ )
1143
+ : [];
1144
+ const receipts = Array.isArray(value.receipts)
1145
+ ? value.receipts.map((receipt) =>
1146
+ DocumentDefaultCollectionBackfillReceiptAudit.parse(receipt),
1147
+ )
1148
+ : [];
1149
+ const operationsHasMore = operations.length > limit;
1150
+ const receiptsHasMore = receipts.length > limit;
1151
+ const operationPage = operations.slice(0, limit);
1152
+ const receiptPage = receipts.slice(0, limit);
1153
+ const operationTail = operationsHasMore ? operationPage.at(-1) : null;
1154
+ const receiptTail = receiptsHasMore ? receiptPage.at(-1) : null;
1155
+ return DocumentDefaultCollectionBackfillAudit.parse({
1156
+ run,
1157
+ operations: operationPage,
1158
+ receipts: receiptPage,
1159
+ operationsHasMore,
1160
+ operationsNextCursor: operationTail
1161
+ ? encodeDocumentMigrationAuditCursor(
1162
+ ["backfill-operations", input.accountId, input.runId],
1163
+ { timestamp: operationTail.createdAt, id: operationTail.operationId },
1164
+ )
1165
+ : null,
1166
+ receiptsHasMore,
1167
+ receiptsNextCursor: receiptTail
1168
+ ? encodeDocumentMigrationAuditCursor(
1169
+ ["backfill-receipts", input.accountId, input.runId],
1170
+ { timestamp: null, id: receiptTail.workspaceId },
1171
+ )
1172
+ : null,
1173
+ });
1174
+ },
1175
+ );
1176
+ }
1177
+
1178
+ export async function listOrganizationDocumentAuthorityReclassifications(
1179
+ db: Database,
1180
+ input: DocumentMigrationAuditInput,
1181
+ ) {
1182
+ const limit = validateDocumentMigrationAuditLimit(input.limit);
1183
+ const cursor = input.cursor
1184
+ ? decodeDocumentMigrationAuditCursor(
1185
+ input.cursor,
1186
+ ["organization-reclassifications", input.accountId],
1187
+ true,
1188
+ )
1189
+ : null;
1190
+ return await withDocumentAccountRls(
1191
+ db,
1192
+ input.accountId,
1193
+ input.workspaceId,
1194
+ { viewerSubjectId: input.actorSubjectId },
1195
+ async (scopedDb) => {
1196
+ const command = {
1197
+ accountId: input.accountId,
1198
+ workspaceId: input.workspaceId,
1199
+ actorSubjectId: input.actorSubjectId,
1200
+ accountAdminAuthorization: input.accountAdminAuthorization,
1201
+ limit: limit + 1,
1202
+ beforeCreatedAt: cursor?.timestamp ?? null,
1203
+ beforeOperationId: cursor?.id ?? null,
1204
+ };
1205
+ const rows = (await scopedDb.execute(sql`
1206
+ SELECT list_organization_document_authority_reclassifications(
1207
+ ${JSON.stringify(command)}::jsonb
1208
+ ) AS result
1209
+ `)) as unknown as Array<{ result: unknown }>;
1210
+ const parsed = rows.map((row) =>
1211
+ OrganizationDocumentAuthorityReclassification.parse(row.result),
1212
+ );
1213
+ const hasMore = parsed.length > limit;
1214
+ const receipts = parsed.slice(0, limit);
1215
+ const tail = hasMore ? receipts.at(-1) : null;
1216
+ return ListOrganizationDocumentAuthorityReclassificationsResponse.parse({
1217
+ receipts,
1218
+ hasMore,
1219
+ nextCursor: tail
1220
+ ? encodeDocumentMigrationAuditCursor(
1221
+ ["organization-reclassifications", input.accountId],
1222
+ { timestamp: tail.createdAt, id: tail.operationId },
1223
+ )
1224
+ : null,
1225
+ });
1226
+ },
1227
+ );
1228
+ }
1229
+
1230
+ function validateDocumentMigrationAuditLimit(value: number | undefined): number {
1231
+ const limit = value ?? 50;
1232
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
1233
+ throw new Error("document migration audit limit must be between 1 and 100");
1234
+ }
1235
+ return limit;
1236
+ }
1237
+
1238
+ type DocumentMigrationAuditCursor = { timestamp: string | null; id: string };
1239
+
1240
+ function documentMigrationAuditCursorScope(parts: string[]): string {
1241
+ return createHash("sha256")
1242
+ .update(JSON.stringify(["document-migration-audit", 1, ...parts]), "utf8")
1243
+ .digest("hex")
1244
+ .slice(0, 32);
1245
+ }
1246
+
1247
+ function encodeDocumentMigrationAuditCursor(
1248
+ parts: string[],
1249
+ cursor: DocumentMigrationAuditCursor,
1250
+ ): string {
1251
+ return Buffer.from(
1252
+ JSON.stringify({
1253
+ v: 1,
1254
+ s: documentMigrationAuditCursorScope(parts),
1255
+ t: cursor.timestamp,
1256
+ i: cursor.id,
1257
+ }),
1258
+ "utf8",
1259
+ ).toString("base64url");
1260
+ }
1261
+
1262
+ function decodeDocumentMigrationAuditCursor(
1263
+ value: string,
1264
+ parts: string[],
1265
+ requireTimestamp: boolean,
1266
+ ): DocumentMigrationAuditCursor {
1267
+ try {
1268
+ if (!value || value.length > 1_024) throw new Error("cursor length");
1269
+ const bytes = Buffer.from(value, "base64url");
1270
+ if (bytes.toString("base64url") !== value) throw new Error("cursor encoding");
1271
+ const parsed = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
1272
+ if (
1273
+ Object.keys(parsed).sort().join(",") !== "i,s,t,v" ||
1274
+ parsed.v !== 1 ||
1275
+ parsed.s !== documentMigrationAuditCursorScope(parts) ||
1276
+ typeof parsed.i !== "string" ||
1277
+ !/^[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(
1278
+ parsed.i,
1279
+ ) ||
1280
+ (requireTimestamp
1281
+ ? typeof parsed.t !== "string" || !Number.isFinite(new Date(parsed.t).getTime())
1282
+ : parsed.t !== null)
1283
+ ) {
1284
+ throw new Error("cursor payload");
1285
+ }
1286
+ return { timestamp: typeof parsed.t === "string" ? parsed.t : null, id: parsed.i };
1287
+ } catch (error) {
1288
+ throw new Error("invalid document migration audit cursor", { cause: error });
1289
+ }
1290
+ }
1291
+
1026
1292
  export async function reclassifyDocumentAuthority(
1027
1293
  db: Database,
1028
1294
  input: ReclassifyDocumentAuthorityInput,