@opengeni/documents 0.5.18 → 0.5.32
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/atlassian.d.ts +78 -0
- package/dist/atlassian.js +140 -0
- package/dist/atlassian.js.map +1 -0
- package/dist/google-drive.d.ts +2 -0
- package/dist/google-drive.js +12 -1
- package/dist/google-drive.js.map +1 -1
- package/dist/index.d.ts +31 -3
- package/dist/index.js +174 -26
- package/dist/index.js.map +1 -1
- package/package.json +10 -6
- package/src/atlassian.ts +250 -0
- package/src/google-drive.ts +21 -1
- package/src/index.ts +246 -26
package/src/index.ts
CHANGED
|
@@ -12,7 +12,9 @@ import type {
|
|
|
12
12
|
DocumentStatus,
|
|
13
13
|
DocumentVisibility,
|
|
14
14
|
FileAsset,
|
|
15
|
+
IndexedDocumentSummary,
|
|
15
16
|
KnowledgeSourceKind,
|
|
17
|
+
ListIndexedDocumentsResponse,
|
|
16
18
|
} from "@opengeni/contracts";
|
|
17
19
|
import {
|
|
18
20
|
requireFile,
|
|
@@ -25,9 +27,9 @@ import {
|
|
|
25
27
|
} from "@opengeni/db";
|
|
26
28
|
import * as schema from "@opengeni/db/schema";
|
|
27
29
|
import type { ObjectStorage } from "@opengeni/storage";
|
|
28
|
-
import {
|
|
29
|
-
import { and, asc, desc, eq, inArray, or, sql, type SQL } from "drizzle-orm";
|
|
30
|
-
import OpenAI from "openai";
|
|
30
|
+
import { createHash } from "node:crypto";
|
|
31
|
+
import { and, asc, desc, eq, gt, inArray, isNotNull, or, sql, type SQL } from "drizzle-orm";
|
|
32
|
+
import type OpenAI from "openai";
|
|
31
33
|
|
|
32
34
|
export const DEFAULT_DOCUMENT_PARSER = "liteparse";
|
|
33
35
|
export const DEFAULT_DOCUMENT_EMBEDDING_MODEL = "text-embedding-3-large";
|
|
@@ -39,6 +41,7 @@ export const DEFAULT_DOCUMENT_CURATION_MODEL = "gpt-4o-mini";
|
|
|
39
41
|
// classify without paying for a full-document prompt on every drop.
|
|
40
42
|
export const DOCUMENT_CURATION_MAX_INPUT_CHARS = 24_000;
|
|
41
43
|
export const DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES = 1024;
|
|
44
|
+
export const DOCUMENT_INDEX_CHECKPOINT_MAX_CHARS = 1_024;
|
|
42
45
|
// A base move is applied automatically only at or above this curator
|
|
43
46
|
// confidence; below it the suggestion is surfaced for human review instead.
|
|
44
47
|
export const DOCUMENT_CURATION_AUTO_FILE_CONFIDENCE = 0.75;
|
|
@@ -186,6 +189,15 @@ export type EffectiveDocumentSearchInput = Omit<DocumentSearchInput, "access"> &
|
|
|
186
189
|
surface: "human" | "agent";
|
|
187
190
|
};
|
|
188
191
|
|
|
192
|
+
export type ListEffectiveIndexedDocumentsInput = {
|
|
193
|
+
accountId: string;
|
|
194
|
+
workspaceId: string;
|
|
195
|
+
/** Immutable human subject accepted for the logical request/turn. */
|
|
196
|
+
initiatingSubjectId: string;
|
|
197
|
+
checkpoint?: string | undefined;
|
|
198
|
+
limit?: number | undefined;
|
|
199
|
+
};
|
|
200
|
+
|
|
189
201
|
export type DocumentIndexHooks = {
|
|
190
202
|
beforeEmbed?: (input: {
|
|
191
203
|
accountId: string;
|
|
@@ -218,6 +230,7 @@ export class LiteParseDocumentParser implements DocumentParser {
|
|
|
218
230
|
|
|
219
231
|
private async parseWithLiteParse(bytes: Uint8Array): Promise<string> {
|
|
220
232
|
return await this.enqueueParse(async () => {
|
|
233
|
+
const { LiteParse } = await import("@llamaindex/liteparse");
|
|
221
234
|
const parser = new LiteParse({ ocrEnabled: true, numWorkers: 1 });
|
|
222
235
|
const result = await parser.parse(Buffer.from(bytes), true);
|
|
223
236
|
const text = typeof result?.text === "string" ? result.text : "";
|
|
@@ -264,7 +277,7 @@ export class RecursiveTextChunker implements DocumentChunker {
|
|
|
264
277
|
}
|
|
265
278
|
|
|
266
279
|
export class OpenAIEmbeddingProvider implements DocumentEmbedder {
|
|
267
|
-
private
|
|
280
|
+
private clientPromise: Promise<OpenAI> | null = null;
|
|
268
281
|
private readonly apiKey: string | undefined;
|
|
269
282
|
private readonly baseURL: string | undefined;
|
|
270
283
|
|
|
@@ -294,7 +307,9 @@ export class OpenAIEmbeddingProvider implements DocumentEmbedder {
|
|
|
294
307
|
const out: number[][] = [];
|
|
295
308
|
for (let start = 0; start < texts.length; start += 64) {
|
|
296
309
|
const batch = texts.slice(start, start + 64);
|
|
297
|
-
const response = await
|
|
310
|
+
const response = await (
|
|
311
|
+
await this.openai()
|
|
312
|
+
).embeddings.create({
|
|
298
313
|
model: this.model,
|
|
299
314
|
input: batch,
|
|
300
315
|
dimensions: this.dimensions,
|
|
@@ -314,17 +329,20 @@ export class OpenAIEmbeddingProvider implements DocumentEmbedder {
|
|
|
314
329
|
return embedding;
|
|
315
330
|
}
|
|
316
331
|
|
|
317
|
-
private openai(): OpenAI {
|
|
332
|
+
private async openai(): Promise<OpenAI> {
|
|
318
333
|
if (!this.apiKey) {
|
|
319
334
|
throw new Error("OpenAI document embeddings require an API key");
|
|
320
335
|
}
|
|
321
|
-
this.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
336
|
+
this.clientPromise ??= import("openai").then(
|
|
337
|
+
({ default: OpenAIClient }) =>
|
|
338
|
+
new OpenAIClient({
|
|
339
|
+
apiKey: this.apiKey,
|
|
340
|
+
...(this.baseURL ? { baseURL: this.baseURL } : {}),
|
|
341
|
+
...(this.defaultQuery ? { defaultQuery: this.defaultQuery } : {}),
|
|
342
|
+
...(this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}),
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
return await this.clientPromise;
|
|
328
346
|
}
|
|
329
347
|
}
|
|
330
348
|
|
|
@@ -410,7 +428,7 @@ const CURATION_SYSTEM_PROMPT = [
|
|
|
410
428
|
].join(" ");
|
|
411
429
|
|
|
412
430
|
export class OpenAICurationProvider implements DocumentCurator {
|
|
413
|
-
private
|
|
431
|
+
private clientPromise: Promise<OpenAI> | null = null;
|
|
414
432
|
private readonly apiKey: string | undefined;
|
|
415
433
|
private readonly baseURL: string | undefined;
|
|
416
434
|
private readonly defaultHeaders: Record<string, string> | undefined;
|
|
@@ -432,7 +450,9 @@ export class OpenAICurationProvider implements DocumentCurator {
|
|
|
432
450
|
}
|
|
433
451
|
|
|
434
452
|
async curate(input: DocumentCurationInput): Promise<DocumentCurationOutcome> {
|
|
435
|
-
const response = await
|
|
453
|
+
const response = await (
|
|
454
|
+
await this.openai()
|
|
455
|
+
).chat.completions.create({
|
|
436
456
|
model: this.model,
|
|
437
457
|
response_format: { type: "json_object" },
|
|
438
458
|
messages: [
|
|
@@ -455,17 +475,20 @@ export class OpenAICurationProvider implements DocumentCurator {
|
|
|
455
475
|
return parseCurationOutcome(raw, input.bases);
|
|
456
476
|
}
|
|
457
477
|
|
|
458
|
-
private openai(): OpenAI {
|
|
478
|
+
private async openai(): Promise<OpenAI> {
|
|
459
479
|
if (!this.apiKey) {
|
|
460
480
|
throw new Error("OpenAI document curation requires an API key");
|
|
461
481
|
}
|
|
462
|
-
this.
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
482
|
+
this.clientPromise ??= import("openai").then(
|
|
483
|
+
({ default: OpenAIClient }) =>
|
|
484
|
+
new OpenAIClient({
|
|
485
|
+
apiKey: this.apiKey,
|
|
486
|
+
...(this.baseURL ? { baseURL: this.baseURL } : {}),
|
|
487
|
+
...(this.defaultQuery ? { defaultQuery: this.defaultQuery } : {}),
|
|
488
|
+
...(this.defaultHeaders ? { defaultHeaders: this.defaultHeaders } : {}),
|
|
489
|
+
}),
|
|
490
|
+
);
|
|
491
|
+
return await this.clientPromise;
|
|
469
492
|
}
|
|
470
493
|
}
|
|
471
494
|
|
|
@@ -897,6 +920,7 @@ export async function addDocumentToBase(
|
|
|
897
920
|
organizationAuthorityGranted?: boolean | undefined;
|
|
898
921
|
curationStatus?: DocumentCurationStatus | undefined;
|
|
899
922
|
access?: DocumentAccessFilter | undefined;
|
|
923
|
+
knowledgeSourceIdentity?: string | null | undefined;
|
|
900
924
|
},
|
|
901
925
|
): Promise<Document> {
|
|
902
926
|
return await withRlsContext(
|
|
@@ -924,6 +948,10 @@ export async function addDocumentToBase(
|
|
|
924
948
|
const base = await getDocumentBase(scopedDb, input.workspaceId, input.baseId);
|
|
925
949
|
if (!base) throw new Error(`Document base not found: ${input.baseId}`);
|
|
926
950
|
const file = await requireReadyFile(scopedDb, input.workspaceId, input.fileId);
|
|
951
|
+
const knowledgeSourceIdentity = cleanString(input.knowledgeSourceIdentity ?? null);
|
|
952
|
+
if (knowledgeSourceIdentity && knowledgeSourceIdentity.length > 512) {
|
|
953
|
+
throw new Error("knowledge source document identity exceeds 512 characters");
|
|
954
|
+
}
|
|
927
955
|
const now = new Date();
|
|
928
956
|
const [existing] = await scopedDb
|
|
929
957
|
.select()
|
|
@@ -931,12 +959,19 @@ export async function addDocumentToBase(
|
|
|
931
959
|
.where(
|
|
932
960
|
and(
|
|
933
961
|
eq(schema.documents.workspaceId, input.workspaceId),
|
|
934
|
-
|
|
935
|
-
|
|
962
|
+
...(knowledgeSourceIdentity
|
|
963
|
+
? [eq(schema.documents.knowledgeSourceIdentity, knowledgeSourceIdentity)]
|
|
964
|
+
: [
|
|
965
|
+
eq(schema.documents.baseId, input.baseId),
|
|
966
|
+
eq(schema.documents.fileId, input.fileId),
|
|
967
|
+
]),
|
|
936
968
|
),
|
|
937
969
|
)
|
|
938
970
|
.limit(1);
|
|
939
971
|
if (existing) {
|
|
972
|
+
if (knowledgeSourceIdentity && existing.fileId !== input.fileId) {
|
|
973
|
+
throw new Error("knowledge source document identity is bound to different content");
|
|
974
|
+
}
|
|
940
975
|
if (!documentMatchesAccess(existing, input.workspaceId, input.access)) {
|
|
941
976
|
throw new Error(`Document not found: ${existing.id}`);
|
|
942
977
|
}
|
|
@@ -992,6 +1027,7 @@ export async function addDocumentToBase(
|
|
|
992
1027
|
sourceCreatedAt: parseOptionalDate(input.sourceCreatedAt),
|
|
993
1028
|
sourceUpdatedAt: parseOptionalDate(input.sourceUpdatedAt),
|
|
994
1029
|
sourceVersion: cleanString(input.sourceVersion) ?? null,
|
|
1030
|
+
knowledgeSourceIdentity,
|
|
995
1031
|
aclTags: cleanStringArray(input.aclTags),
|
|
996
1032
|
authorityKind: authority.kind,
|
|
997
1033
|
authorityWorkspaceId: authority.workspaceId,
|
|
@@ -1060,7 +1096,9 @@ export async function moveDocumentToBase(
|
|
|
1060
1096
|
and(
|
|
1061
1097
|
eq(schema.documents.workspaceId, input.workspaceId),
|
|
1062
1098
|
eq(schema.documents.baseId, targetBaseId),
|
|
1063
|
-
|
|
1099
|
+
...(row.knowledgeSourceIdentity
|
|
1100
|
+
? [eq(schema.documents.knowledgeSourceIdentity, row.knowledgeSourceIdentity)]
|
|
1101
|
+
: [eq(schema.documents.fileId, row.fileId)]),
|
|
1064
1102
|
),
|
|
1065
1103
|
)
|
|
1066
1104
|
.limit(1);
|
|
@@ -1178,6 +1216,151 @@ export async function listDocuments(
|
|
|
1178
1216
|
});
|
|
1179
1217
|
}
|
|
1180
1218
|
|
|
1219
|
+
/**
|
|
1220
|
+
* List newly ready documents in the same effective scope used by agent
|
|
1221
|
+
* retrieval. The opaque checkpoint is bound to the account, requesting
|
|
1222
|
+
* workspace, and immutable initiating subject, so it cannot be reused across
|
|
1223
|
+
* scheduled-task authority boundaries.
|
|
1224
|
+
*/
|
|
1225
|
+
export async function listEffectiveIndexedDocuments(
|
|
1226
|
+
db: Database,
|
|
1227
|
+
input: ListEffectiveIndexedDocumentsInput,
|
|
1228
|
+
): Promise<ListIndexedDocumentsResponse> {
|
|
1229
|
+
const initiatingSubjectId = canonicalEffectiveDocumentSubject(input.initiatingSubjectId);
|
|
1230
|
+
const limit = input.limit ?? 50;
|
|
1231
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
1232
|
+
throw new Error("indexed document list limit must be between 1 and 100");
|
|
1233
|
+
}
|
|
1234
|
+
const afterSequence = input.checkpoint
|
|
1235
|
+
? decodeDocumentIndexCheckpoint(input.checkpoint, {
|
|
1236
|
+
accountId: input.accountId,
|
|
1237
|
+
workspaceId: input.workspaceId,
|
|
1238
|
+
initiatingSubjectId,
|
|
1239
|
+
})
|
|
1240
|
+
: 0n;
|
|
1241
|
+
const access: DocumentAccessFilter = {
|
|
1242
|
+
agentOnly: true,
|
|
1243
|
+
viewerSubjectId: initiatingSubjectId,
|
|
1244
|
+
};
|
|
1245
|
+
const rows = await withDocumentAccountRls(
|
|
1246
|
+
db,
|
|
1247
|
+
input.accountId,
|
|
1248
|
+
input.workspaceId,
|
|
1249
|
+
access,
|
|
1250
|
+
async (scopedDb) =>
|
|
1251
|
+
await scopedDb
|
|
1252
|
+
.select()
|
|
1253
|
+
.from(schema.documents)
|
|
1254
|
+
.where(
|
|
1255
|
+
and(
|
|
1256
|
+
eq(schema.documents.accountId, input.accountId),
|
|
1257
|
+
eq(schema.documents.status, "ready"),
|
|
1258
|
+
isNotNull(schema.documents.indexSequence),
|
|
1259
|
+
gt(schema.documents.indexSequence, afterSequence),
|
|
1260
|
+
...documentAccessConditions(input.workspaceId, access),
|
|
1261
|
+
),
|
|
1262
|
+
)
|
|
1263
|
+
.orderBy(asc(schema.documents.indexSequence))
|
|
1264
|
+
.limit(limit + 1),
|
|
1265
|
+
);
|
|
1266
|
+
const hasMore = rows.length > limit;
|
|
1267
|
+
const pageRows = rows.slice(0, limit);
|
|
1268
|
+
const nextSequence = pageRows.at(-1)?.indexSequence ?? afterSequence;
|
|
1269
|
+
if (nextSequence === null) {
|
|
1270
|
+
throw new Error("ready document is missing its index sequence");
|
|
1271
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
documents: pageRows.map(mapIndexedDocumentSummary),
|
|
1274
|
+
nextCheckpoint: encodeDocumentIndexCheckpoint({
|
|
1275
|
+
accountId: input.accountId,
|
|
1276
|
+
workspaceId: input.workspaceId,
|
|
1277
|
+
initiatingSubjectId,
|
|
1278
|
+
sequence: nextSequence,
|
|
1279
|
+
}),
|
|
1280
|
+
hasMore,
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
export function encodeDocumentIndexCheckpoint(input: {
|
|
1285
|
+
accountId: string;
|
|
1286
|
+
workspaceId: string;
|
|
1287
|
+
initiatingSubjectId: string;
|
|
1288
|
+
sequence: bigint;
|
|
1289
|
+
}): string {
|
|
1290
|
+
if (input.sequence < 0n) throw new Error("document index checkpoint sequence is invalid");
|
|
1291
|
+
return Buffer.from(
|
|
1292
|
+
JSON.stringify({
|
|
1293
|
+
v: 1,
|
|
1294
|
+
s: documentIndexCheckpointScope(input),
|
|
1295
|
+
q: input.sequence.toString(),
|
|
1296
|
+
}),
|
|
1297
|
+
"utf8",
|
|
1298
|
+
).toString("base64url");
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
export function decodeDocumentIndexCheckpoint(
|
|
1302
|
+
value: string,
|
|
1303
|
+
scope: { accountId: string; workspaceId: string; initiatingSubjectId: string },
|
|
1304
|
+
): bigint {
|
|
1305
|
+
try {
|
|
1306
|
+
if (!value || value.length > DOCUMENT_INDEX_CHECKPOINT_MAX_CHARS) {
|
|
1307
|
+
throw new Error("checkpoint length");
|
|
1308
|
+
}
|
|
1309
|
+
const bytes = Buffer.from(value, "base64url");
|
|
1310
|
+
if (bytes.toString("base64url") !== value) throw new Error("checkpoint encoding");
|
|
1311
|
+
const parsed = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
|
|
1312
|
+
if (
|
|
1313
|
+
Object.keys(parsed).sort().join(",") !== "q,s,v" ||
|
|
1314
|
+
parsed.v !== 1 ||
|
|
1315
|
+
typeof parsed.s !== "string" ||
|
|
1316
|
+
typeof parsed.q !== "string" ||
|
|
1317
|
+
!/^(0|[1-9][0-9]*)$/.test(parsed.q)
|
|
1318
|
+
) {
|
|
1319
|
+
throw new Error("checkpoint payload");
|
|
1320
|
+
}
|
|
1321
|
+
if (parsed.s !== documentIndexCheckpointScope(scope)) {
|
|
1322
|
+
throw new Error("document index checkpoint belongs to a different workspace or subject");
|
|
1323
|
+
}
|
|
1324
|
+
return BigInt(parsed.q);
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
if (
|
|
1327
|
+
error instanceof Error &&
|
|
1328
|
+
error.message === "document index checkpoint belongs to a different workspace or subject"
|
|
1329
|
+
) {
|
|
1330
|
+
throw error;
|
|
1331
|
+
}
|
|
1332
|
+
throw new Error("invalid document index checkpoint", { cause: error });
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function documentIndexCheckpointScope(input: {
|
|
1337
|
+
accountId: string;
|
|
1338
|
+
workspaceId: string;
|
|
1339
|
+
initiatingSubjectId: string;
|
|
1340
|
+
}): string {
|
|
1341
|
+
return createHash("sha256")
|
|
1342
|
+
.update("opengeni:document-index-checkpoint:v1\0")
|
|
1343
|
+
.update(input.accountId)
|
|
1344
|
+
.update("\0")
|
|
1345
|
+
.update(input.workspaceId)
|
|
1346
|
+
.update("\0")
|
|
1347
|
+
.update(canonicalEffectiveDocumentSubject(input.initiatingSubjectId))
|
|
1348
|
+
.digest("hex");
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function canonicalEffectiveDocumentSubject(value: string): string {
|
|
1352
|
+
const subjectId = cleanString(value);
|
|
1353
|
+
if (!subjectId || subjectId !== value) {
|
|
1354
|
+
throw new Error("effective document retrieval requires an initiating subject");
|
|
1355
|
+
}
|
|
1356
|
+
if (new TextEncoder().encode(subjectId).byteLength > DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES) {
|
|
1357
|
+
throw new Error(
|
|
1358
|
+
`effective document initiating subject exceeds ${DOCUMENT_AUTHORITY_SUBJECT_MAX_BYTES} UTF-8 bytes`,
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
return subjectId;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1181
1364
|
export async function getDocument(
|
|
1182
1365
|
db: Database,
|
|
1183
1366
|
workspaceId: string,
|
|
@@ -2317,6 +2500,43 @@ function mapDocument(row: typeof schema.documents.$inferSelect): Document {
|
|
|
2317
2500
|
};
|
|
2318
2501
|
}
|
|
2319
2502
|
|
|
2503
|
+
function mapIndexedDocumentSummary(
|
|
2504
|
+
row: typeof schema.documents.$inferSelect,
|
|
2505
|
+
): IndexedDocumentSummary {
|
|
2506
|
+
if (row.indexSequence === null || row.indexedAt === null) {
|
|
2507
|
+
throw new Error(`Ready document is missing index completion metadata: ${row.id}`);
|
|
2508
|
+
}
|
|
2509
|
+
return {
|
|
2510
|
+
id: row.id,
|
|
2511
|
+
title: row.title,
|
|
2512
|
+
parser: row.parser,
|
|
2513
|
+
chunkCount: row.chunkCount,
|
|
2514
|
+
indexedAt: row.indexedAt.toISOString(),
|
|
2515
|
+
summary: row.summary,
|
|
2516
|
+
topics: cleanStringArray(row.topics),
|
|
2517
|
+
source: {
|
|
2518
|
+
kind: normalizeKnowledgeSourceKind(row.sourceKind),
|
|
2519
|
+
uri: row.sourceUri,
|
|
2520
|
+
externalId: row.sourceExternalId,
|
|
2521
|
+
title: row.sourceTitle,
|
|
2522
|
+
author: row.sourceAuthor,
|
|
2523
|
+
createdAt: row.sourceCreatedAt?.toISOString() ?? null,
|
|
2524
|
+
updatedAt: row.sourceUpdatedAt?.toISOString() ?? null,
|
|
2525
|
+
version: row.sourceVersion,
|
|
2526
|
+
},
|
|
2527
|
+
provenance: {
|
|
2528
|
+
ingestionWorkspaceId: row.workspaceId,
|
|
2529
|
+
baseId: row.baseId,
|
|
2530
|
+
fileId: row.fileId,
|
|
2531
|
+
authorityKind: normalizeDocumentAuthorityKind(row.authorityKind),
|
|
2532
|
+
authorityWorkspaceId: row.authorityWorkspaceId,
|
|
2533
|
+
authoritySubjectId: row.authoritySubjectId,
|
|
2534
|
+
createdBy: row.createdBy,
|
|
2535
|
+
createdAt: row.createdAt.toISOString(),
|
|
2536
|
+
},
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2320
2540
|
function normalizeDocumentAuthorityKind(value: string): DocumentAuthorityKind {
|
|
2321
2541
|
switch (value) {
|
|
2322
2542
|
case "organization":
|