@xcitedbs/client 0.2.9 → 0.2.11
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/client.d.ts +122 -6
- package/dist/client.js +562 -111
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +217 -26
- package/dist/user-isolation.test.d.ts +1 -0
- package/dist/user-isolation.test.js +175 -0
- package/llms-full.txt +115 -92
- package/llms.txt +57 -39
- package/package.json +4 -2
package/dist/client.js
CHANGED
|
@@ -33,6 +33,7 @@ class XCiteDBClient {
|
|
|
33
33
|
this.onSessionInvalid = options.onSessionInvalid;
|
|
34
34
|
this.testSessionToken = options.testSessionToken;
|
|
35
35
|
this.testRequireAuth = options.testRequireAuth === true;
|
|
36
|
+
this.userIsolation = options.userIsolation;
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
38
39
|
* Create an ephemeral isolated database: calls `POST /api/v1/test/sessions` with your API key or Bearer,
|
|
@@ -51,6 +52,7 @@ class XCiteDBClient {
|
|
|
51
52
|
onSessionTokensUpdated: opts.onSessionTokensUpdated,
|
|
52
53
|
onAppUserTokensUpdated: opts.onAppUserTokensUpdated,
|
|
53
54
|
onSessionInvalid: opts.onSessionInvalid,
|
|
55
|
+
userIsolation: opts.userIsolation,
|
|
54
56
|
});
|
|
55
57
|
const data = await temp.request('POST', '/api/v1/test/sessions', undefined, undefined, { no401Retry: true });
|
|
56
58
|
return new XCiteDBClient({
|
|
@@ -67,6 +69,7 @@ class XCiteDBClient {
|
|
|
67
69
|
onSessionInvalid: opts.onSessionInvalid,
|
|
68
70
|
testSessionToken: data.session_token,
|
|
69
71
|
testRequireAuth: opts.testRequireAuth,
|
|
72
|
+
userIsolation: opts.userIsolation,
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
75
|
/**
|
|
@@ -144,6 +147,154 @@ class XCiteDBClient {
|
|
|
144
147
|
jti: typeof jti === 'string' ? jti : undefined,
|
|
145
148
|
};
|
|
146
149
|
}
|
|
150
|
+
cacheAppUserIdFromPair(pair) {
|
|
151
|
+
if (pair?.user?.user_id) {
|
|
152
|
+
this.cachedAppUserId = pair.user.user_id;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
clearAppUserIdCache() {
|
|
156
|
+
this.cachedAppUserId = undefined;
|
|
157
|
+
}
|
|
158
|
+
getAppUserId() {
|
|
159
|
+
if (this.cachedAppUserId) {
|
|
160
|
+
return this.cachedAppUserId;
|
|
161
|
+
}
|
|
162
|
+
const raw = this.appUserAccessToken;
|
|
163
|
+
if (!raw) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const p = XCiteDBClient.decodeJwtPayloadJson(raw);
|
|
167
|
+
if (!p) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const sub = p['sub'];
|
|
171
|
+
if (typeof sub === 'string' && sub.length > 0) {
|
|
172
|
+
return sub;
|
|
173
|
+
}
|
|
174
|
+
const uid = p['user_id'];
|
|
175
|
+
if (typeof uid === 'string' && uid.length > 0) {
|
|
176
|
+
return uid;
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
normalizeIsolationNamespaceTemplate(tpl) {
|
|
181
|
+
return tpl.replace(/\$\{user\.id\}/g, '{userId}');
|
|
182
|
+
}
|
|
183
|
+
canonicalId(s) {
|
|
184
|
+
let t = s.trim().replace(/\/+/g, '/');
|
|
185
|
+
if (!t.startsWith('/')) {
|
|
186
|
+
t = `/${t}`;
|
|
187
|
+
}
|
|
188
|
+
return t;
|
|
189
|
+
}
|
|
190
|
+
/** Resolved namespace root, e.g. `/users/abc`, or `null` if isolation is off or no app user id. */
|
|
191
|
+
userIsolationNamespace() {
|
|
192
|
+
if (!this.userIsolation?.enabled) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const uid = this.getAppUserId();
|
|
196
|
+
if (!uid) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const rawTpl = this.normalizeIsolationNamespaceTemplate(this.userIsolation.namespace ?? '/users/{userId}');
|
|
200
|
+
const trimmed = rawTpl.replace(/\/+$/, '') || '';
|
|
201
|
+
if (!trimmed) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return trimmed.replace(/{userId}/g, uid).replace(/{user_id}/g, uid);
|
|
205
|
+
}
|
|
206
|
+
allSharedPassthroughPrefixes() {
|
|
207
|
+
const o = this.userIsolation;
|
|
208
|
+
if (!o) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
return [...(o.shared_read_paths ?? []), ...(o.shared_write_paths ?? [])];
|
|
212
|
+
}
|
|
213
|
+
pathMatchesSharedPassthrough(path) {
|
|
214
|
+
const canon = this.canonicalId(path);
|
|
215
|
+
for (const raw of this.allSharedPassthroughPrefixes()) {
|
|
216
|
+
if (!raw) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const p = this.canonicalId(raw);
|
|
220
|
+
if (canon === p) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
if (p.endsWith('/') && canon.startsWith(p)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (!p.endsWith('/') && (canon === p || canon.startsWith(`${p}/`))) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
isoPrefixId(id) {
|
|
233
|
+
const ns = this.userIsolationNamespace();
|
|
234
|
+
if (!ns) {
|
|
235
|
+
return id;
|
|
236
|
+
}
|
|
237
|
+
if (id.includes('..')) {
|
|
238
|
+
throw new types_1.XCiteDBError('Invalid identifier: path traversal not allowed', 400, null);
|
|
239
|
+
}
|
|
240
|
+
const canonical = this.canonicalId(id);
|
|
241
|
+
if (canonical.startsWith(`${ns}/`) || canonical === ns) {
|
|
242
|
+
return canonical;
|
|
243
|
+
}
|
|
244
|
+
if (this.pathMatchesSharedPassthrough(canonical)) {
|
|
245
|
+
return canonical;
|
|
246
|
+
}
|
|
247
|
+
if (canonical.startsWith('/users/') && !canonical.startsWith(`${ns}/`) && canonical !== ns) {
|
|
248
|
+
return canonical;
|
|
249
|
+
}
|
|
250
|
+
const combined = `${ns}${canonical === '/' ? '/' : canonical}`;
|
|
251
|
+
const finalId = combined.replace(/\/+/g, '/');
|
|
252
|
+
if (!finalId.startsWith(ns) || (finalId.length > ns.length && finalId.charAt(ns.length) !== '/')) {
|
|
253
|
+
throw new types_1.XCiteDBError('Identifier escapes user namespace', 400, null);
|
|
254
|
+
}
|
|
255
|
+
return finalId;
|
|
256
|
+
}
|
|
257
|
+
isoUnprefixId(id) {
|
|
258
|
+
const ns = this.userIsolationNamespace();
|
|
259
|
+
if (!ns) {
|
|
260
|
+
return id;
|
|
261
|
+
}
|
|
262
|
+
const canonical = this.canonicalId(id);
|
|
263
|
+
if (this.pathMatchesSharedPassthrough(canonical)) {
|
|
264
|
+
return canonical;
|
|
265
|
+
}
|
|
266
|
+
if (canonical.startsWith(`${ns}/`)) {
|
|
267
|
+
return canonical.slice(ns.length);
|
|
268
|
+
}
|
|
269
|
+
if (canonical === ns) {
|
|
270
|
+
return '/';
|
|
271
|
+
}
|
|
272
|
+
return canonical;
|
|
273
|
+
}
|
|
274
|
+
isoPrefixQuery(q) {
|
|
275
|
+
if (!this.userIsolationNamespace()) {
|
|
276
|
+
return q;
|
|
277
|
+
}
|
|
278
|
+
const out = { ...q };
|
|
279
|
+
if (out.match) {
|
|
280
|
+
out.match = this.isoPrefixId(out.match);
|
|
281
|
+
}
|
|
282
|
+
if (out.match_start) {
|
|
283
|
+
out.match_start = this.isoPrefixId(out.match_start);
|
|
284
|
+
}
|
|
285
|
+
if (out.match_end) {
|
|
286
|
+
out.match_end = this.isoPrefixId(out.match_end);
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
isoApplyXmlDbIdentifier(xml) {
|
|
291
|
+
if (!this.userIsolationNamespace()) {
|
|
292
|
+
return xml;
|
|
293
|
+
}
|
|
294
|
+
return xml.replace(/(db:identifier\s*=\s*")([^"]*)(")/, (_m, p1, mid, p3) => {
|
|
295
|
+
return `${p1}${this.isoPrefixId(String(mid))}${p3}`;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
147
298
|
/** Destroy this test session on the server (`DELETE /api/v1/test/sessions/current`). */
|
|
148
299
|
async destroyTestSession() {
|
|
149
300
|
if (!this.testSessionToken) {
|
|
@@ -194,7 +345,11 @@ class XCiteDBClient {
|
|
|
194
345
|
this.projectId = projectId;
|
|
195
346
|
}
|
|
196
347
|
setContext(ctx) {
|
|
197
|
-
|
|
348
|
+
const next = { ...this.defaultContext, ...ctx };
|
|
349
|
+
if (ctx.workspace !== undefined) {
|
|
350
|
+
next.branch = ctx.workspace;
|
|
351
|
+
}
|
|
352
|
+
this.defaultContext = next;
|
|
198
353
|
}
|
|
199
354
|
setTokens(access, refresh) {
|
|
200
355
|
this.accessToken = access;
|
|
@@ -206,16 +361,19 @@ class XCiteDBClient {
|
|
|
206
361
|
this.appUserAccessToken = access;
|
|
207
362
|
if (refresh !== undefined)
|
|
208
363
|
this.appUserRefreshToken = refresh;
|
|
364
|
+
this.clearAppUserIdCache();
|
|
209
365
|
}
|
|
210
366
|
clearAppUserTokens() {
|
|
211
367
|
this.appUserAccessToken = undefined;
|
|
212
368
|
this.appUserRefreshToken = undefined;
|
|
369
|
+
this.clearAppUserIdCache();
|
|
213
370
|
}
|
|
214
371
|
contextHeaders() {
|
|
215
372
|
const h = {};
|
|
216
373
|
const c = this.defaultContext;
|
|
217
|
-
|
|
218
|
-
|
|
374
|
+
const ws = c.workspace ?? c.branch;
|
|
375
|
+
if (ws)
|
|
376
|
+
h['X-Workspace'] = ws;
|
|
219
377
|
if (c.date)
|
|
220
378
|
h['X-Date'] = c.date;
|
|
221
379
|
if (c.prefix)
|
|
@@ -261,14 +419,19 @@ class XCiteDBClient {
|
|
|
261
419
|
}
|
|
262
420
|
return h;
|
|
263
421
|
}
|
|
422
|
+
requestHeaders() {
|
|
423
|
+
return {
|
|
424
|
+
...this.authHeaders(),
|
|
425
|
+
...this.contextHeaders(),
|
|
426
|
+
...this.testHeaders(),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
264
429
|
async request(method, path, body, extraHeaders, opts) {
|
|
265
430
|
const no401Retry = opts?.no401Retry === true;
|
|
266
431
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
267
432
|
const url = joinUrl(this.baseUrl, path);
|
|
268
433
|
const headers = {
|
|
269
|
-
...this.
|
|
270
|
-
...this.contextHeaders(),
|
|
271
|
-
...this.testHeaders(),
|
|
434
|
+
...this.requestHeaders(),
|
|
272
435
|
...extraHeaders,
|
|
273
436
|
};
|
|
274
437
|
let init = { method, headers };
|
|
@@ -336,6 +499,7 @@ class XCiteDBClient {
|
|
|
336
499
|
const pair = await this.request('POST', '/api/v1/app/auth/refresh', this.mergeAppTenant({ refresh_token: this.appUserRefreshToken }), undefined, { no401Retry: true });
|
|
337
500
|
this.appUserAccessToken = pair.access_token;
|
|
338
501
|
this.appUserRefreshToken = pair.refresh_token;
|
|
502
|
+
this.cacheAppUserIdFromPair(pair);
|
|
339
503
|
this.onAppUserTokensUpdated?.(pair);
|
|
340
504
|
return pair;
|
|
341
505
|
}
|
|
@@ -474,12 +638,14 @@ class XCiteDBClient {
|
|
|
474
638
|
const pair = await this.request('POST', '/api/v1/app/auth/oauth/exchange', this.mergeAppTenant({ code }));
|
|
475
639
|
this.appUserAccessToken = pair.access_token;
|
|
476
640
|
this.appUserRefreshToken = pair.refresh_token;
|
|
641
|
+
this.cacheAppUserIdFromPair(pair);
|
|
477
642
|
return pair;
|
|
478
643
|
}
|
|
479
644
|
async loginAppUser(email, password) {
|
|
480
645
|
const pair = await this.request('POST', '/api/v1/app/auth/login', this.mergeAppTenant({ email, password }));
|
|
481
646
|
this.appUserAccessToken = pair.access_token;
|
|
482
647
|
this.appUserRefreshToken = pair.refresh_token;
|
|
648
|
+
this.cacheAppUserIdFromPair(pair);
|
|
483
649
|
return pair;
|
|
484
650
|
}
|
|
485
651
|
async refreshAppUser() {
|
|
@@ -489,6 +655,7 @@ class XCiteDBClient {
|
|
|
489
655
|
await this.request('POST', '/api/v1/app/auth/logout', this.mergeAppTenant({ refresh_token: this.appUserRefreshToken }));
|
|
490
656
|
this.appUserAccessToken = undefined;
|
|
491
657
|
this.appUserRefreshToken = undefined;
|
|
658
|
+
this.clearAppUserIdCache();
|
|
492
659
|
}
|
|
493
660
|
async appUserMe() {
|
|
494
661
|
return this.request('GET', '/api/v1/app/auth/me');
|
|
@@ -505,6 +672,7 @@ class XCiteDBClient {
|
|
|
505
672
|
const pair = await this.request('POST', '/api/v1/app/auth/custom-token', { token });
|
|
506
673
|
this.appUserAccessToken = pair.access_token;
|
|
507
674
|
this.appUserRefreshToken = pair.refresh_token;
|
|
675
|
+
this.cacheAppUserIdFromPair(pair);
|
|
508
676
|
return pair;
|
|
509
677
|
}
|
|
510
678
|
/** Change app-user password (requires valid app-user access token). */
|
|
@@ -721,7 +889,7 @@ class XCiteDBClient {
|
|
|
721
889
|
* ```
|
|
722
890
|
*/
|
|
723
891
|
async checkAccess(subject, identifier, action, metaPath, branch) {
|
|
724
|
-
const body = { subject, identifier, action };
|
|
892
|
+
const body = { subject, identifier: this.isoPrefixId(identifier), action };
|
|
725
893
|
if (metaPath !== undefined)
|
|
726
894
|
body.meta_path = metaPath;
|
|
727
895
|
if (branch !== undefined)
|
|
@@ -747,124 +915,237 @@ class XCiteDBClient {
|
|
|
747
915
|
async updateSecurityConfig(config) {
|
|
748
916
|
await this.request('PUT', '/api/v1/security/config', config);
|
|
749
917
|
}
|
|
750
|
-
|
|
918
|
+
/** Per-tenant user data spaces (`GET /api/v1/security/user-isolation`). Requires security admin. */
|
|
919
|
+
async getUserIsolationConfig() {
|
|
920
|
+
return this.request('GET', '/api/v1/security/user-isolation');
|
|
921
|
+
}
|
|
922
|
+
/** Enable or reconfigure user isolation (`PUT /api/v1/security/user-isolation`). */
|
|
923
|
+
async setUserIsolationConfig(config) {
|
|
924
|
+
return this.request('PUT', '/api/v1/security/user-isolation', config);
|
|
925
|
+
}
|
|
926
|
+
/** Disable user isolation and remove generated policies (`DELETE /api/v1/security/user-isolation`). */
|
|
927
|
+
async disableUserIsolation() {
|
|
928
|
+
await this.request('DELETE', '/api/v1/security/user-isolation');
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Loads server isolation config; when enabled, configures client-side identifier prefixing to match
|
|
932
|
+
* the server (namespace + shared paths). Does not send `X-Prefix`; identifiers in requests are rewritten.
|
|
933
|
+
*/
|
|
934
|
+
async enableUserIsolation() {
|
|
935
|
+
const cfg = await this.getUserIsolationConfig();
|
|
936
|
+
if (cfg.enabled) {
|
|
937
|
+
this.userIsolation = {
|
|
938
|
+
enabled: true,
|
|
939
|
+
namespace: cfg.namespace_pattern,
|
|
940
|
+
shared_read_paths: cfg.shared_read_paths,
|
|
941
|
+
shared_write_paths: cfg.shared_write_paths,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
this.userIsolation = { enabled: false };
|
|
946
|
+
}
|
|
947
|
+
return cfg;
|
|
948
|
+
}
|
|
949
|
+
async createWorkspace(name, fromBranch, fromDate) {
|
|
751
950
|
const body = { name };
|
|
752
951
|
if (fromBranch)
|
|
753
952
|
body.from_branch = fromBranch;
|
|
754
953
|
if (fromDate)
|
|
755
954
|
body.from_date = fromDate;
|
|
756
|
-
await this.request('POST', '/api/v1/
|
|
955
|
+
await this.request('POST', '/api/v1/workspaces', body);
|
|
956
|
+
}
|
|
957
|
+
/** @deprecated Use {@link createWorkspace}. */
|
|
958
|
+
async createBranch(name, fromBranch, fromDate) {
|
|
959
|
+
return this.createWorkspace(name, fromBranch, fromDate);
|
|
757
960
|
}
|
|
961
|
+
async deleteWorkspace(name) {
|
|
962
|
+
await this.request('DELETE', `/api/v1/workspaces/${encodeURIComponent(name)}`);
|
|
963
|
+
}
|
|
964
|
+
/** @deprecated Use {@link deleteWorkspace}. */
|
|
758
965
|
async deleteBranch(name) {
|
|
759
|
-
|
|
966
|
+
return this.deleteWorkspace(name);
|
|
967
|
+
}
|
|
968
|
+
async deleteWorkspaceRevision(workspace, date) {
|
|
969
|
+
await this.request('DELETE', `/api/v1/workspaces/${encodeURIComponent(workspace)}/revisions/${encodeURIComponent(date)}`);
|
|
760
970
|
}
|
|
971
|
+
/** @deprecated Use {@link deleteWorkspaceRevision}. */
|
|
761
972
|
async deleteRevision(branch, date) {
|
|
762
|
-
|
|
973
|
+
return this.deleteWorkspaceRevision(branch, date);
|
|
763
974
|
}
|
|
975
|
+
async listWorkspaces() {
|
|
976
|
+
const r = await this.request('GET', '/api/v1/workspaces');
|
|
977
|
+
return r.workspaces ?? r.branches ?? [];
|
|
978
|
+
}
|
|
979
|
+
/** @deprecated Use {@link listWorkspaces}. */
|
|
764
980
|
async listBranches() {
|
|
765
|
-
|
|
766
|
-
|
|
981
|
+
return this.listWorkspaces();
|
|
982
|
+
}
|
|
983
|
+
async getWorkspace(name) {
|
|
984
|
+
const r = await this.request('GET', `/api/v1/workspaces/${encodeURIComponent(name)}`);
|
|
985
|
+
return (r.workspace ?? r.branch);
|
|
767
986
|
}
|
|
987
|
+
/** @deprecated Use {@link getWorkspace}. */
|
|
768
988
|
async getBranch(name) {
|
|
769
|
-
|
|
770
|
-
return r.branch;
|
|
989
|
+
return this.getWorkspace(name);
|
|
771
990
|
}
|
|
772
|
-
async
|
|
991
|
+
async createCheckpoint(message, author) {
|
|
773
992
|
const body = { message };
|
|
774
993
|
if (author)
|
|
775
994
|
body.author = author;
|
|
776
|
-
const r = await this.request('POST', '/api/v1/
|
|
777
|
-
return r.commit;
|
|
995
|
+
const r = await this.request('POST', '/api/v1/checkpoints', body);
|
|
996
|
+
return (r.checkpoint ?? r.commit);
|
|
778
997
|
}
|
|
779
|
-
|
|
998
|
+
/** @deprecated Use {@link createCheckpoint}. */
|
|
999
|
+
async createCommit(message, author) {
|
|
1000
|
+
return this.createCheckpoint(message, author);
|
|
1001
|
+
}
|
|
1002
|
+
async listCheckpoints(options) {
|
|
780
1003
|
const q = buildQuery({
|
|
781
1004
|
branch: options?.branch,
|
|
782
1005
|
limit: options?.limit,
|
|
783
1006
|
offset: options?.offset,
|
|
784
1007
|
});
|
|
785
|
-
|
|
1008
|
+
const r = await this.request('GET', `/api/v1/checkpoints${q}`);
|
|
1009
|
+
const list = r.checkpoints ?? r.commits ?? [];
|
|
1010
|
+
return { checkpoints: list, total: r.total, branch: r.branch };
|
|
1011
|
+
}
|
|
1012
|
+
/** @deprecated Use {@link listCheckpoints}. */
|
|
1013
|
+
async listCommits(options) {
|
|
1014
|
+
const r = await this.listCheckpoints(options);
|
|
1015
|
+
return { commits: r.checkpoints, total: r.total, branch: r.branch };
|
|
1016
|
+
}
|
|
1017
|
+
async getCheckpoint(checkpointId) {
|
|
1018
|
+
const r = await this.request('GET', `/api/v1/checkpoints/${encodeURIComponent(checkpointId)}`);
|
|
1019
|
+
return (r.checkpoint ?? r.commit);
|
|
786
1020
|
}
|
|
1021
|
+
/** @deprecated Use {@link getCheckpoint}. */
|
|
787
1022
|
async getCommit(commitId) {
|
|
788
|
-
|
|
789
|
-
return r.commit;
|
|
1023
|
+
return this.getCheckpoint(commitId);
|
|
790
1024
|
}
|
|
791
|
-
async
|
|
792
|
-
return this.request('POST', `/api/v1/
|
|
1025
|
+
async revertToCheckpoint(checkpointId, _confirm) {
|
|
1026
|
+
return this.request('POST', `/api/v1/checkpoints/${encodeURIComponent(checkpointId)}/revert`, {
|
|
793
1027
|
confirm: true,
|
|
794
1028
|
});
|
|
795
1029
|
}
|
|
796
|
-
|
|
1030
|
+
/** @deprecated Use {@link revertToCheckpoint}. */
|
|
1031
|
+
async rollbackToCommit(commitId, _confirm) {
|
|
1032
|
+
const r = await this.revertToCheckpoint(commitId, _confirm);
|
|
1033
|
+
return {
|
|
1034
|
+
rolled_back_commits: r.rolled_back_commits ?? r.rolled_back_checkpoints,
|
|
1035
|
+
current_tip: r.current_tip,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
async applyCheckpoint(checkpointId, message, author) {
|
|
797
1039
|
const body = {};
|
|
798
1040
|
if (message)
|
|
799
1041
|
body.message = message;
|
|
800
1042
|
if (author)
|
|
801
1043
|
body.author = author;
|
|
802
|
-
const r = await this.request('POST', `/api/v1/
|
|
803
|
-
return r.commit;
|
|
1044
|
+
const r = await this.request('POST', `/api/v1/checkpoints/${encodeURIComponent(checkpointId)}/apply`, body);
|
|
1045
|
+
return (r.checkpoint ?? r.commit);
|
|
804
1046
|
}
|
|
805
|
-
|
|
806
|
-
|
|
1047
|
+
/** @deprecated Use {@link applyCheckpoint}. */
|
|
1048
|
+
async cherryPick(commitId, message, author) {
|
|
1049
|
+
return this.applyCheckpoint(commitId, message, author);
|
|
1050
|
+
}
|
|
1051
|
+
async createBookmark(name, checkpointId, message, author) {
|
|
1052
|
+
const body = { name, checkpoint_id: checkpointId };
|
|
807
1053
|
if (message)
|
|
808
1054
|
body.message = message;
|
|
809
1055
|
if (author)
|
|
810
1056
|
body.author = author;
|
|
811
|
-
const r = await this.request('POST', '/api/v1/
|
|
812
|
-
return r.tag;
|
|
1057
|
+
const r = await this.request('POST', '/api/v1/bookmarks', body);
|
|
1058
|
+
return (r.bookmark ?? r.tag);
|
|
813
1059
|
}
|
|
814
|
-
|
|
1060
|
+
/** @deprecated Use {@link createBookmark}. */
|
|
1061
|
+
async createTag(name, commitId, message, author) {
|
|
1062
|
+
return this.createBookmark(name, commitId, message, author);
|
|
1063
|
+
}
|
|
1064
|
+
async listBookmarks(options) {
|
|
815
1065
|
const q = buildQuery({ limit: options?.limit, offset: options?.offset });
|
|
816
|
-
|
|
1066
|
+
const r = await this.request('GET', `/api/v1/bookmarks${q}`);
|
|
1067
|
+
const list = r.bookmarks ?? r.tags ?? [];
|
|
1068
|
+
return { bookmarks: list, total: r.total };
|
|
817
1069
|
}
|
|
1070
|
+
/** @deprecated Use {@link listBookmarks}. */
|
|
1071
|
+
async listTags(options) {
|
|
1072
|
+
const r = await this.listBookmarks(options);
|
|
1073
|
+
return { tags: r.bookmarks, total: r.total };
|
|
1074
|
+
}
|
|
1075
|
+
async getBookmark(name) {
|
|
1076
|
+
const r = await this.request('GET', `/api/v1/bookmarks/${encodeURIComponent(name)}`);
|
|
1077
|
+
return (r.bookmark ?? r.tag);
|
|
1078
|
+
}
|
|
1079
|
+
/** @deprecated Use {@link getBookmark}. */
|
|
818
1080
|
async getTag(name) {
|
|
819
|
-
|
|
820
|
-
return r.tag;
|
|
1081
|
+
return this.getBookmark(name);
|
|
821
1082
|
}
|
|
1083
|
+
async deleteBookmark(name) {
|
|
1084
|
+
await this.request('DELETE', `/api/v1/bookmarks/${encodeURIComponent(name)}`);
|
|
1085
|
+
}
|
|
1086
|
+
/** @deprecated Use {@link deleteBookmark}. */
|
|
822
1087
|
async deleteTag(name) {
|
|
823
|
-
|
|
1088
|
+
return this.deleteBookmark(name);
|
|
824
1089
|
}
|
|
825
|
-
async
|
|
826
|
-
return this.request('POST', '/api/v1/
|
|
1090
|
+
async compare(from, to, includeContent) {
|
|
1091
|
+
return this.request('POST', '/api/v1/compare', {
|
|
827
1092
|
from,
|
|
828
1093
|
to,
|
|
829
1094
|
include_content: includeContent ?? false,
|
|
830
1095
|
});
|
|
831
1096
|
}
|
|
832
|
-
|
|
833
|
-
|
|
1097
|
+
/** @deprecated Use {@link compare}. */
|
|
1098
|
+
async diff(from, to, includeContent) {
|
|
1099
|
+
return this.compare(from, to, includeContent);
|
|
1100
|
+
}
|
|
1101
|
+
async publishWorkspace(targetWorkspace, sourceWorkspace, options) {
|
|
1102
|
+
const body = {
|
|
1103
|
+
source_workspace: sourceWorkspace,
|
|
1104
|
+
source_branch: sourceWorkspace,
|
|
1105
|
+
};
|
|
834
1106
|
if (options?.message)
|
|
835
1107
|
body.message = options.message;
|
|
836
1108
|
body.auto_resolve = options?.autoResolve ?? 'none';
|
|
837
|
-
return this.request('POST', `/api/v1/
|
|
1109
|
+
return this.request('POST', `/api/v1/workspaces/${encodeURIComponent(targetWorkspace)}/publish`, body);
|
|
1110
|
+
}
|
|
1111
|
+
/** @deprecated Use {@link publishWorkspace}. */
|
|
1112
|
+
async mergeBranch(targetBranch, sourceBranch, options) {
|
|
1113
|
+
return this.publishWorkspace(targetBranch, sourceBranch, options);
|
|
838
1114
|
}
|
|
839
1115
|
/**
|
|
840
|
-
* Create `
|
|
841
|
-
* create a
|
|
842
|
-
* Restores previous {@link DatabaseContext} afterward.
|
|
1116
|
+
* Create `workspaceName` from {@link options.fromBranch} (or current context), run `fn` scoped to that workspace,
|
|
1117
|
+
* create a checkpoint, then publish back unless {@link options.autoMerge} is `false`.
|
|
843
1118
|
*/
|
|
844
|
-
async
|
|
1119
|
+
async withWorkspace(workspaceName, fn, options) {
|
|
845
1120
|
const prev = { ...this.defaultContext };
|
|
846
|
-
const
|
|
847
|
-
let
|
|
848
|
-
let
|
|
1121
|
+
const fromWs = options?.fromBranch ?? prev.branch ?? prev.workspace ?? '';
|
|
1122
|
+
let checkpoint;
|
|
1123
|
+
let publish;
|
|
849
1124
|
try {
|
|
850
|
-
await this.
|
|
851
|
-
this.setContext({ branch:
|
|
1125
|
+
await this.createWorkspace(workspaceName, fromWs || undefined, prev.date || undefined);
|
|
1126
|
+
this.setContext({ workspace: workspaceName, branch: workspaceName });
|
|
852
1127
|
const result = await fn(this);
|
|
853
|
-
|
|
1128
|
+
checkpoint = await this.createCheckpoint(options?.message ?? `Workspace ${workspaceName}`, options?.author);
|
|
854
1129
|
if (options?.autoMerge !== false) {
|
|
855
|
-
|
|
1130
|
+
publish = await this.publishWorkspace(fromWs, workspaceName, {
|
|
856
1131
|
message: options?.message,
|
|
857
1132
|
});
|
|
858
1133
|
}
|
|
859
|
-
return { result,
|
|
1134
|
+
return { result, checkpoint, publish };
|
|
860
1135
|
}
|
|
861
1136
|
finally {
|
|
862
1137
|
this.setContext(prev);
|
|
863
1138
|
}
|
|
864
1139
|
}
|
|
1140
|
+
/** @deprecated Use {@link withWorkspace}. */
|
|
1141
|
+
async withBranch(branchName, fn, options) {
|
|
1142
|
+
const r = await this.withWorkspace(branchName, fn, options);
|
|
1143
|
+
return { result: r.result, commit: r.checkpoint, merge: r.publish };
|
|
1144
|
+
}
|
|
865
1145
|
/** Send raw XML body (`Content-Type: application/xml`). For JSON wrapper + options use `writeXmlDocument`. */
|
|
866
1146
|
async writeXML(xml, _options) {
|
|
867
|
-
|
|
1147
|
+
const payload = this.isoApplyXmlDbIdentifier(xml);
|
|
1148
|
+
await this.request('POST', '/api/v1/documents', payload, {
|
|
868
1149
|
'Content-Type': 'application/xml',
|
|
869
1150
|
});
|
|
870
1151
|
}
|
|
@@ -874,7 +1155,7 @@ class XCiteDBClient {
|
|
|
874
1155
|
*/
|
|
875
1156
|
async writeXmlDocument(xml, options) {
|
|
876
1157
|
await this.request('POST', '/api/v1/documents', {
|
|
877
|
-
xml,
|
|
1158
|
+
xml: this.isoApplyXmlDbIdentifier(xml),
|
|
878
1159
|
is_top: options?.is_top ?? true,
|
|
879
1160
|
compare_attributes: options?.compare_attributes ?? false,
|
|
880
1161
|
});
|
|
@@ -887,92 +1168,97 @@ class XCiteDBClient {
|
|
|
887
1168
|
}
|
|
888
1169
|
async queryByIdentifier(identifier, flags, filter, pathFilter) {
|
|
889
1170
|
const q = buildQuery({
|
|
890
|
-
identifier,
|
|
1171
|
+
identifier: this.isoPrefixId(identifier),
|
|
891
1172
|
flags: flags,
|
|
892
1173
|
filter,
|
|
893
1174
|
path_filter: pathFilter,
|
|
894
1175
|
});
|
|
895
|
-
|
|
1176
|
+
const rows = await this.request('GET', `/api/v1/documents/by-id${q}`);
|
|
1177
|
+
return Array.isArray(rows) ? rows.map((x) => this.isoUnprefixId(String(x))) : rows;
|
|
896
1178
|
}
|
|
897
1179
|
async queryDocuments(query, flags, filter, pathFilter) {
|
|
1180
|
+
const pq = this.isoPrefixQuery(query);
|
|
898
1181
|
const params = {
|
|
899
|
-
match:
|
|
900
|
-
match_start:
|
|
901
|
-
match_end:
|
|
902
|
-
regex:
|
|
1182
|
+
match: pq.match,
|
|
1183
|
+
match_start: pq.match_start,
|
|
1184
|
+
match_end: pq.match_end,
|
|
1185
|
+
regex: pq.regex,
|
|
903
1186
|
flags: flags,
|
|
904
1187
|
filter,
|
|
905
1188
|
path_filter: pathFilter,
|
|
906
1189
|
};
|
|
907
|
-
if (
|
|
908
|
-
const c = Array.isArray(
|
|
1190
|
+
if (pq.contains !== undefined) {
|
|
1191
|
+
const c = Array.isArray(pq.contains) ? pq.contains.join(',') : pq.contains;
|
|
909
1192
|
params.contains = c;
|
|
910
1193
|
}
|
|
911
|
-
if (
|
|
912
|
-
params.required_meta_paths =
|
|
1194
|
+
if (pq.required_meta_paths !== undefined && pq.required_meta_paths.length > 0) {
|
|
1195
|
+
params.required_meta_paths = pq.required_meta_paths.join(',');
|
|
913
1196
|
}
|
|
914
|
-
if (
|
|
1197
|
+
if (pq.filter_any_meta === true) {
|
|
915
1198
|
params.any_meta = '1';
|
|
916
1199
|
}
|
|
917
|
-
|
|
1200
|
+
const rows = await this.request('GET', `/api/v1/documents${buildQuery(params)}`);
|
|
1201
|
+
return Array.isArray(rows) ? rows.map((x) => this.isoUnprefixId(String(x))) : rows;
|
|
918
1202
|
}
|
|
919
1203
|
async deleteDocument(identifier) {
|
|
920
|
-
await this.request('DELETE', `/api/v1/documents/by-id${buildQuery({ identifier })}`);
|
|
1204
|
+
await this.request('DELETE', `/api/v1/documents/by-id${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
921
1205
|
}
|
|
922
1206
|
async addIdentifier(identifier) {
|
|
923
1207
|
const r = await this.request('POST', '/api/v1/documents/identifiers', {
|
|
924
|
-
identifier,
|
|
1208
|
+
identifier: this.isoPrefixId(identifier),
|
|
925
1209
|
});
|
|
926
1210
|
return r?.ok !== false;
|
|
927
1211
|
}
|
|
928
1212
|
async addAlias(original, alias) {
|
|
929
|
-
const r = await this.request('POST', '/api/v1/documents/identifiers/alias', { original, alias });
|
|
1213
|
+
const r = await this.request('POST', '/api/v1/documents/identifiers/alias', { original: this.isoPrefixId(original), alias: this.isoPrefixId(alias) });
|
|
930
1214
|
return r?.ok !== false;
|
|
931
1215
|
}
|
|
932
1216
|
async queryChangeDate(identifier) {
|
|
933
|
-
const r = await this.request('GET', `/api/v1/documents/change-date${buildQuery({ identifier })}`);
|
|
1217
|
+
const r = await this.request('GET', `/api/v1/documents/change-date${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
934
1218
|
return r?.change_date ?? r?.date ?? '';
|
|
935
1219
|
}
|
|
936
1220
|
async getXcitepath(identifier) {
|
|
937
|
-
const r = await this.request('GET', `/api/v1/documents/xcitepath${buildQuery({ identifier })}`);
|
|
1221
|
+
const r = await this.request('GET', `/api/v1/documents/xcitepath${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
938
1222
|
return r?.xcitepath ?? '';
|
|
939
1223
|
}
|
|
940
1224
|
async changedIdentifiers(branch, fromDate, toDate) {
|
|
941
|
-
|
|
1225
|
+
const ids = await this.request('GET', `/api/v1/documents/changed${buildQuery({ branch, from_date: fromDate, to_date: toDate })}`);
|
|
1226
|
+
return Array.isArray(ids) ? ids.map((x) => this.isoUnprefixId(String(x))) : ids;
|
|
942
1227
|
}
|
|
943
1228
|
async listIdentifiers(query) {
|
|
1229
|
+
const pq = this.isoPrefixQuery(query);
|
|
944
1230
|
const params = {
|
|
945
|
-
match:
|
|
946
|
-
match_start:
|
|
947
|
-
match_end:
|
|
948
|
-
regex:
|
|
949
|
-
limit:
|
|
950
|
-
offset:
|
|
1231
|
+
match: pq.match,
|
|
1232
|
+
match_start: pq.match_start,
|
|
1233
|
+
match_end: pq.match_end,
|
|
1234
|
+
regex: pq.regex,
|
|
1235
|
+
limit: pq.limit,
|
|
1236
|
+
offset: pq.offset,
|
|
951
1237
|
};
|
|
952
|
-
if (
|
|
953
|
-
params.contains = Array.isArray(
|
|
954
|
-
? query.contains.join(',')
|
|
955
|
-
: query.contains;
|
|
1238
|
+
if (pq.contains !== undefined) {
|
|
1239
|
+
params.contains = Array.isArray(pq.contains) ? pq.contains.join(',') : pq.contains;
|
|
956
1240
|
}
|
|
957
|
-
if (
|
|
958
|
-
params.required_meta_paths =
|
|
1241
|
+
if (pq.required_meta_paths !== undefined && pq.required_meta_paths.length > 0) {
|
|
1242
|
+
params.required_meta_paths = pq.required_meta_paths.join(',');
|
|
959
1243
|
}
|
|
960
|
-
if (
|
|
1244
|
+
if (pq.filter_any_meta === true) {
|
|
961
1245
|
params.any_meta = '1';
|
|
962
1246
|
}
|
|
963
1247
|
const data = await this.request('GET', `/api/v1/documents/identifiers${buildQuery(params)}`);
|
|
1248
|
+
const un = (ids) => ids.map((id) => this.isoUnprefixId(id));
|
|
964
1249
|
// Older servers returned a bare string[]; paginated API returns { identifiers, total, offset, limit }.
|
|
965
1250
|
if (Array.isArray(data)) {
|
|
1251
|
+
const ids = un(data);
|
|
966
1252
|
return {
|
|
967
|
-
identifiers:
|
|
968
|
-
total:
|
|
1253
|
+
identifiers: ids,
|
|
1254
|
+
total: ids.length,
|
|
969
1255
|
offset: 0,
|
|
970
|
-
limit:
|
|
1256
|
+
limit: ids.length,
|
|
971
1257
|
};
|
|
972
1258
|
}
|
|
973
1259
|
if (data !== null && typeof data === 'object') {
|
|
974
1260
|
const o = data;
|
|
975
|
-
const ids = Array.isArray(o.identifiers) ? o.identifiers : [];
|
|
1261
|
+
const ids = Array.isArray(o.identifiers) ? un(o.identifiers) : [];
|
|
976
1262
|
return {
|
|
977
1263
|
identifiers: ids,
|
|
978
1264
|
total: typeof o.total === 'number' ? o.total : ids.length,
|
|
@@ -985,7 +1271,7 @@ class XCiteDBClient {
|
|
|
985
1271
|
async listIdentifierChildren(parentPath) {
|
|
986
1272
|
const params = {};
|
|
987
1273
|
if (parentPath !== undefined && parentPath !== '') {
|
|
988
|
-
params.parent_path = parentPath;
|
|
1274
|
+
params.parent_path = this.isoPrefixId(parentPath);
|
|
989
1275
|
}
|
|
990
1276
|
const data = await this.request('GET', `/api/v1/documents/identifier-children${buildQuery(params)}`);
|
|
991
1277
|
if (data !== null && typeof data === 'object') {
|
|
@@ -998,7 +1284,7 @@ class XCiteDBClient {
|
|
|
998
1284
|
const r = row;
|
|
999
1285
|
children.push({
|
|
1000
1286
|
segment: typeof r.segment === 'string' ? r.segment : '',
|
|
1001
|
-
full_path: typeof r.full_path === 'string' ? r.full_path : '',
|
|
1287
|
+
full_path: typeof r.full_path === 'string' ? this.isoUnprefixId(r.full_path) : '',
|
|
1002
1288
|
is_identifier: r.is_identifier === true,
|
|
1003
1289
|
has_children: r.has_children === true,
|
|
1004
1290
|
});
|
|
@@ -1006,7 +1292,7 @@ class XCiteDBClient {
|
|
|
1006
1292
|
}
|
|
1007
1293
|
}
|
|
1008
1294
|
return {
|
|
1009
|
-
parent_path: typeof o.parent_path === 'string' ? o.parent_path : '',
|
|
1295
|
+
parent_path: typeof o.parent_path === 'string' ? this.isoUnprefixId(o.parent_path) : '',
|
|
1010
1296
|
parent_is_identifier: o.parent_is_identifier === true,
|
|
1011
1297
|
hierarchy_index_available: o.hierarchy_index_available === true,
|
|
1012
1298
|
children,
|
|
@@ -1020,16 +1306,17 @@ class XCiteDBClient {
|
|
|
1020
1306
|
};
|
|
1021
1307
|
}
|
|
1022
1308
|
async queryLog(query, fromDate, toDate) {
|
|
1309
|
+
const pq = this.isoPrefixQuery(query);
|
|
1023
1310
|
const params = {
|
|
1024
|
-
match_start:
|
|
1025
|
-
match:
|
|
1311
|
+
match_start: pq.match_start,
|
|
1312
|
+
match: pq.match,
|
|
1026
1313
|
from_date: fromDate,
|
|
1027
1314
|
to_date: toDate,
|
|
1028
1315
|
};
|
|
1029
1316
|
return this.request('GET', `/api/v1/documents/log${buildQuery(params)}`);
|
|
1030
1317
|
}
|
|
1031
1318
|
async addMeta(identifier, value, path = '', opts) {
|
|
1032
|
-
const body = { identifier, value, path };
|
|
1319
|
+
const body = { identifier: this.isoPrefixId(identifier), value, path };
|
|
1033
1320
|
if (opts?.mode === 'append')
|
|
1034
1321
|
body.mode = 'append';
|
|
1035
1322
|
const r = await this.request('POST', '/api/v1/meta', body);
|
|
@@ -1037,7 +1324,7 @@ class XCiteDBClient {
|
|
|
1037
1324
|
}
|
|
1038
1325
|
async addMetaByQuery(query, value, path = '', firstMatch = false, opts) {
|
|
1039
1326
|
const body = {
|
|
1040
|
-
query,
|
|
1327
|
+
query: this.isoPrefixQuery(query),
|
|
1041
1328
|
value,
|
|
1042
1329
|
path,
|
|
1043
1330
|
first_match: firstMatch,
|
|
@@ -1054,27 +1341,36 @@ class XCiteDBClient {
|
|
|
1054
1341
|
return this.addMetaByQuery(query, value, path, firstMatch, { mode: 'append' });
|
|
1055
1342
|
}
|
|
1056
1343
|
async queryMeta(identifier, path = '') {
|
|
1057
|
-
return this.request('GET', `/api/v1/meta${buildQuery({ identifier, path })}`);
|
|
1344
|
+
return this.request('GET', `/api/v1/meta${buildQuery({ identifier: this.isoPrefixId(identifier), path })}`);
|
|
1058
1345
|
}
|
|
1059
1346
|
async queryMetaByQuery(query, path = '') {
|
|
1060
|
-
return this.request('POST', '/api/v1/meta/query', { query, path });
|
|
1347
|
+
return this.request('POST', '/api/v1/meta/query', { query: this.isoPrefixQuery(query), path });
|
|
1061
1348
|
}
|
|
1062
1349
|
async clearMeta(query) {
|
|
1063
|
-
const r = await this.request('DELETE', '/api/v1/meta', {
|
|
1350
|
+
const r = await this.request('DELETE', '/api/v1/meta', {
|
|
1351
|
+
query: this.isoPrefixQuery(query),
|
|
1352
|
+
});
|
|
1064
1353
|
return r?.ok !== false;
|
|
1065
1354
|
}
|
|
1066
1355
|
async acquireLock(identifier, expires = 0) {
|
|
1067
|
-
|
|
1356
|
+
const lock = await this.request('POST', '/api/v1/locks', {
|
|
1357
|
+
identifier: this.isoPrefixId(identifier),
|
|
1358
|
+
expires,
|
|
1359
|
+
});
|
|
1360
|
+
return { ...lock, identifier: this.isoUnprefixId(lock.identifier) };
|
|
1068
1361
|
}
|
|
1069
1362
|
async releaseLock(identifier, lockId) {
|
|
1070
1363
|
const r = await this.request('DELETE', '/api/v1/locks', {
|
|
1071
|
-
identifier,
|
|
1364
|
+
identifier: this.isoPrefixId(identifier),
|
|
1072
1365
|
lock_id: lockId,
|
|
1073
1366
|
});
|
|
1074
1367
|
return r?.ok !== false;
|
|
1075
1368
|
}
|
|
1076
1369
|
async findLocks(identifier) {
|
|
1077
|
-
|
|
1370
|
+
const locks = await this.request('GET', `/api/v1/locks${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
1371
|
+
return Array.isArray(locks)
|
|
1372
|
+
? locks.map((L) => ({ ...L, identifier: this.isoUnprefixId(L.identifier) }))
|
|
1373
|
+
: locks;
|
|
1078
1374
|
}
|
|
1079
1375
|
/**
|
|
1080
1376
|
* Run Unquery (`POST /api/v1/unquery`): declarative analytics over documents matching `query`.
|
|
@@ -1102,7 +1398,7 @@ class XCiteDBClient {
|
|
|
1102
1398
|
* ```
|
|
1103
1399
|
*/
|
|
1104
1400
|
async unquery(query, unquery) {
|
|
1105
|
-
return this.request('POST', '/api/v1/unquery', { query, unquery });
|
|
1401
|
+
return this.request('POST', '/api/v1/unquery', { query: this.isoPrefixQuery(query), unquery });
|
|
1106
1402
|
}
|
|
1107
1403
|
async search(q) {
|
|
1108
1404
|
const body = { query: q.query };
|
|
@@ -1114,6 +1410,12 @@ class XCiteDBClient {
|
|
|
1114
1410
|
body.offset = q.offset;
|
|
1115
1411
|
if (q.limit !== undefined)
|
|
1116
1412
|
body.limit = q.limit;
|
|
1413
|
+
if (q.mode)
|
|
1414
|
+
body.mode = q.mode;
|
|
1415
|
+
if (q.min_score !== undefined)
|
|
1416
|
+
body.min_score = q.min_score;
|
|
1417
|
+
if (q.semantic_weight !== undefined)
|
|
1418
|
+
body.semantic_weight = q.semantic_weight;
|
|
1117
1419
|
const data = await this.request('POST', '/api/v1/search', body);
|
|
1118
1420
|
const hits = [];
|
|
1119
1421
|
if (Array.isArray(data.hits)) {
|
|
@@ -1121,7 +1423,7 @@ class XCiteDBClient {
|
|
|
1121
1423
|
if (h && typeof h === 'object') {
|
|
1122
1424
|
const o = h;
|
|
1123
1425
|
const hit = {
|
|
1124
|
-
identifier: String(o.identifier ?? ''),
|
|
1426
|
+
identifier: this.isoUnprefixId(String(o.identifier ?? '')),
|
|
1125
1427
|
path: String(o.path ?? ''),
|
|
1126
1428
|
doc_type: o.doc_type === 'json' ? 'json' : 'xml',
|
|
1127
1429
|
branch: String(o.branch ?? ''),
|
|
@@ -1131,6 +1433,9 @@ class XCiteDBClient {
|
|
|
1131
1433
|
if (typeof o.xcitepath === 'string' && o.xcitepath.length > 0) {
|
|
1132
1434
|
hit.xcitepath = o.xcitepath;
|
|
1133
1435
|
}
|
|
1436
|
+
if (o.source === 'fts' || o.source === 'semantic' || o.source === 'both') {
|
|
1437
|
+
hit.source = o.source;
|
|
1438
|
+
}
|
|
1134
1439
|
hits.push(hit);
|
|
1135
1440
|
}
|
|
1136
1441
|
}
|
|
@@ -1144,24 +1449,170 @@ class XCiteDBClient {
|
|
|
1144
1449
|
async reindex() {
|
|
1145
1450
|
return this.request('POST', '/api/v1/search/reindex', {});
|
|
1146
1451
|
}
|
|
1452
|
+
async reembedVector() {
|
|
1453
|
+
return this.request('POST', '/api/v1/search/reembed-vector', {});
|
|
1454
|
+
}
|
|
1455
|
+
/** Project search / FTS / vector / LLM configuration (`GET /api/v1/project/settings/search`). */
|
|
1456
|
+
async getProjectSearchSettings() {
|
|
1457
|
+
return this.request('GET', '/api/v1/project/settings/search');
|
|
1458
|
+
}
|
|
1459
|
+
/** Update project search settings (`PUT /api/v1/project/settings/search`). Returns the same shape as GET. */
|
|
1460
|
+
async updateProjectSearchSettings(patch) {
|
|
1461
|
+
return this.request('PUT', '/api/v1/project/settings/search', patch);
|
|
1462
|
+
}
|
|
1463
|
+
/** Blocking full DB scan (admin; no calls to embedding API). Prefer {@link postVectorIndexEstimateSession} for UI. */
|
|
1464
|
+
async getVectorIndexEstimate() {
|
|
1465
|
+
return this.request('GET', '/api/v1/project/settings/search/vector-index-estimate', undefined);
|
|
1466
|
+
}
|
|
1467
|
+
/** Start background estimate (202); cancel prior session for this tenant. */
|
|
1468
|
+
async postVectorIndexEstimateSession() {
|
|
1469
|
+
return this.request('POST', '/api/v1/project/settings/search/vector-index-estimate', {});
|
|
1470
|
+
}
|
|
1471
|
+
async getVectorIndexEstimateSession(sessionId) {
|
|
1472
|
+
const q = buildQuery({ session: sessionId });
|
|
1473
|
+
return this.request('GET', `/api/v1/project/settings/search/vector-index-estimate${q}`, undefined);
|
|
1474
|
+
}
|
|
1475
|
+
async deleteVectorIndexEstimateSession(sessionId) {
|
|
1476
|
+
const q = buildQuery({ session: sessionId });
|
|
1477
|
+
return this.request('DELETE', `/api/v1/project/settings/search/vector-index-estimate${q}`, undefined);
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* RAG over indexed documents (`POST /api/v1/rag/query` with JSON body).
|
|
1481
|
+
* Requires LLM completion; embedding required when retrieval uses semantic or hybrid.
|
|
1482
|
+
*/
|
|
1483
|
+
async ragQuery(options) {
|
|
1484
|
+
const body = {
|
|
1485
|
+
question: options.question,
|
|
1486
|
+
stream: false,
|
|
1487
|
+
};
|
|
1488
|
+
if (options.branch !== undefined)
|
|
1489
|
+
body.branch = options.branch;
|
|
1490
|
+
if (options.max_context_docs !== undefined)
|
|
1491
|
+
body.max_context_docs = options.max_context_docs;
|
|
1492
|
+
if (options.doc_types?.length)
|
|
1493
|
+
body.doc_types = options.doc_types;
|
|
1494
|
+
if (options.search_mode !== undefined)
|
|
1495
|
+
body.search_mode = options.search_mode;
|
|
1496
|
+
if (options.min_score !== undefined)
|
|
1497
|
+
body.min_score = options.min_score;
|
|
1498
|
+
if (options.semantic_weight !== undefined)
|
|
1499
|
+
body.semantic_weight = options.semantic_weight;
|
|
1500
|
+
const data = await this.request('POST', '/api/v1/rag/query', body);
|
|
1501
|
+
if (!data || typeof data !== 'object') {
|
|
1502
|
+
throw new types_1.XCiteDBError('Invalid RAG response', 500, data);
|
|
1503
|
+
}
|
|
1504
|
+
const answer = typeof data.answer === 'string' ? data.answer : '';
|
|
1505
|
+
const sources = 'sources' in data ? data.sources : [];
|
|
1506
|
+
return { answer, sources };
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Streaming RAG (`POST /api/v1/rag/query` with `stream: true`). Parses SSE `data: {...}` lines.
|
|
1510
|
+
* The final event has `done: true` and may include `sources`.
|
|
1511
|
+
*/
|
|
1512
|
+
async ragQueryStream(options, onEvent) {
|
|
1513
|
+
const path = '/api/v1/rag/query';
|
|
1514
|
+
const payload = JSON.stringify({
|
|
1515
|
+
question: options.question,
|
|
1516
|
+
stream: true,
|
|
1517
|
+
...(options.branch !== undefined ? { branch: options.branch } : {}),
|
|
1518
|
+
...(options.max_context_docs !== undefined ? { max_context_docs: options.max_context_docs } : {}),
|
|
1519
|
+
...(options.doc_types?.length ? { doc_types: options.doc_types } : {}),
|
|
1520
|
+
...(options.search_mode !== undefined ? { search_mode: options.search_mode } : {}),
|
|
1521
|
+
...(options.min_score !== undefined ? { min_score: options.min_score } : {}),
|
|
1522
|
+
...(options.semantic_weight !== undefined ? { semantic_weight: options.semantic_weight } : {}),
|
|
1523
|
+
});
|
|
1524
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1525
|
+
const url = joinUrl(this.baseUrl, path);
|
|
1526
|
+
const headers = {
|
|
1527
|
+
...this.requestHeaders(),
|
|
1528
|
+
'Content-Type': 'application/json',
|
|
1529
|
+
};
|
|
1530
|
+
const res = await fetch(url, { method: 'POST', headers, body: payload });
|
|
1531
|
+
if (res.status === 401 &&
|
|
1532
|
+
attempt === 0 &&
|
|
1533
|
+
(await this.tryRefreshSessionAfter401())) {
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
if (!res.ok) {
|
|
1537
|
+
const text = await res.text();
|
|
1538
|
+
let data;
|
|
1539
|
+
try {
|
|
1540
|
+
data = text ? JSON.parse(text) : null;
|
|
1541
|
+
}
|
|
1542
|
+
catch {
|
|
1543
|
+
data = text;
|
|
1544
|
+
}
|
|
1545
|
+
const msg = typeof data === 'object' && data !== null && 'message' in data
|
|
1546
|
+
? String(data.message)
|
|
1547
|
+
: res.statusText;
|
|
1548
|
+
this.notifySessionInvalidIfNeeded(path, res.status);
|
|
1549
|
+
throw new types_1.XCiteDBError(msg || `HTTP ${res.status}`, res.status, data);
|
|
1550
|
+
}
|
|
1551
|
+
const streamBody = res.body;
|
|
1552
|
+
if (!streamBody) {
|
|
1553
|
+
const text = await res.text();
|
|
1554
|
+
throw new types_1.XCiteDBError('RAG stream: empty response body', res.status, text);
|
|
1555
|
+
}
|
|
1556
|
+
const reader = streamBody.getReader();
|
|
1557
|
+
const decoder = new TextDecoder();
|
|
1558
|
+
let buf = '';
|
|
1559
|
+
try {
|
|
1560
|
+
for (;;) {
|
|
1561
|
+
const { done, value } = await reader.read();
|
|
1562
|
+
if (value) {
|
|
1563
|
+
buf += decoder.decode(value, { stream: !done });
|
|
1564
|
+
}
|
|
1565
|
+
let sep;
|
|
1566
|
+
while ((sep = buf.indexOf('\n\n')) >= 0) {
|
|
1567
|
+
const block = buf.slice(0, sep);
|
|
1568
|
+
buf = buf.slice(sep + 2);
|
|
1569
|
+
const lines = block.split('\n');
|
|
1570
|
+
const dataLine = lines.find((l) => l.startsWith('data:'));
|
|
1571
|
+
if (!dataLine)
|
|
1572
|
+
continue;
|
|
1573
|
+
const jsonStr = dataLine.replace(/^data:\s*/, '').trim();
|
|
1574
|
+
if (!jsonStr)
|
|
1575
|
+
continue;
|
|
1576
|
+
let ev;
|
|
1577
|
+
try {
|
|
1578
|
+
ev = JSON.parse(jsonStr);
|
|
1579
|
+
}
|
|
1580
|
+
catch {
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
onEvent(ev);
|
|
1584
|
+
}
|
|
1585
|
+
if (done)
|
|
1586
|
+
break;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
finally {
|
|
1590
|
+
reader.releaseLock();
|
|
1591
|
+
}
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
throw new types_1.XCiteDBError('RAG stream failed after retry', 401, null);
|
|
1595
|
+
}
|
|
1147
1596
|
async writeJsonDocument(identifier, data) {
|
|
1148
|
-
await this.request('POST', '/api/v1/json-documents', { identifier, data });
|
|
1597
|
+
await this.request('POST', '/api/v1/json-documents', { identifier: this.isoPrefixId(identifier), data });
|
|
1149
1598
|
}
|
|
1150
1599
|
async readJsonDocument(identifier) {
|
|
1151
|
-
return this.request('GET', `/api/v1/json-documents${buildQuery({ identifier })}`);
|
|
1600
|
+
return this.request('GET', `/api/v1/json-documents${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
1152
1601
|
}
|
|
1153
1602
|
async deleteJsonDocument(identifier) {
|
|
1154
|
-
await this.request('DELETE', `/api/v1/json-documents${buildQuery({ identifier })}`);
|
|
1603
|
+
await this.request('DELETE', `/api/v1/json-documents${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
1155
1604
|
}
|
|
1156
1605
|
async listJsonDocuments(match, limit, offset) {
|
|
1157
|
-
const
|
|
1606
|
+
const m = match !== undefined && match !== '' ? this.isoPrefixId(match) : match;
|
|
1607
|
+
const data = await this.request('GET', `/api/v1/json-documents/list${buildQuery({ match: m, limit, offset })}`);
|
|
1608
|
+
const un = (ids) => ids.map((id) => this.isoUnprefixId(id));
|
|
1158
1609
|
if (Array.isArray(data)) {
|
|
1159
|
-
const identifiers = data;
|
|
1610
|
+
const identifiers = un(data);
|
|
1160
1611
|
return { identifiers, total: identifiers.length, offset: 0, limit: identifiers.length };
|
|
1161
1612
|
}
|
|
1162
1613
|
if (data !== null && typeof data === 'object') {
|
|
1163
1614
|
const o = data;
|
|
1164
|
-
const ids = Array.isArray(o.identifiers) ? o.identifiers : [];
|
|
1615
|
+
const ids = Array.isArray(o.identifiers) ? un(o.identifiers) : [];
|
|
1165
1616
|
return {
|
|
1166
1617
|
identifiers: ids,
|
|
1167
1618
|
total: typeof o.total === 'number' ? o.total : ids.length,
|