@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/documents",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.32",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"./google-drive": {
|
|
24
24
|
"types": "./dist/google-drive.d.ts",
|
|
25
25
|
"import": "./dist/google-drive.js"
|
|
26
|
+
},
|
|
27
|
+
"./atlassian": {
|
|
28
|
+
"types": "./dist/atlassian.d.ts",
|
|
29
|
+
"import": "./dist/atlassian.js"
|
|
26
30
|
}
|
|
27
31
|
},
|
|
28
32
|
"publishConfig": {
|
|
@@ -36,11 +40,11 @@
|
|
|
36
40
|
},
|
|
37
41
|
"dependencies": {
|
|
38
42
|
"@llamaindex/liteparse": "^1.5.3",
|
|
39
|
-
"@opengeni/config": "^0.
|
|
40
|
-
"@opengeni/contracts": "^0.
|
|
41
|
-
"@opengeni/db": "^0.
|
|
42
|
-
"@opengeni/storage": "^0.2.
|
|
43
|
+
"@opengeni/config": "^0.13.2",
|
|
44
|
+
"@opengeni/contracts": "^0.44.1",
|
|
45
|
+
"@opengeni/db": "^0.31.1",
|
|
46
|
+
"@opengeni/storage": "^0.2.87",
|
|
43
47
|
"drizzle-orm": "^0.45.2",
|
|
44
|
-
"openai": "6.
|
|
48
|
+
"openai": "6.47.0"
|
|
45
49
|
}
|
|
46
50
|
}
|
package/src/atlassian.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { ScopedKnowledgeScope } from "@opengeni/contracts";
|
|
2
|
+
import {
|
|
3
|
+
connectorDestinationDocumentAuthority,
|
|
4
|
+
resolveConnectorDocumentDestination,
|
|
5
|
+
type ConnectorDocumentDestination,
|
|
6
|
+
} from "@opengeni/contracts/connector-destinations";
|
|
7
|
+
import type { AtlassianSelectedSource } from "@opengeni/contracts/atlassian";
|
|
8
|
+
|
|
9
|
+
export const ATLASSIAN_PROVIDER_KEY = "atlassian" as const;
|
|
10
|
+
|
|
11
|
+
export type AtlassianKnowledgeSourceIdentity = {
|
|
12
|
+
providerKey: typeof ATLASSIAN_PROVIDER_KEY;
|
|
13
|
+
externalTenantId: string;
|
|
14
|
+
externalSourceId: string;
|
|
15
|
+
sourceKind: "atlassian-jira-project" | "atlassian-confluence-space";
|
|
16
|
+
sourceUri: string;
|
|
17
|
+
scope: ScopedKnowledgeScope;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type AtlassianInventoryProviderItem = {
|
|
21
|
+
id: string;
|
|
22
|
+
key: string;
|
|
23
|
+
title: string;
|
|
24
|
+
version: string | null;
|
|
25
|
+
createdAt: string | null;
|
|
26
|
+
updatedAt: string | null;
|
|
27
|
+
webUrl: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type AtlassianInventoryPage = {
|
|
31
|
+
items: AtlassianInventoryProviderItem[];
|
|
32
|
+
nextCursor: string | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type AtlassianInventoryEntry = {
|
|
36
|
+
externalObjectId: string;
|
|
37
|
+
externalVersionId: string | null;
|
|
38
|
+
sourceId: string;
|
|
39
|
+
parentFolderId: string;
|
|
40
|
+
driveId: null;
|
|
41
|
+
title: string;
|
|
42
|
+
mimeType: "text/markdown";
|
|
43
|
+
modifiedTime: string | null;
|
|
44
|
+
createdTime: string | null;
|
|
45
|
+
sourceUri: string;
|
|
46
|
+
transfer: {
|
|
47
|
+
action: "download";
|
|
48
|
+
contentType: "text/markdown";
|
|
49
|
+
filename: string;
|
|
50
|
+
declaredBytes: null;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type AtlassianInventoryStopReason =
|
|
55
|
+
| "api_request_limit"
|
|
56
|
+
| "elapsed_time_limit"
|
|
57
|
+
| "item_limit"
|
|
58
|
+
| "provider_error";
|
|
59
|
+
|
|
60
|
+
export type AtlassianInventoryCheckpoint = {
|
|
61
|
+
version: 1;
|
|
62
|
+
cloudId: string;
|
|
63
|
+
sourceId: string;
|
|
64
|
+
cursor: string | null;
|
|
65
|
+
itemCount: number;
|
|
66
|
+
apiRequestCount: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function atlassianKnowledgeScope(
|
|
70
|
+
destination: ConnectorDocumentDestination,
|
|
71
|
+
): ScopedKnowledgeScope {
|
|
72
|
+
const authority = connectorDestinationDocumentAuthority(destination);
|
|
73
|
+
if (authority.authorityKind === "organization") {
|
|
74
|
+
return { kind: "organization", workspaceId: null, subjectId: null };
|
|
75
|
+
}
|
|
76
|
+
if (authority.authorityKind === "workspace") {
|
|
77
|
+
if (!authority.authorityWorkspaceId) throw new Error("workspace authority is missing");
|
|
78
|
+
return { kind: "workspace", workspaceId: authority.authorityWorkspaceId, subjectId: null };
|
|
79
|
+
}
|
|
80
|
+
if (!authority.authorityWorkspaceId || !authority.authoritySubjectId) {
|
|
81
|
+
throw new Error("personal authority is missing");
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
kind: "personal",
|
|
85
|
+
workspaceId: authority.authorityWorkspaceId,
|
|
86
|
+
subjectId: authority.authoritySubjectId,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function atlassianKnowledgeSourceIdentity(input: {
|
|
91
|
+
source: AtlassianSelectedSource;
|
|
92
|
+
accountId: string;
|
|
93
|
+
workspaceId: string;
|
|
94
|
+
connectionSubjectId: string;
|
|
95
|
+
}): AtlassianKnowledgeSourceIdentity {
|
|
96
|
+
const destination = resolveConnectorDocumentDestination(input.source.destination, {
|
|
97
|
+
accountId: input.accountId,
|
|
98
|
+
workspaceId: input.workspaceId,
|
|
99
|
+
connectionSubjectId: input.connectionSubjectId,
|
|
100
|
+
});
|
|
101
|
+
const siteUrl = new URL(input.source.siteUrl);
|
|
102
|
+
const sourceUri =
|
|
103
|
+
input.source.kind === "jira_project"
|
|
104
|
+
? new URL(`/jira/software/c/projects/${encodeURIComponent(input.source.key)}`, siteUrl)
|
|
105
|
+
: new URL(`/wiki/spaces/${encodeURIComponent(input.source.key)}`, siteUrl);
|
|
106
|
+
return {
|
|
107
|
+
providerKey: ATLASSIAN_PROVIDER_KEY,
|
|
108
|
+
externalTenantId: bounded(input.source.cloudId, 256, "cloudId"),
|
|
109
|
+
externalSourceId: bounded(input.source.id, 300, "source.id"),
|
|
110
|
+
sourceKind:
|
|
111
|
+
input.source.kind === "jira_project"
|
|
112
|
+
? "atlassian-jira-project"
|
|
113
|
+
: "atlassian-confluence-space",
|
|
114
|
+
sourceUri: sourceUri.toString(),
|
|
115
|
+
scope: atlassianKnowledgeScope(destination),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function inventoryAtlassianSource(input: {
|
|
120
|
+
cloudId: string;
|
|
121
|
+
source: AtlassianSelectedSource;
|
|
122
|
+
limits: { maxItems: number; maxApiRequests: number; maxElapsedMs: number; pageSize: number };
|
|
123
|
+
checkpoint: Record<string, unknown> | null;
|
|
124
|
+
listPage: (cursor: string | null, pageSize: number) => Promise<AtlassianInventoryPage>;
|
|
125
|
+
}): Promise<{
|
|
126
|
+
status: "complete" | "paused";
|
|
127
|
+
stopReason: AtlassianInventoryStopReason | null;
|
|
128
|
+
entries: AtlassianInventoryEntry[];
|
|
129
|
+
checkpoint: AtlassianInventoryCheckpoint | null;
|
|
130
|
+
providerRequests: number;
|
|
131
|
+
elapsedMs: number;
|
|
132
|
+
}> {
|
|
133
|
+
const startedAt = Date.now();
|
|
134
|
+
const restored = parseCheckpoint(input.checkpoint, input.cloudId, input.source.id);
|
|
135
|
+
let cursor = restored?.cursor ?? null;
|
|
136
|
+
let itemCount = restored?.itemCount ?? 0;
|
|
137
|
+
let apiRequestCount = restored?.apiRequestCount ?? 0;
|
|
138
|
+
const entries: AtlassianInventoryEntry[] = [];
|
|
139
|
+
|
|
140
|
+
while (true) {
|
|
141
|
+
if (Date.now() - startedAt >= input.limits.maxElapsedMs) return paused("elapsed_time_limit");
|
|
142
|
+
if (apiRequestCount >= input.limits.maxApiRequests) return paused("api_request_limit");
|
|
143
|
+
if (itemCount >= input.limits.maxItems) return paused("item_limit");
|
|
144
|
+
|
|
145
|
+
let page: AtlassianInventoryPage;
|
|
146
|
+
try {
|
|
147
|
+
page = await input.listPage(
|
|
148
|
+
cursor,
|
|
149
|
+
Math.min(input.limits.pageSize, input.limits.maxItems - itemCount),
|
|
150
|
+
);
|
|
151
|
+
} catch {
|
|
152
|
+
return paused("provider_error");
|
|
153
|
+
}
|
|
154
|
+
apiRequestCount += 1;
|
|
155
|
+
for (const item of page.items) {
|
|
156
|
+
if (itemCount >= input.limits.maxItems) return paused("item_limit");
|
|
157
|
+
entries.push(atlassianEntry(input.source, item));
|
|
158
|
+
itemCount += 1;
|
|
159
|
+
}
|
|
160
|
+
cursor = page.nextCursor;
|
|
161
|
+
if (!cursor) {
|
|
162
|
+
return {
|
|
163
|
+
status: "complete",
|
|
164
|
+
stopReason: null,
|
|
165
|
+
entries,
|
|
166
|
+
checkpoint: null,
|
|
167
|
+
providerRequests: apiRequestCount - (restored?.apiRequestCount ?? 0),
|
|
168
|
+
elapsedMs: Date.now() - startedAt,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function paused(stopReason: AtlassianInventoryStopReason) {
|
|
174
|
+
return {
|
|
175
|
+
status: "paused" as const,
|
|
176
|
+
stopReason,
|
|
177
|
+
entries,
|
|
178
|
+
checkpoint: {
|
|
179
|
+
version: 1 as const,
|
|
180
|
+
cloudId: input.cloudId,
|
|
181
|
+
sourceId: input.source.id,
|
|
182
|
+
cursor,
|
|
183
|
+
itemCount,
|
|
184
|
+
apiRequestCount,
|
|
185
|
+
},
|
|
186
|
+
providerRequests: apiRequestCount - (restored?.apiRequestCount ?? 0),
|
|
187
|
+
elapsedMs: Date.now() - startedAt,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function atlassianEntry(
|
|
193
|
+
source: AtlassianSelectedSource,
|
|
194
|
+
item: AtlassianInventoryProviderItem,
|
|
195
|
+
): AtlassianInventoryEntry {
|
|
196
|
+
const title = bounded(item.title, 1024, "title");
|
|
197
|
+
return {
|
|
198
|
+
externalObjectId: bounded(`${source.kind}:${item.id}`, 512, "item.id"),
|
|
199
|
+
externalVersionId: item.version,
|
|
200
|
+
sourceId: source.id,
|
|
201
|
+
parentFolderId: source.resourceId,
|
|
202
|
+
driveId: null,
|
|
203
|
+
title,
|
|
204
|
+
mimeType: "text/markdown",
|
|
205
|
+
modifiedTime: item.updatedAt,
|
|
206
|
+
createdTime: item.createdAt,
|
|
207
|
+
sourceUri: new URL(item.webUrl).toString(),
|
|
208
|
+
transfer: {
|
|
209
|
+
action: "download",
|
|
210
|
+
contentType: "text/markdown",
|
|
211
|
+
filename: `${safeFilename(item.key || title)}.md`,
|
|
212
|
+
declaredBytes: null,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseCheckpoint(
|
|
218
|
+
value: Record<string, unknown> | null,
|
|
219
|
+
cloudId: string,
|
|
220
|
+
sourceId: string,
|
|
221
|
+
): AtlassianInventoryCheckpoint | null {
|
|
222
|
+
if (!value) return null;
|
|
223
|
+
if (
|
|
224
|
+
value.version !== 1 ||
|
|
225
|
+
value.cloudId !== cloudId ||
|
|
226
|
+
value.sourceId !== sourceId ||
|
|
227
|
+
(value.cursor !== null && typeof value.cursor !== "string") ||
|
|
228
|
+
!Number.isSafeInteger(value.itemCount) ||
|
|
229
|
+
!Number.isSafeInteger(value.apiRequestCount)
|
|
230
|
+
) {
|
|
231
|
+
throw new Error("invalid Atlassian inventory checkpoint");
|
|
232
|
+
}
|
|
233
|
+
return value as AtlassianInventoryCheckpoint;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function bounded(value: string, max: number, label: string): string {
|
|
237
|
+
const normalized = value.trim();
|
|
238
|
+
if (!normalized || normalized.length > max) throw new Error(`invalid ${label}`);
|
|
239
|
+
return normalized;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function safeFilename(value: string): string {
|
|
243
|
+
return (
|
|
244
|
+
value
|
|
245
|
+
.normalize("NFKC")
|
|
246
|
+
.replace(/[\\/:*?"<>|\0-\x1f]/g, "_")
|
|
247
|
+
.trim()
|
|
248
|
+
.slice(0, 220) || "item"
|
|
249
|
+
);
|
|
250
|
+
}
|
package/src/google-drive.ts
CHANGED
|
@@ -262,6 +262,7 @@ export function googleDriveKnowledgeScope(
|
|
|
262
262
|
|
|
263
263
|
export function googleDriveKnowledgeSourceIdentity(input: {
|
|
264
264
|
googlePermissionId: string;
|
|
265
|
+
googleEmail?: string | undefined;
|
|
265
266
|
source: Pick<GoogleDriveSelectedSource, "id" | "driveId" | "destination" | "targetScope">;
|
|
266
267
|
accountId?: string | undefined;
|
|
267
268
|
workspaceId: string;
|
|
@@ -270,7 +271,11 @@ export function googleDriveKnowledgeSourceIdentity(input: {
|
|
|
270
271
|
initiatingSubjectId?: string | undefined;
|
|
271
272
|
}): GoogleDriveKnowledgeSourceIdentity {
|
|
272
273
|
const sourceId = driveId(input.source.id, "source.id");
|
|
273
|
-
|
|
274
|
+
// permissionId identifies a principal, not a Google tenant. Consumer
|
|
275
|
+
// accounts intentionally share one provider tenant partition; Workspace
|
|
276
|
+
// accounts use their normalized hosted domain. Per-principal authority stays
|
|
277
|
+
// on the immutable connection owner binding.
|
|
278
|
+
const externalTenantId = googleTenantIdentity(input.googleEmail);
|
|
274
279
|
const sourceDriveId = nullableDriveId(input.source.driveId, "source.driveId");
|
|
275
280
|
const destination = resolveConnectorDocumentDestination(input.source.destination, {
|
|
276
281
|
accountId: input.accountId ?? input.workspaceId,
|
|
@@ -295,6 +300,19 @@ export function googleDriveKnowledgeSourceIdentity(input: {
|
|
|
295
300
|
};
|
|
296
301
|
}
|
|
297
302
|
|
|
303
|
+
function googleTenantIdentity(email: string | undefined): string {
|
|
304
|
+
if (email === undefined) return "google-consumer";
|
|
305
|
+
const normalized = email.trim().toLowerCase();
|
|
306
|
+
const separator = normalized.lastIndexOf("@");
|
|
307
|
+
if (separator <= 0 || separator === normalized.length - 1) {
|
|
308
|
+
throw new Error("googleEmail must be a normalized email address");
|
|
309
|
+
}
|
|
310
|
+
const domain = normalized.slice(separator + 1);
|
|
311
|
+
return domain === "gmail.com" || domain === "googlemail.com"
|
|
312
|
+
? "google-consumer"
|
|
313
|
+
: `google-workspace:${domain}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
298
316
|
export function planGoogleDriveTransfer(
|
|
299
317
|
item: GoogleDriveInventoryProviderItem,
|
|
300
318
|
maxFileBytes: number,
|
|
@@ -340,6 +358,7 @@ export function planGoogleDriveTransfer(
|
|
|
340
358
|
|
|
341
359
|
export async function inventoryGoogleDriveSource(input: {
|
|
342
360
|
googlePermissionId: string;
|
|
361
|
+
googleEmail?: string | undefined;
|
|
343
362
|
source: GoogleDriveSelectedSource;
|
|
344
363
|
accountId?: string | undefined;
|
|
345
364
|
workspaceId: string;
|
|
@@ -355,6 +374,7 @@ export async function inventoryGoogleDriveSource(input: {
|
|
|
355
374
|
const googlePermissionId = normalizedGooglePermissionId(input.googlePermissionId);
|
|
356
375
|
const source = googleDriveKnowledgeSourceIdentity({
|
|
357
376
|
googlePermissionId,
|
|
377
|
+
googleEmail: input.googleEmail,
|
|
358
378
|
source: input.source,
|
|
359
379
|
...(input.accountId ? { accountId: input.accountId } : {}),
|
|
360
380
|
workspaceId: input.workspaceId,
|