@xcitedbs/client 0.2.23 → 0.2.25
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/assetUri.d.ts +14 -0
- package/dist/assetUri.js +119 -0
- package/dist/assetUri.test.d.ts +1 -0
- package/dist/assetUri.test.js +20 -0
- package/dist/client.d.ts +65 -11
- package/dist/client.js +332 -13
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/types.d.ts +129 -0
- package/llms-full.txt +55 -7
- package/llms.txt +51 -4
- package/package.json +1 -1
- package/unquery-ai-guide.md +2 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** URI prefix for asset identifiers embedded in documents (`xcitedb:asset:/path/...`). */
|
|
2
|
+
export declare const ASSET_URI_PREFIX: "xcitedb:asset:";
|
|
3
|
+
/**
|
|
4
|
+
* Parses `xcitedb:asset:<identifier>` where identifier must start with `/`.
|
|
5
|
+
* Percent-decodes the path portion then normalizes slashes.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseAssetUri(s: string): string | null;
|
|
8
|
+
/** Build `xcitedb:asset:<identifier>`; identifier must begin with `/` after normalization. */
|
|
9
|
+
export declare function formatAssetUri(identifier: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Scan arbitrary text for `xcitedb:asset:` paths (GC / reference discovery).
|
|
12
|
+
* Paths are normalized the same way as {@link parseAssetUri} would accept.
|
|
13
|
+
*/
|
|
14
|
+
export declare function collectIdentifiersFromText(text: string, out: Set<string>): void;
|
package/dist/assetUri.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ASSET_URI_PREFIX = void 0;
|
|
4
|
+
exports.parseAssetUri = parseAssetUri;
|
|
5
|
+
exports.formatAssetUri = formatAssetUri;
|
|
6
|
+
exports.collectIdentifiersFromText = collectIdentifiersFromText;
|
|
7
|
+
/** URI prefix for asset identifiers embedded in documents (`xcitedb:asset:/path/...`). */
|
|
8
|
+
exports.ASSET_URI_PREFIX = 'xcitedb:asset:';
|
|
9
|
+
function isPchar(c) {
|
|
10
|
+
if ((c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || (c >= 0x30 && c <= 0x39)) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
switch (c) {
|
|
14
|
+
case 0x2d: // -
|
|
15
|
+
case 0x2e: // .
|
|
16
|
+
case 0x5f: // _
|
|
17
|
+
case 0x7e: // ~
|
|
18
|
+
case 0x25: // %
|
|
19
|
+
case 0x3a: // :
|
|
20
|
+
case 0x40: // @
|
|
21
|
+
case 0x21: // !
|
|
22
|
+
case 0x24: // $
|
|
23
|
+
case 0x26: // &
|
|
24
|
+
case 0x27: // '
|
|
25
|
+
case 0x28: // (
|
|
26
|
+
case 0x29: // )
|
|
27
|
+
case 0x2a: // *
|
|
28
|
+
case 0x2b: // +
|
|
29
|
+
case 0x2c: // ,
|
|
30
|
+
case 0x3b: // ;
|
|
31
|
+
case 0x3d: // =
|
|
32
|
+
return true;
|
|
33
|
+
default:
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Normalize path: collapse slashes, ensure leading `/`. */
|
|
38
|
+
function normalizeIdentifierPath(p) {
|
|
39
|
+
let s = p.trim().replace(/\/+/g, '/');
|
|
40
|
+
if (!s.startsWith('/')) {
|
|
41
|
+
s = `/${s}`;
|
|
42
|
+
}
|
|
43
|
+
return s;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parses `xcitedb:asset:<identifier>` where identifier must start with `/`.
|
|
47
|
+
* Percent-decodes the path portion then normalizes slashes.
|
|
48
|
+
*/
|
|
49
|
+
function parseAssetUri(s) {
|
|
50
|
+
const t = s.trim();
|
|
51
|
+
if (!t.startsWith(exports.ASSET_URI_PREFIX)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
let rest = t.slice(exports.ASSET_URI_PREFIX.length);
|
|
55
|
+
if (!rest.startsWith('/')) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
rest = decodeURIComponent(rest);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const id = normalizeIdentifierPath(rest);
|
|
65
|
+
if (id === '/' || id.length < 2) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return id;
|
|
69
|
+
}
|
|
70
|
+
/** Build `xcitedb:asset:<identifier>`; identifier must begin with `/` after normalization. */
|
|
71
|
+
function formatAssetUri(identifier) {
|
|
72
|
+
const id = normalizeIdentifierPath(identifier);
|
|
73
|
+
if (id === '/' || !id.startsWith('/')) {
|
|
74
|
+
throw new Error('formatAssetUri: identifier must be a non-root absolute path');
|
|
75
|
+
}
|
|
76
|
+
return `${exports.ASSET_URI_PREFIX}${id}`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Scan arbitrary text for `xcitedb:asset:` paths (GC / reference discovery).
|
|
80
|
+
* Paths are normalized the same way as {@link parseAssetUri} would accept.
|
|
81
|
+
*/
|
|
82
|
+
function collectIdentifiersFromText(text, out) {
|
|
83
|
+
const p = exports.ASSET_URI_PREFIX;
|
|
84
|
+
let pos = 0;
|
|
85
|
+
while (pos < text.length) {
|
|
86
|
+
const f = text.indexOf(p, pos);
|
|
87
|
+
if (f < 0) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
let i = f + p.length;
|
|
91
|
+
if (i >= text.length || text.charCodeAt(i) !== 0x2f) {
|
|
92
|
+
pos = f + 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
let j = i;
|
|
96
|
+
while (j < text.length) {
|
|
97
|
+
const c = text.charCodeAt(j);
|
|
98
|
+
if (c === 0x2f || isPchar(c)) {
|
|
99
|
+
j += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
if (j > i) {
|
|
105
|
+
const raw = text.slice(i, j);
|
|
106
|
+
try {
|
|
107
|
+
const decoded = decodeURIComponent(raw);
|
|
108
|
+
const id = normalizeIdentifierPath(decoded);
|
|
109
|
+
if (id.length > 1) {
|
|
110
|
+
out.add(id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// skip malformed percent-encoding
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
pos = j;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const assetUri_1 = require("./assetUri");
|
|
9
|
+
(0, node_test_1.default)('parseAssetUri accepts xcitedb:asset path', () => {
|
|
10
|
+
strict_1.default.equal((0, assetUri_1.parseAssetUri)(`${assetUri_1.ASSET_URI_PREFIX}/users/u1/assets/a.png`), '/users/u1/assets/a.png');
|
|
11
|
+
});
|
|
12
|
+
(0, node_test_1.default)('formatAssetUri round-trip', () => {
|
|
13
|
+
const id = '/assets/photos/x';
|
|
14
|
+
strict_1.default.equal((0, assetUri_1.parseAssetUri)((0, assetUri_1.formatAssetUri)(id)), id);
|
|
15
|
+
});
|
|
16
|
+
(0, node_test_1.default)('collectIdentifiersFromText', () => {
|
|
17
|
+
const s = new Set();
|
|
18
|
+
(0, assetUri_1.collectIdentifiersFromText)(`see ${assetUri_1.ASSET_URI_PREFIX}/a/b and done`, s);
|
|
19
|
+
strict_1.default.ok(s.has('/a/b'));
|
|
20
|
+
});
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, BookmarkRecord, CheckpointRecord, CommitRecord, CompareRef, CompareResult, DatabaseContext, DiffRef, DiffResult, DocumentBatchResponse, DocumentExportFormat, ExportDocumentResult, Flags, JsonDocumentBatchItem, ImportDocumentOptions, ImportDocumentResult,
|
|
1
|
+
import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, BookmarkRecord, CheckpointRecord, CommitRecord, CompareRef, CompareResult, DatabaseContext, DiffRef, DiffResult, DocumentBatchResponse, DocumentExportFormat, ExportDocumentResult, Flags, JsonDocumentBatchItem, ImportDocumentOptions, ImportDocumentResult, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LogEntry, MergeResult, PublishResult, RebaseUserWorkspaceResult, WorkspaceInfo, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetMagicLinkListResponse, AssetMagicLinkResult, AssetShareListResponse, AssetShareRequest, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, VectorIndexEstimate, RagQueryOptions, RagQueryResult, RagStreamEvent, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, TestSessionBootstrapSummary, TestSessionInfo, XCiteQuery, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationShareResult } from './types';
|
|
2
2
|
import { WebSocketSubscription } from './websocket';
|
|
3
3
|
export declare class XCiteDBClient {
|
|
4
4
|
private baseUrl;
|
|
@@ -327,6 +327,21 @@ export declare class XCiteDBClient {
|
|
|
327
327
|
* When {@link XCiteDBClient.enableUserIsolation} / `userIsolation` is active, `identifier` is prefixed like other document APIs.
|
|
328
328
|
*/
|
|
329
329
|
createUserIsolationShare(params: UserIsolationCreateShareParams): Promise<UserIsolationShareResult>;
|
|
330
|
+
/** Share an asset path via prefix alias (`POST /api/v1/security/user-isolation/asset-shares`). */
|
|
331
|
+
createAssetShare(params: AssetShareRequest): Promise<UserIsolationShareResult>;
|
|
332
|
+
/** Remove an asset share (`DELETE /api/v1/security/user-isolation/asset-shares`). */
|
|
333
|
+
deleteAssetShare(params: AssetUnshareRequest): Promise<void>;
|
|
334
|
+
/** List incoming/public asset share aliases (`GET /api/v1/security/user-isolation/asset-shares`). */
|
|
335
|
+
listAssetShares(opts?: {
|
|
336
|
+
direction?: 'incoming' | 'outgoing' | 'public';
|
|
337
|
+
scope?: string;
|
|
338
|
+
}): Promise<AssetShareListResponse>;
|
|
339
|
+
/** Issue a time-limited magic link for one asset identifier (`POST /api/v1/security/asset-magic-links`). */
|
|
340
|
+
createAssetMagicLink(body: CreateAssetMagicLinkRequest): Promise<AssetMagicLinkResult>;
|
|
341
|
+
/** List issued magic links (no secrets) (`GET /api/v1/security/asset-magic-links`). */
|
|
342
|
+
listAssetMagicLinks(): Promise<AssetMagicLinkListResponse>;
|
|
343
|
+
/** Revoke a magic link (`DELETE /api/v1/security/asset-magic-links/{token_id}`). */
|
|
344
|
+
revokeAssetMagicLink(tokenId: string): Promise<void>;
|
|
330
345
|
createWorkspace(name: string, fromBranch?: string, fromDate?: string): Promise<void>;
|
|
331
346
|
/** @deprecated Use {@link createWorkspace}. */
|
|
332
347
|
createBranch(name: string, fromBranch?: string, fromDate?: string): Promise<void>;
|
|
@@ -494,12 +509,18 @@ export declare class XCiteDBClient {
|
|
|
494
509
|
* Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
|
|
495
510
|
* For storing JSON data by key, use `writeJsonDocument`.
|
|
496
511
|
*
|
|
512
|
+
* **Pruned writes:** you may include **`db:stub="true"`** (or legacy **`<db:N…/>`**) under a parent to preserve shredded children
|
|
513
|
+
* without inlining their bodies; stubs must be empty and reference an existing row (see product `llms.txt` / OpenAPI notes).
|
|
514
|
+
* Malformed hierarchical identifiers return **400** `invalid_identifier`; invalid stub/placeholder XML returns **400** `xml_write_rejected`.
|
|
515
|
+
*
|
|
497
516
|
* On cooperative-lock conflict the server returns **423** with a {@link LockConflictBody} shape in {@link XCiteDBError.body}.
|
|
498
517
|
*/
|
|
499
518
|
writeXmlDocument(xml: string, options?: WriteDocumentOptions): Promise<void>;
|
|
500
519
|
/**
|
|
501
520
|
* Best-effort batch XML writes (`POST /api/v1/documents/batch`). Each item is independent; check `results[].ok`.
|
|
502
521
|
* Rows with `error === 'lock_conflict'` include `current_lock` (code **423** per row).
|
|
522
|
+
* **`409`** precheck rows may include **`detail`** (`identifier` for duplicates; `parent` + `inlined_child` for parent inline).
|
|
523
|
+
* Identifiers on **`db:stub="true"`** only are batch references and do not count as inlined bodies for precheck.
|
|
503
524
|
*/
|
|
504
525
|
writeXmlDocumentsBatch(items: XmlDocumentBatchItem[]): Promise<DocumentBatchResponse>;
|
|
505
526
|
/**
|
|
@@ -519,20 +540,21 @@ export declare class XCiteDBClient {
|
|
|
519
540
|
writeDocumentJson(xml: string, options?: WriteDocumentOptions): Promise<void>;
|
|
520
541
|
queryByIdentifier(identifier: string, flags?: Flags, filter?: string, pathFilter?: string): Promise<string[]>;
|
|
521
542
|
/**
|
|
522
|
-
* Shallow read
|
|
523
|
-
*
|
|
524
|
-
*
|
|
543
|
+
* Shallow read (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`):
|
|
544
|
+
* root element with inline leaves plus **identifier-bearing stub elements** (`db:stub="true"`) for shredded
|
|
545
|
+
* child slots instead of opaque `db:N*` placeholders. Safe to round-trip with {@link writeXmlDocument}.
|
|
546
|
+
* For sidebar / AST navigation, pair with {@link listIdentifierChildren} (e.g. `Promise.all` of shallow + children).
|
|
525
547
|
*/
|
|
526
548
|
queryByIdentifierShallow(identifier: string, filter?: string, pathFilter?: string): Promise<string[]>;
|
|
527
|
-
/** Alias of {@link listIdentifierChildren} — immediate child segments under `parentPath` (identifier hierarchy). */
|
|
528
|
-
listChildIdentifiers(parentPath?: string): Promise<ListIdentifierChildrenResult>;
|
|
529
549
|
/**
|
|
530
|
-
*
|
|
550
|
+
* Load a document with all shredded children inlined (`FirstMatch,IncludeChildren` on `GET /by-id`).
|
|
551
|
+
* The returned array has **length 0 or 1**; when present, index `0` is the full XML string.
|
|
552
|
+
* Use this for editor round-trips. For navigation without loading the full subtree, use
|
|
553
|
+
* {@link queryByIdentifierShallow} plus {@link listIdentifierChildren}.
|
|
531
554
|
*/
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}>;
|
|
555
|
+
queryByIdentifierFull(identifier: string, filter?: string, pathFilter?: string): Promise<string[]>;
|
|
556
|
+
/** Alias of {@link listIdentifierChildren} — immediate child segments under `parentPath` (identifier hierarchy). */
|
|
557
|
+
listChildIdentifiers(parentPath?: string): Promise<ListIdentifierChildrenResult>;
|
|
536
558
|
queryDocuments(query: XCiteQuery, flags?: Flags, filter?: string, pathFilter?: string): Promise<string[]>;
|
|
537
559
|
deleteDocument(identifier: string): Promise<void>;
|
|
538
560
|
addIdentifier(identifier: string): Promise<boolean>;
|
|
@@ -636,6 +658,38 @@ export declare class XCiteDBClient {
|
|
|
636
658
|
}): Promise<ProjectDocConfResponse>;
|
|
637
659
|
/** Remove project override; server uses platform default (`DELETE /api/v1/project/settings/doc-conf`). */
|
|
638
660
|
deleteProjectDocConf(): Promise<ProjectDocConfResponse>;
|
|
661
|
+
/** Admin: asset storage v2 (`GET /api/v1/project/settings/asset-storage`). */
|
|
662
|
+
getProjectAssetStorage(): Promise<ProjectAssetStorageConfig>;
|
|
663
|
+
/** Admin: save asset storage v2 (`PUT /api/v1/project/settings/asset-storage`). */
|
|
664
|
+
updateProjectAssetStorage(body: ProjectAssetStorageConfig): Promise<ProjectAssetStorageConfig>;
|
|
665
|
+
private resolveAssetIdentifier;
|
|
666
|
+
/** URL path under `/api/v1/assets/…` with per-segment encoding. */
|
|
667
|
+
private assetRequestPath;
|
|
668
|
+
private assetQuerySuffix;
|
|
669
|
+
/**
|
|
670
|
+
* Upload raw bytes (`POST /api/v1/assets`). Returns canonical `identifier` and `xcitedb:asset:…` `uri`.
|
|
671
|
+
*/
|
|
672
|
+
uploadAsset(bytes: Blob | ArrayBuffer | Uint8Array, opts: UploadAssetOptions): Promise<AssetUploadResult>;
|
|
673
|
+
/**
|
|
674
|
+
* Download asset bytes (`GET /api/v1/assets/…`). Follows `302` to presigned S3 URLs.
|
|
675
|
+
* Pass `ml` for magic-link access (same token as `Authorization: MagicLink …` when not using cookies).
|
|
676
|
+
*/
|
|
677
|
+
getAsset(uriOrIdentifier: string, opts?: {
|
|
678
|
+
ifNoneMatch?: string;
|
|
679
|
+
ml?: string;
|
|
680
|
+
}): Promise<Blob>;
|
|
681
|
+
/** Delete an asset (`DELETE /api/v1/assets/…`). */
|
|
682
|
+
deleteAsset(uriOrIdentifier: string, opts?: {
|
|
683
|
+
ml?: string;
|
|
684
|
+
}): Promise<void>;
|
|
685
|
+
/** `HEAD /api/v1/assets/…` — metadata only. */
|
|
686
|
+
headAsset(uriOrIdentifier: string, opts?: {
|
|
687
|
+
ml?: string;
|
|
688
|
+
}): Promise<AssetHeadResult>;
|
|
689
|
+
/** Admin: GC dry-run — manifest vs live meta references (`POST /api/v1/admin/assets/gc/dry-run`). */
|
|
690
|
+
adminAssetsGcDryRun(opts?: {
|
|
691
|
+
ownerUserId?: string;
|
|
692
|
+
}): Promise<AssetGcDryRunResult>;
|
|
639
693
|
/** Embedded platform default `document.conf` text (`GET /api/v1/platform/default-doc-conf`). */
|
|
640
694
|
getPlatformDefaultDocConf(): Promise<PlatformDefaultDocConfResponse>;
|
|
641
695
|
/** Blocking full DB scan (admin; no calls to embedding API). Prefer {@link postVectorIndexEstimateSession} for UI. */
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.XCiteDBClient = void 0;
|
|
4
4
|
const types_1 = require("./types");
|
|
5
5
|
const websocket_1 = require("./websocket");
|
|
6
|
+
const assetUri_1 = require("./assetUri");
|
|
6
7
|
function joinUrl(base, path) {
|
|
7
8
|
const b = base.replace(/\/+$/, '');
|
|
8
9
|
const p = path.startsWith('/') ? path : `/${path}`;
|
|
@@ -1214,6 +1215,64 @@ class XCiteDBClient {
|
|
|
1214
1215
|
mode: params.mode,
|
|
1215
1216
|
});
|
|
1216
1217
|
}
|
|
1218
|
+
/** Share an asset path via prefix alias (`POST /api/v1/security/user-isolation/asset-shares`). */
|
|
1219
|
+
async createAssetShare(params) {
|
|
1220
|
+
const body = {
|
|
1221
|
+
target_user_id: params.target_user_id,
|
|
1222
|
+
mode: params.mode,
|
|
1223
|
+
};
|
|
1224
|
+
if (params.source_uri !== undefined && params.source_uri !== '') {
|
|
1225
|
+
body.source_uri = params.source_uri;
|
|
1226
|
+
}
|
|
1227
|
+
else if (params.identifier !== undefined && params.identifier !== '') {
|
|
1228
|
+
body.identifier = this.isoPrefixId(params.identifier);
|
|
1229
|
+
}
|
|
1230
|
+
else {
|
|
1231
|
+
throw new types_1.XCiteDBError('createAssetShare requires identifier or source_uri', 400, null);
|
|
1232
|
+
}
|
|
1233
|
+
return this.request('POST', '/api/v1/security/user-isolation/asset-shares', body);
|
|
1234
|
+
}
|
|
1235
|
+
/** Remove an asset share (`DELETE /api/v1/security/user-isolation/asset-shares`). */
|
|
1236
|
+
async deleteAssetShare(params) {
|
|
1237
|
+
const body = {};
|
|
1238
|
+
if (params.source_uri !== undefined && params.source_uri !== '') {
|
|
1239
|
+
body.source_uri = params.source_uri;
|
|
1240
|
+
}
|
|
1241
|
+
else if (params.identifier !== undefined && params.identifier !== '') {
|
|
1242
|
+
body.identifier = this.isoPrefixId(params.identifier);
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
throw new types_1.XCiteDBError('deleteAssetShare requires identifier or source_uri', 400, null);
|
|
1246
|
+
}
|
|
1247
|
+
if (params.target_user_id !== undefined) {
|
|
1248
|
+
body.target_user_id = params.target_user_id;
|
|
1249
|
+
}
|
|
1250
|
+
if (params.sharer_user_id !== undefined) {
|
|
1251
|
+
body.sharer_user_id = params.sharer_user_id;
|
|
1252
|
+
}
|
|
1253
|
+
await this.request('DELETE', '/api/v1/security/user-isolation/asset-shares', body);
|
|
1254
|
+
}
|
|
1255
|
+
/** List incoming/public asset share aliases (`GET /api/v1/security/user-isolation/asset-shares`). */
|
|
1256
|
+
async listAssetShares(opts) {
|
|
1257
|
+
const q = buildQuery({
|
|
1258
|
+
direction: opts?.direction ?? 'incoming',
|
|
1259
|
+
...(opts?.scope ? { scope: opts.scope } : {}),
|
|
1260
|
+
});
|
|
1261
|
+
return this.request('GET', `/api/v1/security/user-isolation/asset-shares${q}`);
|
|
1262
|
+
}
|
|
1263
|
+
/** Issue a time-limited magic link for one asset identifier (`POST /api/v1/security/asset-magic-links`). */
|
|
1264
|
+
async createAssetMagicLink(body) {
|
|
1265
|
+
return this.request('POST', '/api/v1/security/asset-magic-links', body);
|
|
1266
|
+
}
|
|
1267
|
+
/** List issued magic links (no secrets) (`GET /api/v1/security/asset-magic-links`). */
|
|
1268
|
+
async listAssetMagicLinks() {
|
|
1269
|
+
return this.request('GET', '/api/v1/security/asset-magic-links');
|
|
1270
|
+
}
|
|
1271
|
+
/** Revoke a magic link (`DELETE /api/v1/security/asset-magic-links/{token_id}`). */
|
|
1272
|
+
async revokeAssetMagicLink(tokenId) {
|
|
1273
|
+
const id = encodeURIComponent(tokenId.trim());
|
|
1274
|
+
await this.request('DELETE', `/api/v1/security/asset-magic-links/${id}`);
|
|
1275
|
+
}
|
|
1217
1276
|
async createWorkspace(name, fromBranch, fromDate) {
|
|
1218
1277
|
const body = { name };
|
|
1219
1278
|
if (fromBranch)
|
|
@@ -1479,6 +1538,10 @@ class XCiteDBClient {
|
|
|
1479
1538
|
* Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
|
|
1480
1539
|
* For storing JSON data by key, use `writeJsonDocument`.
|
|
1481
1540
|
*
|
|
1541
|
+
* **Pruned writes:** you may include **`db:stub="true"`** (or legacy **`<db:N…/>`**) under a parent to preserve shredded children
|
|
1542
|
+
* without inlining their bodies; stubs must be empty and reference an existing row (see product `llms.txt` / OpenAPI notes).
|
|
1543
|
+
* Malformed hierarchical identifiers return **400** `invalid_identifier`; invalid stub/placeholder XML returns **400** `xml_write_rejected`.
|
|
1544
|
+
*
|
|
1482
1545
|
* On cooperative-lock conflict the server returns **423** with a {@link LockConflictBody} shape in {@link XCiteDBError.body}.
|
|
1483
1546
|
*/
|
|
1484
1547
|
async writeXmlDocument(xml, options) {
|
|
@@ -1491,6 +1554,8 @@ class XCiteDBClient {
|
|
|
1491
1554
|
/**
|
|
1492
1555
|
* Best-effort batch XML writes (`POST /api/v1/documents/batch`). Each item is independent; check `results[].ok`.
|
|
1493
1556
|
* Rows with `error === 'lock_conflict'` include `current_lock` (code **423** per row).
|
|
1557
|
+
* **`409`** precheck rows may include **`detail`** (`identifier` for duplicates; `parent` + `inlined_child` for parent inline).
|
|
1558
|
+
* Identifiers on **`db:stub="true"`** only are batch references and do not count as inlined bodies for precheck.
|
|
1494
1559
|
*/
|
|
1495
1560
|
async writeXmlDocumentsBatch(items) {
|
|
1496
1561
|
const body = {
|
|
@@ -1575,27 +1640,27 @@ class XCiteDBClient {
|
|
|
1575
1640
|
return Array.isArray(rows) ? rows.map((x) => this.isoUnprefixId(String(x))) : rows;
|
|
1576
1641
|
}
|
|
1577
1642
|
/**
|
|
1578
|
-
* Shallow read
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1643
|
+
* Shallow read (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`):
|
|
1644
|
+
* root element with inline leaves plus **identifier-bearing stub elements** (`db:stub="true"`) for shredded
|
|
1645
|
+
* child slots instead of opaque `db:N*` placeholders. Safe to round-trip with {@link writeXmlDocument}.
|
|
1646
|
+
* For sidebar / AST navigation, pair with {@link listIdentifierChildren} (e.g. `Promise.all` of shallow + children).
|
|
1581
1647
|
*/
|
|
1582
1648
|
async queryByIdentifierShallow(identifier, filter, pathFilter) {
|
|
1583
1649
|
return this.queryByIdentifier(identifier, 'NoChildren,KeepIndexNodes,FirstMatch', filter, pathFilter);
|
|
1584
1650
|
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Load a document with all shredded children inlined (`FirstMatch,IncludeChildren` on `GET /by-id`).
|
|
1653
|
+
* The returned array has **length 0 or 1**; when present, index `0` is the full XML string.
|
|
1654
|
+
* Use this for editor round-trips. For navigation without loading the full subtree, use
|
|
1655
|
+
* {@link queryByIdentifierShallow} plus {@link listIdentifierChildren}.
|
|
1656
|
+
*/
|
|
1657
|
+
async queryByIdentifierFull(identifier, filter, pathFilter) {
|
|
1658
|
+
return this.queryByIdentifier(identifier, 'FirstMatch,IncludeChildren', filter, pathFilter);
|
|
1659
|
+
}
|
|
1585
1660
|
/** Alias of {@link listIdentifierChildren} — immediate child segments under `parentPath` (identifier hierarchy). */
|
|
1586
1661
|
async listChildIdentifiers(parentPath) {
|
|
1587
1662
|
return this.listIdentifierChildren(parentPath);
|
|
1588
1663
|
}
|
|
1589
|
-
/**
|
|
1590
|
-
* Parallel shallow node + one level of identifier children (`GET /by-id` + `GET /identifier-children`).
|
|
1591
|
-
*/
|
|
1592
|
-
async queryByIdentifierWithChildren(identifier, filter, pathFilter) {
|
|
1593
|
-
const [node, listed] = await Promise.all([
|
|
1594
|
-
this.queryByIdentifierShallow(identifier, filter, pathFilter),
|
|
1595
|
-
this.listIdentifierChildren(identifier),
|
|
1596
|
-
]);
|
|
1597
|
-
return { node, children: listed.children };
|
|
1598
|
-
}
|
|
1599
1664
|
async queryDocuments(query, flags, filter, pathFilter) {
|
|
1600
1665
|
const pq = this.isoPrefixQuery(query);
|
|
1601
1666
|
const params = {
|
|
@@ -1953,6 +2018,260 @@ class XCiteDBClient {
|
|
|
1953
2018
|
async deleteProjectDocConf() {
|
|
1954
2019
|
return this.request('DELETE', '/api/v1/project/settings/doc-conf');
|
|
1955
2020
|
}
|
|
2021
|
+
/** Admin: asset storage v2 (`GET /api/v1/project/settings/asset-storage`). */
|
|
2022
|
+
async getProjectAssetStorage() {
|
|
2023
|
+
return this.request('GET', '/api/v1/project/settings/asset-storage');
|
|
2024
|
+
}
|
|
2025
|
+
/** Admin: save asset storage v2 (`PUT /api/v1/project/settings/asset-storage`). */
|
|
2026
|
+
async updateProjectAssetStorage(body) {
|
|
2027
|
+
return this.request('PUT', '/api/v1/project/settings/asset-storage', body);
|
|
2028
|
+
}
|
|
2029
|
+
resolveAssetIdentifier(uriOrIdentifier) {
|
|
2030
|
+
const trimmed = uriOrIdentifier.trim();
|
|
2031
|
+
const fromUri = (0, assetUri_1.parseAssetUri)(trimmed);
|
|
2032
|
+
if (fromUri) {
|
|
2033
|
+
return this.canonicalId(fromUri);
|
|
2034
|
+
}
|
|
2035
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)) {
|
|
2036
|
+
return `/assets/${trimmed.toLowerCase()}`;
|
|
2037
|
+
}
|
|
2038
|
+
return this.canonicalId(trimmed);
|
|
2039
|
+
}
|
|
2040
|
+
/** URL path under `/api/v1/assets/…` with per-segment encoding. */
|
|
2041
|
+
assetRequestPath(uriOrIdentifier) {
|
|
2042
|
+
const id = this.resolveAssetIdentifier(uriOrIdentifier);
|
|
2043
|
+
const segments = id.split('/').filter((s) => s.length > 0).map((s) => encodeURIComponent(s));
|
|
2044
|
+
return `/api/v1/assets/${segments.join('/')}`;
|
|
2045
|
+
}
|
|
2046
|
+
assetQuerySuffix(opts) {
|
|
2047
|
+
const ml = opts?.ml?.trim();
|
|
2048
|
+
if (!ml) {
|
|
2049
|
+
return '';
|
|
2050
|
+
}
|
|
2051
|
+
return `?ml=${encodeURIComponent(ml)}`;
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Upload raw bytes (`POST /api/v1/assets`). Returns canonical `identifier` and `xcitedb:asset:…` `uri`.
|
|
2055
|
+
*/
|
|
2056
|
+
async uploadAsset(bytes, opts) {
|
|
2057
|
+
let bodyBytes;
|
|
2058
|
+
if (bytes instanceof Blob) {
|
|
2059
|
+
bodyBytes = await bytes.arrayBuffer();
|
|
2060
|
+
}
|
|
2061
|
+
else if (bytes instanceof ArrayBuffer) {
|
|
2062
|
+
bodyBytes = bytes;
|
|
2063
|
+
}
|
|
2064
|
+
else {
|
|
2065
|
+
bodyBytes = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
2066
|
+
}
|
|
2067
|
+
const ct = opts.contentType.trim() || 'application/octet-stream';
|
|
2068
|
+
const qParts = [];
|
|
2069
|
+
if (opts.scope === 'public') {
|
|
2070
|
+
qParts.push('scope=public');
|
|
2071
|
+
}
|
|
2072
|
+
if (opts.identifier !== undefined && opts.identifier !== '') {
|
|
2073
|
+
qParts.push(`identifier=${encodeURIComponent(opts.identifier)}`);
|
|
2074
|
+
}
|
|
2075
|
+
const q = qParts.length ? `?${qParts.join('&')}` : '';
|
|
2076
|
+
const no401Retry = false;
|
|
2077
|
+
const suppressTestSessionHeader = false;
|
|
2078
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2079
|
+
const url = joinUrl(this.baseUrl, `/api/v1/assets${q}`);
|
|
2080
|
+
const outgoingRequestId = newClientRequestId();
|
|
2081
|
+
const headers = {
|
|
2082
|
+
...this.authHeaders(),
|
|
2083
|
+
...this.contextHeaders(),
|
|
2084
|
+
...(suppressTestSessionHeader ? {} : this.testHeaders()),
|
|
2085
|
+
'X-Request-Id': outgoingRequestId,
|
|
2086
|
+
'Content-Type': ct,
|
|
2087
|
+
};
|
|
2088
|
+
const init = { method: 'POST', headers, body: bodyBytes };
|
|
2089
|
+
const sig = requestTimeoutSignal(this.requestTimeoutMs);
|
|
2090
|
+
if (sig)
|
|
2091
|
+
init.signal = sig;
|
|
2092
|
+
let res;
|
|
2093
|
+
try {
|
|
2094
|
+
res = await fetch(url, init);
|
|
2095
|
+
}
|
|
2096
|
+
catch (e) {
|
|
2097
|
+
const m = e instanceof Error ? e.message : 'Network error';
|
|
2098
|
+
throw new types_1.XCiteDBError(m, 0, null, { clientRequestId: outgoingRequestId });
|
|
2099
|
+
}
|
|
2100
|
+
const text = await res.text();
|
|
2101
|
+
let data;
|
|
2102
|
+
try {
|
|
2103
|
+
data = text ? JSON.parse(text) : null;
|
|
2104
|
+
}
|
|
2105
|
+
catch {
|
|
2106
|
+
data = text;
|
|
2107
|
+
}
|
|
2108
|
+
if (res.ok) {
|
|
2109
|
+
if (!data || typeof data !== 'object') {
|
|
2110
|
+
throw new types_1.XCiteDBError('Invalid asset upload response', res.status, data);
|
|
2111
|
+
}
|
|
2112
|
+
const o = data;
|
|
2113
|
+
const identifier = typeof o.identifier === 'string' ? o.identifier : '';
|
|
2114
|
+
const uri = typeof o.uri === 'string' ? o.uri : '';
|
|
2115
|
+
if (!identifier || !uri) {
|
|
2116
|
+
throw new types_1.XCiteDBError('Invalid asset upload response', res.status, data);
|
|
2117
|
+
}
|
|
2118
|
+
const out = { identifier, uri };
|
|
2119
|
+
if (typeof o.target_name === 'string')
|
|
2120
|
+
out.target_name = o.target_name;
|
|
2121
|
+
if (typeof o.content_type === 'string')
|
|
2122
|
+
out.content_type = o.content_type;
|
|
2123
|
+
if (typeof o.size === 'number')
|
|
2124
|
+
out.size = o.size;
|
|
2125
|
+
if (typeof o.sha256 === 'string')
|
|
2126
|
+
out.sha256 = o.sha256;
|
|
2127
|
+
if (typeof o.etag === 'string')
|
|
2128
|
+
out.etag = o.etag;
|
|
2129
|
+
return out;
|
|
2130
|
+
}
|
|
2131
|
+
if (res.status === 401 &&
|
|
2132
|
+
attempt === 0 &&
|
|
2133
|
+
!no401Retry &&
|
|
2134
|
+
(await this.tryRefreshSessionAfter401())) {
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2137
|
+
const msg = typeof data === 'object' && data !== null && 'message' in data
|
|
2138
|
+
? String(data.message)
|
|
2139
|
+
: res.statusText;
|
|
2140
|
+
this.notifySessionInvalidIfNeeded('/api/v1/assets', res.status);
|
|
2141
|
+
throwForFailedHttp(res.status, '/api/v1/assets', data, msg || `HTTP ${res.status}`, res.headers.get('X-Request-Id') ?? undefined, res.headers.get('X-Client-Request-Id') ?? undefined);
|
|
2142
|
+
}
|
|
2143
|
+
this.notifySessionInvalidIfNeeded('/api/v1/assets', 401);
|
|
2144
|
+
throw new types_1.XCiteDBError('Request failed after retry', 401, null);
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Download asset bytes (`GET /api/v1/assets/…`). Follows `302` to presigned S3 URLs.
|
|
2148
|
+
* Pass `ml` for magic-link access (same token as `Authorization: MagicLink …` when not using cookies).
|
|
2149
|
+
*/
|
|
2150
|
+
async getAsset(uriOrIdentifier, opts) {
|
|
2151
|
+
const path = `${this.assetRequestPath(uriOrIdentifier)}${this.assetQuerySuffix({ ml: opts?.ml })}`;
|
|
2152
|
+
const no401Retry = false;
|
|
2153
|
+
const suppressTestSessionHeader = false;
|
|
2154
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2155
|
+
const url = joinUrl(this.baseUrl, path);
|
|
2156
|
+
const outgoingRequestId = newClientRequestId();
|
|
2157
|
+
const headers = {
|
|
2158
|
+
...this.authHeaders(),
|
|
2159
|
+
...this.contextHeaders(),
|
|
2160
|
+
...(suppressTestSessionHeader ? {} : this.testHeaders()),
|
|
2161
|
+
'X-Request-Id': outgoingRequestId,
|
|
2162
|
+
};
|
|
2163
|
+
if (opts?.ifNoneMatch) {
|
|
2164
|
+
headers['If-None-Match'] = opts.ifNoneMatch;
|
|
2165
|
+
}
|
|
2166
|
+
const init = { method: 'GET', headers, redirect: 'follow' };
|
|
2167
|
+
const sig = requestTimeoutSignal(this.requestTimeoutMs);
|
|
2168
|
+
if (sig)
|
|
2169
|
+
init.signal = sig;
|
|
2170
|
+
let res;
|
|
2171
|
+
try {
|
|
2172
|
+
res = await fetch(url, init);
|
|
2173
|
+
}
|
|
2174
|
+
catch (e) {
|
|
2175
|
+
const m = e instanceof Error ? e.message : 'Network error';
|
|
2176
|
+
throw new types_1.XCiteDBError(m, 0, null, { clientRequestId: outgoingRequestId });
|
|
2177
|
+
}
|
|
2178
|
+
if (res.status === 304) {
|
|
2179
|
+
return new Blob();
|
|
2180
|
+
}
|
|
2181
|
+
if (res.ok) {
|
|
2182
|
+
return await res.blob();
|
|
2183
|
+
}
|
|
2184
|
+
if (res.status === 401 &&
|
|
2185
|
+
attempt === 0 &&
|
|
2186
|
+
!no401Retry &&
|
|
2187
|
+
(await this.tryRefreshSessionAfter401())) {
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
const text = await res.text();
|
|
2191
|
+
let data;
|
|
2192
|
+
try {
|
|
2193
|
+
data = text ? JSON.parse(text) : null;
|
|
2194
|
+
}
|
|
2195
|
+
catch {
|
|
2196
|
+
data = text;
|
|
2197
|
+
}
|
|
2198
|
+
const msg = typeof data === 'object' && data !== null && 'message' in data
|
|
2199
|
+
? String(data.message)
|
|
2200
|
+
: res.statusText;
|
|
2201
|
+
this.notifySessionInvalidIfNeeded(path, res.status);
|
|
2202
|
+
throwForFailedHttp(res.status, path, data, msg || `HTTP ${res.status}`, res.headers.get('X-Request-Id') ?? undefined, res.headers.get('X-Client-Request-Id') ?? undefined);
|
|
2203
|
+
}
|
|
2204
|
+
this.notifySessionInvalidIfNeeded(path, 401);
|
|
2205
|
+
throw new types_1.XCiteDBError('Request failed after retry', 401, null);
|
|
2206
|
+
}
|
|
2207
|
+
/** Delete an asset (`DELETE /api/v1/assets/…`). */
|
|
2208
|
+
async deleteAsset(uriOrIdentifier, opts) {
|
|
2209
|
+
const path = `${this.assetRequestPath(uriOrIdentifier)}${this.assetQuerySuffix({ ml: opts?.ml })}`;
|
|
2210
|
+
await this.request('DELETE', path);
|
|
2211
|
+
}
|
|
2212
|
+
/** `HEAD /api/v1/assets/…` — metadata only. */
|
|
2213
|
+
async headAsset(uriOrIdentifier, opts) {
|
|
2214
|
+
const path = `${this.assetRequestPath(uriOrIdentifier)}${this.assetQuerySuffix({ ml: opts?.ml })}`;
|
|
2215
|
+
const no401Retry = false;
|
|
2216
|
+
const suppressTestSessionHeader = false;
|
|
2217
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2218
|
+
const url = joinUrl(this.baseUrl, path);
|
|
2219
|
+
const outgoingRequestId = newClientRequestId();
|
|
2220
|
+
const headers = {
|
|
2221
|
+
...this.authHeaders(),
|
|
2222
|
+
...this.contextHeaders(),
|
|
2223
|
+
...(suppressTestSessionHeader ? {} : this.testHeaders()),
|
|
2224
|
+
'X-Request-Id': outgoingRequestId,
|
|
2225
|
+
};
|
|
2226
|
+
const init = { method: 'HEAD', headers };
|
|
2227
|
+
const sig = requestTimeoutSignal(this.requestTimeoutMs);
|
|
2228
|
+
if (sig)
|
|
2229
|
+
init.signal = sig;
|
|
2230
|
+
let res;
|
|
2231
|
+
try {
|
|
2232
|
+
res = await fetch(url, init);
|
|
2233
|
+
}
|
|
2234
|
+
catch (e) {
|
|
2235
|
+
const m = e instanceof Error ? e.message : 'Network error';
|
|
2236
|
+
throw new types_1.XCiteDBError(m, 0, null, { clientRequestId: outgoingRequestId });
|
|
2237
|
+
}
|
|
2238
|
+
if (res.ok) {
|
|
2239
|
+
const etagRaw = res.headers.get('ETag') ?? undefined;
|
|
2240
|
+
const ct = res.headers.get('Content-Type') ?? 'application/octet-stream';
|
|
2241
|
+
const len = res.headers.get('Content-Length');
|
|
2242
|
+
const size = len ? parseInt(len, 10) : 0;
|
|
2243
|
+
return { contentType: ct, size: Number.isFinite(size) ? size : 0, etag: etagRaw };
|
|
2244
|
+
}
|
|
2245
|
+
if (res.status === 401 &&
|
|
2246
|
+
attempt === 0 &&
|
|
2247
|
+
!no401Retry &&
|
|
2248
|
+
(await this.tryRefreshSessionAfter401())) {
|
|
2249
|
+
continue;
|
|
2250
|
+
}
|
|
2251
|
+
const text = await res.text();
|
|
2252
|
+
let data;
|
|
2253
|
+
try {
|
|
2254
|
+
data = text ? JSON.parse(text) : null;
|
|
2255
|
+
}
|
|
2256
|
+
catch {
|
|
2257
|
+
data = text;
|
|
2258
|
+
}
|
|
2259
|
+
const msg = typeof data === 'object' && data !== null && 'message' in data
|
|
2260
|
+
? String(data.message)
|
|
2261
|
+
: res.statusText;
|
|
2262
|
+
this.notifySessionInvalidIfNeeded(path, res.status);
|
|
2263
|
+
throwForFailedHttp(res.status, path, data, msg || `HTTP ${res.status}`, res.headers.get('X-Request-Id') ?? undefined, res.headers.get('X-Client-Request-Id') ?? undefined);
|
|
2264
|
+
}
|
|
2265
|
+
this.notifySessionInvalidIfNeeded(path, 401);
|
|
2266
|
+
throw new types_1.XCiteDBError('Request failed after retry', 401, null);
|
|
2267
|
+
}
|
|
2268
|
+
/** Admin: GC dry-run — manifest vs live meta references (`POST /api/v1/admin/assets/gc/dry-run`). */
|
|
2269
|
+
async adminAssetsGcDryRun(opts) {
|
|
2270
|
+
const q = opts?.ownerUserId && opts.ownerUserId.trim()
|
|
2271
|
+
? `?owner=${encodeURIComponent(opts.ownerUserId.trim())}`
|
|
2272
|
+
: '';
|
|
2273
|
+
return this.request('POST', `/api/v1/admin/assets/gc/dry-run${q}`, {});
|
|
2274
|
+
}
|
|
1956
2275
|
/** Embedded platform default `document.conf` text (`GET /api/v1/platform/default-doc-conf`). */
|
|
1957
2276
|
async getPlatformDefaultDocConf() {
|
|
1958
2277
|
return this.request('GET', '/api/v1/platform/default-doc-conf');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { XCiteDBClient } from './client';
|
|
2
|
+
export { parseAssetUri, formatAssetUri, collectIdentifiersFromText, ASSET_URI_PREFIX } from './assetUri';
|
|
2
3
|
export { WebSocketSubscription } from './websocket';
|
|
3
|
-
export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BookmarkRecord, BranchInfo, BranchListItem, CheckpointRecord, CommitRecord, CompareEntry, CompareRef, CompareResult, DatabaseContext, DiffEntry, DiffRef, DiffResult, DocumentBatchResponse, DocumentBatchResultRow, DocumentExportFormat, DocumentImportFormat, ExportDocumentResult, Flags, ImportDocumentOptions, ImportDocumentResult, JsonDocumentData, JsonDocumentBatchItem, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LockConflictBody, LockExpiredBody, LockUnknownBody, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, PlatformDefaultDocConfResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, RebaseUserWorkspaceResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationOptions, UserIsolationShareMode, UserIsolationShareResult, WorkspaceInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, TestSessionBootstrap, TestSessionBootstrapSummary, TestSessionInfo, XCiteDBClientOptions, XCiteDBErrorExtras, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
|
|
4
|
+
export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BookmarkRecord, BranchInfo, BranchListItem, CheckpointRecord, CommitRecord, CompareEntry, CompareRef, CompareResult, DatabaseContext, DiffEntry, DiffRef, DiffResult, DocumentBatchResponse, DocumentBatchResultRow, DocumentExportFormat, DocumentImportFormat, ExportDocumentResult, Flags, ImportDocumentOptions, ImportDocumentResult, JsonDocumentData, JsonDocumentBatchItem, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LockConflictBody, LockExpiredBody, LockUnknownBody, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetMagicLinkListResponse, AssetMagicLinkRecord, AssetMagicLinkResult, AssetShareListEntry, AssetShareListResponse, AssetShareRequest, AssetStorageImport, AssetStorageMount, AssetStorageTarget, AssetStorageTargetType, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, RebaseUserWorkspaceResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationOptions, UserIsolationShareMode, UserIsolationShareResult, WorkspaceInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, TestSessionBootstrap, TestSessionBootstrapSummary, TestSessionInfo, XCiteDBClientOptions, XCiteDBErrorExtras, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
|
|
4
5
|
export { XCiteDBError, XCiteDBForbiddenError, XCiteDBNotFoundError, XCiteDBAuthError, XCiteDBLockConflictError, } from './types';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.XCiteDBLockConflictError = exports.XCiteDBAuthError = exports.XCiteDBNotFoundError = exports.XCiteDBForbiddenError = exports.XCiteDBError = exports.WebSocketSubscription = exports.XCiteDBClient = void 0;
|
|
3
|
+
exports.XCiteDBLockConflictError = exports.XCiteDBAuthError = exports.XCiteDBNotFoundError = exports.XCiteDBForbiddenError = exports.XCiteDBError = exports.WebSocketSubscription = exports.ASSET_URI_PREFIX = exports.collectIdentifiersFromText = exports.formatAssetUri = exports.parseAssetUri = exports.XCiteDBClient = void 0;
|
|
4
4
|
var client_1 = require("./client");
|
|
5
5
|
Object.defineProperty(exports, "XCiteDBClient", { enumerable: true, get: function () { return client_1.XCiteDBClient; } });
|
|
6
|
+
var assetUri_1 = require("./assetUri");
|
|
7
|
+
Object.defineProperty(exports, "parseAssetUri", { enumerable: true, get: function () { return assetUri_1.parseAssetUri; } });
|
|
8
|
+
Object.defineProperty(exports, "formatAssetUri", { enumerable: true, get: function () { return assetUri_1.formatAssetUri; } });
|
|
9
|
+
Object.defineProperty(exports, "collectIdentifiersFromText", { enumerable: true, get: function () { return assetUri_1.collectIdentifiersFromText; } });
|
|
10
|
+
Object.defineProperty(exports, "ASSET_URI_PREFIX", { enumerable: true, get: function () { return assetUri_1.ASSET_URI_PREFIX; } });
|
|
6
11
|
var websocket_1 = require("./websocket");
|
|
7
12
|
Object.defineProperty(exports, "WebSocketSubscription", { enumerable: true, get: function () { return websocket_1.WebSocketSubscription; } });
|
|
8
13
|
var types_1 = require("./types");
|
package/dist/types.d.ts
CHANGED
|
@@ -170,6 +170,125 @@ export interface ProjectDocConfResponse {
|
|
|
170
170
|
has_project_override: boolean;
|
|
171
171
|
doc_conf_text: string | null;
|
|
172
172
|
}
|
|
173
|
+
/** `POST /api/v1/assets` success body (`201`). */
|
|
174
|
+
export interface AssetUploadResult {
|
|
175
|
+
identifier: string;
|
|
176
|
+
uri: string;
|
|
177
|
+
target_name?: string;
|
|
178
|
+
content_type?: string;
|
|
179
|
+
size?: number;
|
|
180
|
+
sha256?: string;
|
|
181
|
+
etag?: string;
|
|
182
|
+
}
|
|
183
|
+
/** Options for {@link XCiteDBClient.uploadAsset}. */
|
|
184
|
+
export interface UploadAssetOptions {
|
|
185
|
+
contentType: string;
|
|
186
|
+
/** When set (with default scope), uploaded to this path after user-isolation prefixing rules on the server. */
|
|
187
|
+
identifier?: string;
|
|
188
|
+
/** `public` writes under `/public/assets/…`. Default project/user scope when omitted. */
|
|
189
|
+
scope?: 'project' | 'public';
|
|
190
|
+
}
|
|
191
|
+
/** Result of {@link XCiteDBClient.headAsset}. */
|
|
192
|
+
export interface AssetHeadResult {
|
|
193
|
+
contentType: string;
|
|
194
|
+
size: number;
|
|
195
|
+
etag?: string;
|
|
196
|
+
}
|
|
197
|
+
/** `POST /api/v1/admin/assets/gc/dry-run` (admin). */
|
|
198
|
+
export interface AssetGcDryRunResult {
|
|
199
|
+
manifest_count: number;
|
|
200
|
+
referenced_asset_count: number;
|
|
201
|
+
orphan_count: number;
|
|
202
|
+
orphan_sample: string[];
|
|
203
|
+
}
|
|
204
|
+
export type AssetStorageTargetType = 'internal' | 's3';
|
|
205
|
+
/** One named storage target in `asset_storage_v2` (`GET/PUT /api/v1/project/settings/asset-storage`). */
|
|
206
|
+
export interface AssetStorageTarget {
|
|
207
|
+
type: AssetStorageTargetType;
|
|
208
|
+
writable?: boolean;
|
|
209
|
+
s3_endpoint_url?: string;
|
|
210
|
+
s3_bucket?: string;
|
|
211
|
+
s3_region?: string;
|
|
212
|
+
access_key_id?: string;
|
|
213
|
+
/** On GET, redacted as `***` when set. */
|
|
214
|
+
secret_access_key?: string;
|
|
215
|
+
small_file_threshold_bytes?: number;
|
|
216
|
+
redirect_threshold_bytes?: number;
|
|
217
|
+
import_head_cache_ttl_seconds?: number;
|
|
218
|
+
[key: string]: unknown;
|
|
219
|
+
}
|
|
220
|
+
export interface AssetStorageMount {
|
|
221
|
+
path: string;
|
|
222
|
+
target: string;
|
|
223
|
+
key_template: string;
|
|
224
|
+
}
|
|
225
|
+
export interface AssetStorageImport {
|
|
226
|
+
path: string;
|
|
227
|
+
target: string;
|
|
228
|
+
key: string;
|
|
229
|
+
tree?: boolean;
|
|
230
|
+
content_type?: string;
|
|
231
|
+
}
|
|
232
|
+
/** Project asset storage v2 (`GET/PUT /api/v1/project/settings/asset-storage`). */
|
|
233
|
+
export interface ProjectAssetStorageConfig {
|
|
234
|
+
targets: Record<string, AssetStorageTarget>;
|
|
235
|
+
mounts: AssetStorageMount[];
|
|
236
|
+
imports: AssetStorageImport[];
|
|
237
|
+
}
|
|
238
|
+
/** Body for `POST /api/v1/security/user-isolation/asset-shares`. */
|
|
239
|
+
export interface AssetShareRequest {
|
|
240
|
+
/** Canonical or app-relative asset path (subject to user-isolation prefixing when enabled). */
|
|
241
|
+
identifier?: string;
|
|
242
|
+
/** `xcitedb:asset:/…` or raw `/…` path; not prefixed by the client. */
|
|
243
|
+
source_uri?: string;
|
|
244
|
+
target_user_id: string;
|
|
245
|
+
mode: UserIsolationShareMode;
|
|
246
|
+
}
|
|
247
|
+
/** Body for `DELETE /api/v1/security/user-isolation/asset-shares`. */
|
|
248
|
+
export interface AssetUnshareRequest {
|
|
249
|
+
identifier?: string;
|
|
250
|
+
source_uri?: string;
|
|
251
|
+
target_user_id?: string;
|
|
252
|
+
sharer_user_id?: string;
|
|
253
|
+
}
|
|
254
|
+
export interface AssetShareListEntry {
|
|
255
|
+
identifier: string;
|
|
256
|
+
mode: string;
|
|
257
|
+
}
|
|
258
|
+
/** `GET /api/v1/security/user-isolation/asset-shares` */
|
|
259
|
+
export interface AssetShareListResponse {
|
|
260
|
+
direction: string;
|
|
261
|
+
identifiers: AssetShareListEntry[];
|
|
262
|
+
scope?: string;
|
|
263
|
+
note?: string;
|
|
264
|
+
}
|
|
265
|
+
/** `POST /api/v1/security/asset-magic-links` */
|
|
266
|
+
export interface CreateAssetMagicLinkRequest {
|
|
267
|
+
source_uri: string;
|
|
268
|
+
actions: ('read' | 'write')[];
|
|
269
|
+
expires_in_seconds: number;
|
|
270
|
+
max_uses?: number;
|
|
271
|
+
}
|
|
272
|
+
export interface AssetMagicLinkResult {
|
|
273
|
+
token: string;
|
|
274
|
+
token_id?: string;
|
|
275
|
+
expires_at: string;
|
|
276
|
+
link_url: string;
|
|
277
|
+
}
|
|
278
|
+
export interface AssetMagicLinkRecord {
|
|
279
|
+
token_id: string;
|
|
280
|
+
identifier: string;
|
|
281
|
+
actions?: unknown;
|
|
282
|
+
issued_by_user_id?: string;
|
|
283
|
+
issued_at?: string;
|
|
284
|
+
expires_at?: string;
|
|
285
|
+
max_uses?: number;
|
|
286
|
+
uses?: number;
|
|
287
|
+
revoked?: boolean;
|
|
288
|
+
}
|
|
289
|
+
export interface AssetMagicLinkListResponse {
|
|
290
|
+
tokens: AssetMagicLinkRecord[];
|
|
291
|
+
}
|
|
173
292
|
/** `GET /api/v1/platform/default-doc-conf` */
|
|
174
293
|
export interface PlatformDefaultDocConfResponse {
|
|
175
294
|
doc_conf_text: string;
|
|
@@ -429,8 +548,18 @@ export interface DocumentBatchResultRow {
|
|
|
429
548
|
index: number;
|
|
430
549
|
identifier: string;
|
|
431
550
|
ok: boolean;
|
|
551
|
+
/**
|
|
552
|
+
* When `ok` is false, server batch precheck / write reason. Includes:
|
|
553
|
+
* `duplicate_identifier_in_batch` (409), `parent_inlines_batch_member` (409),
|
|
554
|
+
* `lock_conflict` (423), and other parse/policy strings.
|
|
555
|
+
*/
|
|
432
556
|
error?: string;
|
|
433
557
|
code?: number;
|
|
558
|
+
/**
|
|
559
|
+
* Structured context for XML batch **`409`** precheck rows, e.g. `{ identifier }` for
|
|
560
|
+
* `duplicate_identifier_in_batch` or `{ parent, inlined_child }` for `parent_inlines_batch_member`.
|
|
561
|
+
*/
|
|
562
|
+
detail?: Record<string, string>;
|
|
434
563
|
/** Present when `error === 'lock_conflict'` (HTTP-style code 423 in batch row). */
|
|
435
564
|
current_lock?: LockInfo;
|
|
436
565
|
}
|
package/llms-full.txt
CHANGED
|
@@ -12,7 +12,7 @@ Before reading the full reference, note these critical differences from typical
|
|
|
12
12
|
|
|
13
13
|
2. **Identifiers are hierarchical, path-like strings** — e.g. `/us/bills/hr1`, `/manual/v2/chapter3`. They are NOT auto-generated UUIDs. The leading `/` is part of the identifier. Parent/child relationships are derived from the path structure.
|
|
14
14
|
|
|
15
|
-
3. **Documents are XML or JSON, not arbitrary blobs.** XML documents are the primary document type. JSON documents are a parallel store. Both are fully versioned.
|
|
15
|
+
3. **Documents are XML or JSON, not arbitrary blobs.** XML documents are the primary document type. JSON documents are a parallel store. Both are fully versioned. **Binary files** use the separate **assets** subsystem (not document shredding): canonical paths like `/assets/<uuid>`, stable **`xcitedb:asset:/…`** URIs, **`POST` / `GET` / `HEAD` / `DELETE /api/v1/assets/…`**, project **`asset-storage` v2** config, optional user-isolation **asset-shares**, and **asset magic links** (`?ml=`).
|
|
16
16
|
|
|
17
17
|
4. **Context (workspace + date) travels as HTTP headers** (`X-Workspace` preferred, `X-Branch` alias, `X-Date`, and optionally `X-Unversioned` for explicit flat writes), not URL path segments.
|
|
18
18
|
|
|
@@ -28,7 +28,9 @@ Before reading the full reference, note these critical differences from typical
|
|
|
28
28
|
|
|
29
29
|
10. **Ephemeral test sessions.** `POST /api/v1/test/sessions` (authenticated) returns a UUID **`session_token`**. Clients send **`X-Test-Session: <token>`** on API calls to use an isolated, TTL- and quota-limited LMDB instead of production project data. Unless **`X-Test-Auth: required`** is set, **developer** JWT/API-key checks are bypassed (synthetic admin for wet tests), but **app-user** identity via **`X-App-User-Token`** or Bearer app-user JWT is still recognized. Management routes under **`/api/v1/test/*`** must not include `X-Test-Session`. With a default body (omit or `{}`), the test LMDB starts **empty** (no cloned production project config).
|
|
30
30
|
|
|
31
|
-
11. **Overlay test sessions.** Same **`POST`**, with JSON **`{"overlay":true}`**, while authenticated for the **project to debug** (project-scoped API key, or platform Bearer + **`X-Project-Id`**). The session metadata records overlay mode; subsequent requests need only **`X-Test-Session`**. The server opens **`XCiteDB(_test/<uuid>/data, <production data path>)`**: production is used as a **read-only base**; reads merge overlay + base; **writes never modify production**. If the production data directory is missing, opening the session database fails. JS **`createTestSession({ …, overlay: true })`**, C++ **`test_session_overlay`** + **`create_test_session`**, MCP **`create_test_session`** tool **`overlay: true`**.
|
|
31
|
+
11. **Overlay test sessions.** Same **`POST`**, with JSON **`{"overlay":true}`**, while authenticated for the **project to debug** (project-scoped API key, or platform Bearer + **`X-Project-Id`**). The session metadata records overlay mode; subsequent requests need only **`X-Test-Session`**. The server opens **`XCiteDB(_test/<uuid>/data, <production data path>)`**: production is used as a **read-only base**; reads merge overlay + base; **writes never modify production**. If the production data directory is missing, opening the session database fails. JS **`createTestSession({ …, overlay: true })`**, C++ **`test_session_overlay`** + **`create_test_session`**, MCP **`create_test_session`** tool **`overlay: true`**. **Locks:** cooperative lock metadata is read from the **base** layer as well as the overlay, but lock acquire/release only affects the overlay—so **`lock_conflict`** during overlay tests can reflect a lock still held in production; use **`findLocks` / `forceReleaseLock`** on your paths or a non-overlay session when testing locks.
|
|
32
|
+
|
|
33
|
+
12. **SDK asset helpers.** JavaScript: `uploadAsset`, `getAsset`, `headAsset`, `deleteAsset`, `resolveAssetIdentifier`, `formatAssetUri`, `collectIdentifiersFromText`, plus `createAssetShare` / `createAssetMagicLink` and related APIs. Python: `upload_asset`, `get_asset`, `head_asset`, `xcitedb.asset_uri`. C++: `xcitedb/asset_uri.hpp`, `XCiteDBClient::upload_asset`, `get_asset_raw`, `head_asset_raw`, `delete_asset`, and security helpers mirroring the other SDKs.
|
|
32
34
|
|
|
33
35
|
## Choosing the Right Versioning Approach
|
|
34
36
|
|
|
@@ -63,6 +65,8 @@ Legacy REST paths under `/api/v1/branches`, `/commits`, `/tags`, `/diff` remain
|
|
|
63
65
|
|
|
64
66
|
7. **Do not mock XciteDB in tests — use ephemeral test sessions instead.** Unlike most BaaS platforms, XciteDB has built-in support for isolated, throwaway database sessions specifically designed for wet integration tests. Mocking the client skips the actual storage, versioning, querying, and ABAC behavior, producing tests that don't catch real integration issues. Use `createTestSession()` / `test_session()` / `create_test_session()` (SDK helpers) or `POST /api/v1/test/sessions` directly to get a real LMDB under `_test/<uuid>/` (empty by default, or **overlay** on read-only production with **`{"overlay":true}`** / **`overlay: true`** / **`test_session_overlay`**). See "Ephemeral test sessions" below.
|
|
65
67
|
|
|
68
|
+
8. **Identifier strings (minting).** The API canonicalizes hierarchical paths (**leading `/`**, **no trailing `/`**). **`POST /api/v1/documents`** rejects malformed ids with **`400`** JSON **`{ "error": "invalid_identifier", "reason", "identifier", "detail" }`**: no commas, no ASCII control characters (including **`0x01`**), no `//` or empty segments, no adjacent duplicate segments (e.g. `/a/b/b/c`). Reserved namespaces include **`/_xcitedb/`**, **`/assets/`**, **`/public/assets/`**, and **`/public/shared/<tenant>/assets/`**. **`identifier_hierarchy_max_depth`** (default **4**) tunes hierarchy indexing; deeper paths still work but **`GET …/identifier-children`** may scan.
|
|
69
|
+
|
|
66
70
|
---
|
|
67
71
|
|
|
68
72
|
# Part 1: Product Overview
|
|
@@ -426,7 +430,7 @@ With `Content-Type: application/json`:
|
|
|
426
430
|
|
|
427
431
|
With `Content-Type: application/xml`: send raw XML body.
|
|
428
432
|
|
|
429
|
-
The identifier is extracted from the `db:identifier` attribute in the root XML element.
|
|
433
|
+
The identifier is extracted from the `db:identifier` attribute in the root XML element. Malformed root identifiers return **`400`** (`invalid_identifier`). Invalid stub/placeholder XML on write returns **`400`** with `error: "xml_write_rejected"` and a `message` string.
|
|
430
434
|
|
|
431
435
|
## Get document by identifier
|
|
432
436
|
|
|
@@ -436,14 +440,57 @@ Query parameters: `identifier` (required), `flags` (optional: `FirstMatch`, `Inc
|
|
|
436
440
|
|
|
437
441
|
### Shallow node + subtree navigation
|
|
438
442
|
|
|
439
|
-
- **`flags=NoChildren,KeepIndexNodes,FirstMatch`**: returns the matching node with inline leaf content plus
|
|
443
|
+
- **`flags=NoChildren,KeepIndexNodes,FirstMatch`**: returns the matching node with inline leaf content plus **identifier-bearing stub elements** **`db:stub="true"`** for shredded child slots (real element names; avoids loading the full deep subtree in one response). The engine still stores internal **`<db:N…/>`** placeholders on disk.
|
|
444
|
+
- **`GET …/by-id`** with **`FirstMatch,IncludeChildren`** returns a JSON array of **length 0 or 1**; index `0` is the full assembled XML when present.
|
|
440
445
|
- Combine with **`GET /api/v1/documents/identifier-children?parent_path=…`** for the next level of hierarchy (`segment`, `full_path`, `is_identifier`, `has_children`).
|
|
441
|
-
- **JavaScript SDK:** `
|
|
446
|
+
- **JavaScript SDK:** `queryByIdentifierFull(id)` loads **`FirstMatch,IncludeChildren`** for editor round-trips; **`queryByIdentifierShallow(id)`** + **`listChildIdentifiers(id)`** (e.g. `Promise.all` client-side) for sidebar / tree expansion.
|
|
442
447
|
|
|
443
448
|
### Standalone subtree write (shredded model)
|
|
444
449
|
|
|
445
450
|
You may `POST /api/v1/documents` with a root element whose `db:identifier` is a nested path (e.g. `/spaces/u/docs/doc1/sec-1`) without re-posting the parent document: the engine writes that subtree only and updates the identifier hierarchy index for the parent automatically. Keep `is_top: true` (default) on the write.
|
|
446
451
|
|
|
452
|
+
### XML shredding semantics (must read)
|
|
453
|
+
|
|
454
|
+
- **Auto-shred.** Any element with **`db:identifier`** is stored as its own LMDB row. The parent’s stored XML keeps an internal **`<db:N…/>`** placeholder for that slot (engine-only). There is **no** size threshold and **no** opt-out.
|
|
455
|
+
- **Pruned writes.** Clients may send **stub** elements (**`db:stub="true"`**) or legacy **`<db:N…/>`** under a parent to preserve subtrees without inlining full XML. Stubs must be empty, reference an **existing** identifier, and must not appear at the document root; violations are **`400`** (`xml_write_rejected`).
|
|
456
|
+
- **`is_top` is a marker only.** It registers a **`TOP:<xcitepath>`** alias for tooling; it does **not** change shredding, merge, or overwrite behavior.
|
|
457
|
+
- **Batch ordering.** **`POST /api/v1/documents/batch`** processes **`items[]`** **sequentially**. Each item’s XML is shredded so **every** `db:identifier` in that payload becomes (or overwrites) its own row **except** ids that appear **only** on **`db:stub="true"`** elements (references, not inlined bodies). Precheck returns per-row **`409`**: **`duplicate_identifier_in_batch`** when the same canonical root id appears in more than one item (the **later** row is rejected), and **`parent_inlines_batch_member`** when one item’s XML still **fully inlines** another item’s **root** id in the same batch (stubs referencing another batch member are allowed).
|
|
458
|
+
- **Idempotency (retries).** Re-sending the same XML with the same **`X-Date`** (or unversioned mode) is a no-op when the revision is unchanged; reuse the date key on retries.
|
|
459
|
+
|
|
460
|
+
**Round-trip:** Element structure, child order, and text are preserved. The server may add **`db:order`**, **`db:xcitepath`**, **`xmlns:db`**. Attribute order and insignificant whitespace are **not** byte-stable—compare by DOM, not raw bytes.
|
|
461
|
+
|
|
462
|
+
### Batch XML writes
|
|
463
|
+
|
|
464
|
+
**`POST /api/v1/documents/batch`** — JSON body **`{ "items": [ { "xml": "<…>", "is_top": true, "compare_attributes": false, "identifier": "/optional/match" }, … ] }`**. Response **`200`** with **`{ "results": [ { "index", "ok", "identifier", "error"?, "code"?, "detail"?, "current_lock"? } ] }`** (`detail` for **`409`** precheck rows: duplicate → `{ "identifier": "<root>" }`; parent inline → `{ "parent": "<root>", "inlined_child": "<other>" }`). Best-effort: later rows still run when earlier rows fail. JavaScript: **`writeXmlDocumentsBatch`**. Python: **`write_xml_documents_batch`**. C++: **`write_xml_documents_batch`**.
|
|
465
|
+
|
|
466
|
+
### Minimal SPA editor recipe
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
await client.enableUserIsolation();
|
|
470
|
+
client.setContext({ workspace: '', project_id: projectId });
|
|
471
|
+
|
|
472
|
+
async function loadDocForEditor(id: string) {
|
|
473
|
+
return client.queryByIdentifierFull(id);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function sidebarRow(id: string) {
|
|
477
|
+
const [node, listed] = await Promise.all([
|
|
478
|
+
client.queryByIdentifierShallow(id),
|
|
479
|
+
client.listIdentifierChildren(id),
|
|
480
|
+
]);
|
|
481
|
+
return { node, children: listed.children };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function saveDirty(items: XmlDocumentBatchItem[]) {
|
|
485
|
+
const { results } = await client.writeXmlDocumentsBatch(items);
|
|
486
|
+
results.forEach((r, i) => {
|
|
487
|
+
if (!r.ok) console.warn(i, r.error, r.code);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Batch save rules:** (1) At most **one** batch item per flush may use a given root **`db:identifier`** (`duplicate_identifier_in_batch`). (2) Do not ship a parent that **fully inlines** another item’s root id in the same batch (`parent_inlines_batch_member`); **`db:stub="true"`** references to another batch member are **allowed**. (3) **`is_top`** does not relax (1) or (2).
|
|
493
|
+
|
|
447
494
|
## Delete document
|
|
448
495
|
|
|
449
496
|
**`DELETE /api/v1/documents/by-id?identifier=/book/ch1`**
|
|
@@ -1538,9 +1585,10 @@ interface DatabaseContext {
|
|
|
1538
1585
|
- `importDocument(file, options?)` → `ImportDocumentResult` — multipart `POST /api/v1/documents/import`
|
|
1539
1586
|
- `exportDocument(identifier, format?, options?)` → `ExportDocumentResult` — binary `GET /api/v1/documents/export`
|
|
1540
1587
|
- `queryByIdentifier(identifier, flags?, filter?, pathFilter?)` → `string[]`
|
|
1541
|
-
- `
|
|
1588
|
+
- `queryByIdentifierFull(identifier, filter?, pathFilter?)` → `string[]` — `FirstMatch,IncludeChildren` (editor load); array length **0 or 1**
|
|
1589
|
+
- `queryByIdentifierShallow(identifier, filter?, pathFilter?)` → `string[]` — `NoChildren,KeepIndexNodes,FirstMatch` (skeleton with **`db:stub="true"`** child markers)
|
|
1542
1590
|
- `listChildIdentifiers(parentPath?)` → `ListIdentifierChildrenResult` — alias of `listIdentifierChildren`
|
|
1543
|
-
- `
|
|
1591
|
+
- `writeXmlDocumentsBatch(items)` → `DocumentBatchResponse` — `POST /api/v1/documents/batch` (per-row `409` + optional **`detail`**; stubs exempt from duplicate-root / parent-inline checks per semantics above)
|
|
1544
1592
|
- `queryDocuments(query: XCiteQuery, flags?, filter?, pathFilter?)` → `string[]`
|
|
1545
1593
|
- `deleteDocument(identifier)` → `void`
|
|
1546
1594
|
- `listIdentifiers(query: XCiteQuery)` → `ListIdentifiersResult`
|
package/llms.txt
CHANGED
|
@@ -22,7 +22,9 @@ These are the most common sources of confusion for developers and AI assistants:
|
|
|
22
22
|
|
|
23
23
|
8. **Project vs tenant id.** In the SDK, prefer `context.project_id` (and `listMyProjects` / `switchProject`). Many JSON bodies and JWT claims still use the field name `tenant_id` for the same value — the client sends that wire name automatically.
|
|
24
24
|
|
|
25
|
-
9. **Ephemeral test sessions (wet tests).** Call **`POST /api/v1/test/sessions`** with a normal API key or Bearer token to get a `session_token` (UUID). Send **`X-Test-Session: <token>`** on subsequent document/API calls to use an isolated, short-lived LMDB instead of production data. By default **developer** auth (API key / platform JWT) is bypassed, but **app-user** identity (`X-App-User-Token` or Bearer app-user JWT) is still recognized. Send **`X-Test-Auth: required`** to exercise full developer JWT/API-key auth and ABAC against the same test database. Do not send `X-Test-Session` on `/api/v1/test/*` management routes. Server limits apply (`test.session_ttl_seconds`, `test.max_sessions_per_key`, `test.max_test_db_size_bytes` in config). By default the test LMDB starts **empty** (no production data). **Overlay mode:** same **`POST`** with JSON body **`{"overlay":true}`** (while authenticated for the project you want to inspect—project-scoped API key, or platform Bearer + **`X-Project-Id`**). The server opens a **writable** LMDB under `_test/<uuid>/` with that project’s on-disk store as a **read-only base** (dual LMDB): reads see production + overlay deltas; **writes never touch production**. **`XCiteDBClient.createTestSession({ …, overlay: true })`** sends that body. After creation, only **`X-Test-Session`** is required on requests (overlay is stored in session metadata).
|
|
25
|
+
9. **Ephemeral test sessions (wet tests).** Call **`POST /api/v1/test/sessions`** with a normal API key or Bearer token to get a `session_token` (UUID). Send **`X-Test-Session: <token>`** on subsequent document/API calls to use an isolated, short-lived LMDB instead of production data. By default **developer** auth (API key / platform JWT) is bypassed, but **app-user** identity (`X-App-User-Token` or Bearer app-user JWT) is still recognized. Send **`X-Test-Auth: required`** to exercise full developer JWT/API-key auth and ABAC against the same test database. Do not send `X-Test-Session` on `/api/v1/test/*` management routes. Server limits apply (`test.session_ttl_seconds`, `test.max_sessions_per_key`, `test.max_test_db_size_bytes` in config). By default the test LMDB starts **empty** (no production data). **Overlay mode:** same **`POST`** with JSON body **`{"overlay":true}`** (while authenticated for the project you want to inspect—project-scoped API key, or platform Bearer + **`X-Project-Id`**). The server opens a **writable** LMDB under `_test/<uuid>/` with that project’s on-disk store as a **read-only base** (dual LMDB): reads see production + overlay deltas; **writes never touch production**. **`XCiteDBClient.createTestSession({ …, overlay: true })`** sends that body. After creation, only **`X-Test-Session`** is required on requests (overlay is stored in session metadata). **Overlay + locks:** cooperative locks are stored in LMDB meta; overlay sessions **read through** to **base-layer** locks, but **`acquireLock` / `releaseLock`** only mutate the overlay—so a `lock_conflict` in an overlay test can still mean a lock held in production; clear with **`findLocks`** / **`forceReleaseLock`** or use a non-overlay session for lock-sensitive tests.
|
|
26
|
+
|
|
27
|
+
10. **Binary assets (v2) are not XML/JSON documents.** Files live under canonical asset identifiers (e.g. `/assets/<uuid>`) and stable **`xcitedb:asset:/…`** URIs. **`POST /api/v1/assets`** uploads raw bytes; **`GET` / `HEAD` / `DELETE /api/v1/assets/…`** use per-segment URL encoding (optional **`?ml=`** magic-link bearer). Project routing is **`GET/PUT /api/v1/project/settings/asset-storage`** (`targets`, `mounts`, `imports`). User-isolation **asset-shares** and **asset magic links** live under **`/api/v1/security/user-isolation/asset-shares`** and **`/api/v1/security/asset-magic-links`**. JS/Python/C++ SDKs expose upload/get/head/delete + URI helpers (`parseAssetUri`, `asset_uri.parse`, `asset_uri::parse`) and share/magic-link clients.
|
|
26
28
|
|
|
27
29
|
## Choosing the Right Versioning Approach
|
|
28
30
|
|
|
@@ -72,6 +74,8 @@ Legacy REST paths (`/api/v1/branches`, `/commits`, `/tags`, `/diff`) remain as *
|
|
|
72
74
|
|
|
73
75
|
12. **`X-Date` / `context.date` is not “server clock only.”** Revisions are keyed by the **instant you send**, not an implicit “now” when you set the header. To record a document under a **business date** (published, approved, effective), set **`X-Date`** or **`context.date`** to that instant before writing. Omitting **`X-Date`** does **not** substitute the current time on the write path — it selects **flat** writes (see convention 4).
|
|
74
76
|
|
|
77
|
+
13. **Identifier strings (minting).** The API canonicalizes paths (**leading `/`**, **no trailing `/`**). **`POST /api/v1/documents`** rejects malformed hierarchical ids with **`400`** and JSON **`{ "error": "invalid_identifier", "reason", "identifier", "detail" }`**: no commas, no ASCII control characters (including **`0x01`** LMDB separator), no `//` or empty path segments, no **adjacent duplicate** path segments (e.g. `/a/b/b/c`). Reserved namespaces include **`/_xcitedb/`**, **`/assets/`**, **`/public/assets/`**, and **`/public/shared/<tenant>/assets/`**. Server config **`identifier_hierarchy_max_depth`** defaults to **4**—deeper paths still work, but **`identifier-children`** listing may fall back to a scan.
|
|
78
|
+
|
|
75
79
|
## API key capability matrix (typical)
|
|
76
80
|
|
|
77
81
|
| Capability | API key `role` | Public key allowed? |
|
|
@@ -162,6 +166,48 @@ When you build a backend that calls XCiteDB on behalf of users:
|
|
|
162
166
|
|
|
163
167
|
- **Standalone subtree root.** Because XCiteDB shreds XML per identifier, you can `writeXmlDocument('<sec db:identifier="/spaces/u/docs/doc1/sec-1">…</sec>')` and the engine writes only that subtree — you do **not** need to re-send the parent document. The parent's children index (`id_hier`) is updated automatically. Keep `is_top: true` (default) on `POST /api/v1/documents` even when the `db:identifier` is nested under another path. Use `compare_attributes: true` only when you need attribute-level diffing for triggers.
|
|
164
168
|
|
|
169
|
+
## XML shredding semantics (must read)
|
|
170
|
+
|
|
171
|
+
- **Auto-shred.** Any element with **`db:identifier`** is stored as its own LMDB row. The parent’s stored XML keeps an internal **`<db:N…/>`** placeholder for that slot (engine-only). There is **no** size threshold and **no** opt-out.
|
|
172
|
+
- **Shallow reads (`NoChildren,KeepIndexNodes`).** The API returns **identifier-bearing stubs** instead of opaque placeholders: **`<elem db:identifier="…" db:xcitepath="…" db:order="…" db:stub="true"/>`** (real element name, empty element). Clients may round-trip these stubs through **`writeXmlDocument`**; preserved subtrees are not deleted. You may also send legacy **`<db:N…/>`** placeholders from older clients; the engine resolves them under the parent’s **`db:xcitepath`**.
|
|
173
|
+
- **Pruned writes.** You may mix **full** identified subtrees and **stub** markers (or placeholders) under one parent. Stubs must be **empty** (no element children, no non-whitespace text), carry **`db:identifier`** (or legacy **`identifier`**) pointing at an **existing** row, and optional **`db:order`**, **`db:xcitepath`**, **`xmlns:db`** only. The document root must not be a stub. Violations yield **`400`** with `error: "xml_write_rejected"` and a **`message`** string (`stub_*` / `placeholder_*`).
|
|
174
|
+
- **`is_top` is a marker only.** It registers a **`TOP:<xcitepath>`** alias for tooling; it does **not** change shredding, merge, or overwrite behavior. Leaving **`true`** on every write is normal.
|
|
175
|
+
- **`POST /api/v1/documents/batch`** runs items **in order**; each item’s write replaces LMDB rows for **every** `db:identifier` present in that item’s XML **except** identifiers that appear only on **`db:stub="true"`** elements (those are references, not inlined document bodies). The server prechecks each row and may return **`409`** with **`duplicate_identifier_in_batch`** (same root id in two items) or **`parent_inlines_batch_member`** (a parent’s XML still **fully inlines** another item’s root id). Failed rows include a JSON **`detail`** object: for duplicates **`{ "identifier": "<root>" }`**; for parent inline **`{ "parent": "<root>", "inlined_child": "<other root>" }`**. Check **`results[i].ok`** on the JSON response.
|
|
176
|
+
- **Idempotency (retries).** Re-sending the same XML body with the same **`X-Date`** (or unversioned mode) is a safe no-op when the stored revision is unchanged; use the same date key on client retries to avoid duplicate logical revisions.
|
|
177
|
+
|
|
178
|
+
**Round-trip:** Element structure, child order, and text are preserved. The server may add **`db:order`**, **`db:xcitepath`**, **`xmlns:db`**. Any app-specific structural metadata that must survive write→read (e.g. numbering aligned with an editor) must live **inside the stored XML** (or JSON metadata via **`addMeta`**); the server does not sync arbitrary sidecar fields. Attribute order and insignificant whitespace are **not** byte-stable—compare by DOM, not raw bytes.
|
|
179
|
+
|
|
180
|
+
## Minimal SPA editor recipe
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
await client.enableUserIsolation(); // if app users edit under namespaced paths
|
|
184
|
+
client.setContext({ workspace: '', project_id: projectId });
|
|
185
|
+
|
|
186
|
+
/** Full document for the editor (all shredded children inlined). */
|
|
187
|
+
async function loadDocForEditor(id: string) {
|
|
188
|
+
return client.queryByIdentifierFull(id);
|
|
189
|
+
// Returns JSON array length 0 or 1: when present, index 0 is one fully assembled XML string (document order).
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Sidebar / tree: shallow shell + one hierarchy level (two parallel requests). */
|
|
193
|
+
async function sidebarRow(id: string) {
|
|
194
|
+
const [node, listed] = await Promise.all([
|
|
195
|
+
client.queryByIdentifierShallow(id),
|
|
196
|
+
client.listIdentifierChildren(id),
|
|
197
|
+
]);
|
|
198
|
+
return { node, children: listed.children };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function saveDirty(items: XmlDocumentBatchItem[]) {
|
|
202
|
+
const { results } = await client.writeXmlDocumentsBatch(items);
|
|
203
|
+
results.forEach((r, i) => {
|
|
204
|
+
if (!r.ok) console.warn(i, r.error, r.code);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Batch save rules:** (1) Each intended root **`db:identifier`** appears as the **root** of **at most one** batch item per flush (**`duplicate_identifier_in_batch`**). (2) Do not ship a parent’s **full** subtree for a child id that is also its **own** batch row in the same request (**`parent_inlines_batch_member`**). Stubs **`db:stub="true"`** referencing another batch member are **allowed** and do not trigger (2). (3) **`is_top`** does not relax (1) or (2).
|
|
210
|
+
|
|
165
211
|
## JavaScript/TypeScript SDK (`@xcitedbs/client`)
|
|
166
212
|
|
|
167
213
|
Install: `npm install @xcitedbs/client`
|
|
@@ -282,13 +328,14 @@ interface XCiteDBClientOptions {
|
|
|
282
328
|
|
|
283
329
|
**XML Documents:**
|
|
284
330
|
- `writeXmlDocument(xml, options?)` — Store XML via JSON body (identifier inside XML). Deprecated alias: `writeDocumentJson`.
|
|
331
|
+
- `writeXmlDocumentsBatch(items)` — `POST /api/v1/documents/batch`; per-row `ok` / `error` / `code` / optional `detail` (see shredding section for `409` batch conflicts)
|
|
285
332
|
- `writeXML(xml)` — Store raw XML with `Content-Type: application/xml`
|
|
286
333
|
- `importDocument(file, options?)` — `POST /api/v1/documents/import` (multipart `file`). Server converts DOCX, ODF, RTF, PDF, Markdown, AsciiDoc, or plain text into shredded XML. Optional `identifier` query override; optional `filename` for sniffing when not a `File`.
|
|
287
334
|
- `exportDocument(id, format?, options?)` — `GET /api/v1/documents/export` binary (`xml` | `docx` | `odt` | `pdf` | `txt` | `md` | `adoc`). Optional `strict` for text exports.
|
|
288
335
|
- `queryByIdentifier(id, flags?, filter?)` — Get document(s) by identifier
|
|
289
|
-
- `
|
|
290
|
-
- `
|
|
291
|
-
- `
|
|
336
|
+
- `queryByIdentifierFull(id, filter?, pathFilter?)` — Same as `flags=FirstMatch,IncludeChildren` (editor load: full subtree). Response array is **length 0 or 1**; the single string is the full document.
|
|
337
|
+
- `queryByIdentifierShallow(id, filter?, pathFilter?)` — Same as `flags=NoChildren,KeepIndexNodes,FirstMatch` (skeleton: **`db:stub="true"`** elements for shredded child slots; avoids full subtree)
|
|
338
|
+
- `listChildIdentifiers(parentPath?)` — Alias of `listIdentifierChildren`; next level of identifier hierarchy (pair with `queryByIdentifierShallow` for sidebars—`Promise.all` both if you want one round-trip client-side)
|
|
292
339
|
- `queryDocuments(query, flags?)` — List/filter documents
|
|
293
340
|
- `deleteDocument(identifier)` — Delete by identifier
|
|
294
341
|
- `listIdentifiers(query)` — List known identifiers with pagination
|
package/package.json
CHANGED
package/unquery-ai-guide.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
Unquery is a declarative language for querying and transforming structured documents (JSON and XML). It is the query language of XCiteDB and the `unq` command-line tool. Every query is itself a JSON document. The result is also JSON.
|
|
6
6
|
|
|
7
|
+
> **Maintainers — parse regression tests:** Fenced `json` / `text` examples in this file, plus Unquery listings in [`web/src/docs/content/unquery-tutorial.html`](../web/src/docs/content/unquery-tutorial.html), feed the generated fixture [`XCiteDB2/tests/fixtures/unquery_tutorial_parse_cases.json`](../XCiteDB2/tests/fixtures/unquery_tutorial_parse_cases.json) (do not edit by hand). After changing examples here or in that HTML file, regenerate from the repo root with `python3 scripts/gen_unquery_tutorial_parse_cases.py`, then commit the updated JSON. CI / local builds run `unquery_tutorial_parse_test` (see [`XCiteDB2/tests/run_tests.sh`](../XCiteDB2/tests/run_tests.sh)).
|
|
8
|
+
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
## 0. AI preamble — must-read before writing a query
|