@xcitedbs/client 0.3.9 → 0.3.10
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 +49 -9
- package/dist/client.js +79 -47
- package/dist/client.test.js +279 -0
- package/dist/types.d.ts +2 -0
- package/dist/user-isolation.test.js +7 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -188,6 +188,19 @@ export declare class XCiteDBClient {
|
|
|
188
188
|
/** Sets active project for platform console mode (`X-Project-Id`). */
|
|
189
189
|
setProjectId(projectId: string): void;
|
|
190
190
|
setContext(ctx: DatabaseContext): void;
|
|
191
|
+
private static mergeContext;
|
|
192
|
+
/**
|
|
193
|
+
* Create a child client that shares transport/auth wiring with this one but has an
|
|
194
|
+
* independent context. Forking does not mutate the parent; child and parent can be used
|
|
195
|
+
* concurrently with different workspaces, dates, or prefixes. Use this in place of
|
|
196
|
+
* `setContext → do work → setContext(prev)`.
|
|
197
|
+
*
|
|
198
|
+
* Auth tokens (api key, access token, app-user tokens) are copied at fork time. Subsequent
|
|
199
|
+
* token rotation via `setTokens` / `setAppUserTokens` (or callbacks) updates only the
|
|
200
|
+
* instance that triggered it; if you fork long-lived clients, keep token state in sync via
|
|
201
|
+
* the `onSessionTokensUpdated` / `onAppUserTokensUpdated` callbacks supplied at construction.
|
|
202
|
+
*/
|
|
203
|
+
fork(partial?: Partial<DatabaseContext>): XCiteDBClient;
|
|
191
204
|
setTokens(access: string, refresh?: string): void;
|
|
192
205
|
/** End-user (app) tokens. With developer `accessToken`/`apiKey`, sent as `X-App-User-Token`. */
|
|
193
206
|
setAppUserTokens(access: string, refresh?: string): void;
|
|
@@ -751,7 +764,7 @@ export declare class XCiteDBClient {
|
|
|
751
764
|
merge?: MergeResult;
|
|
752
765
|
}>;
|
|
753
766
|
/** Send raw XML body (`Content-Type: application/xml`). For JSON wrapper + options use `writeXmlDocument`. */
|
|
754
|
-
writeXML(xml: string,
|
|
767
|
+
writeXML(xml: string, options?: WriteDocumentOptions): Promise<void>;
|
|
755
768
|
/**
|
|
756
769
|
* Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
|
|
757
770
|
* For storing JSON data by key, use `writeJsonDocument`.
|
|
@@ -785,21 +798,27 @@ export declare class XCiteDBClient {
|
|
|
785
798
|
* @deprecated Use {@link writeXmlDocument}. This name was misleading: it writes **XML** via a JSON wrapper, not a JSON document.
|
|
786
799
|
*/
|
|
787
800
|
writeDocumentJson(xml: string, options?: WriteDocumentOptions): Promise<void>;
|
|
788
|
-
queryByIdentifier(identifier: string, flags?: Flags, filter?: string, pathFilter?: string
|
|
801
|
+
queryByIdentifier(identifier: string, flags?: Flags, filter?: string, pathFilter?: string, opts?: {
|
|
802
|
+
context?: Partial<DatabaseContext>;
|
|
803
|
+
}): Promise<string[]>;
|
|
789
804
|
/**
|
|
790
805
|
* Shallow read (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`):
|
|
791
806
|
* root element with inline leaves plus **identifier-bearing stub elements** (`db:stub="true"`) for shredded
|
|
792
807
|
* child slots instead of opaque `db:N*` placeholders. Safe to round-trip with {@link writeXmlDocument}.
|
|
793
808
|
* For sidebar / AST navigation, pair with {@link listIdentifierChildren} (e.g. `Promise.all` of shallow + children).
|
|
794
809
|
*/
|
|
795
|
-
queryByIdentifierShallow(identifier: string, filter?: string, pathFilter?: string
|
|
810
|
+
queryByIdentifierShallow(identifier: string, filter?: string, pathFilter?: string, opts?: {
|
|
811
|
+
context?: Partial<DatabaseContext>;
|
|
812
|
+
}): Promise<string[]>;
|
|
796
813
|
/**
|
|
797
814
|
* Load a document with all shredded children inlined (`FirstMatch,IncludeChildren` on `GET /by-id`).
|
|
798
815
|
* The returned array has **length 0 or 1**; when present, index `0` is the full XML string.
|
|
799
816
|
* Use this for editor round-trips. For navigation without loading the full subtree, use
|
|
800
817
|
* {@link queryByIdentifierShallow} plus {@link listIdentifierChildren}.
|
|
801
818
|
*/
|
|
802
|
-
queryByIdentifierFull(identifier: string, filter?: string, pathFilter?: string
|
|
819
|
+
queryByIdentifierFull(identifier: string, filter?: string, pathFilter?: string, opts?: {
|
|
820
|
+
context?: Partial<DatabaseContext>;
|
|
821
|
+
}): Promise<string[]>;
|
|
803
822
|
/** Alias of {@link listIdentifierChildren} — immediate child segments under `parentPath` (identifier hierarchy). */
|
|
804
823
|
listChildIdentifiers(parentPath?: string): Promise<ListIdentifierChildrenResult>;
|
|
805
824
|
queryDocuments(query: XCiteQuery, flags?: Flags, filter?: string, pathFilter?: string): Promise<string[]>;
|
|
@@ -815,10 +834,12 @@ export declare class XCiteDBClient {
|
|
|
815
834
|
addMeta(identifier: string, value: unknown, path?: string, opts?: {
|
|
816
835
|
mode?: 'set' | 'append' | 'merge_append';
|
|
817
836
|
overwrite?: boolean;
|
|
837
|
+
context?: Partial<DatabaseContext>;
|
|
818
838
|
}): Promise<boolean>;
|
|
819
839
|
addMetaByQuery(query: XCiteQuery, value: unknown, path?: string, firstMatch?: boolean, opts?: {
|
|
820
840
|
mode?: 'set' | 'append' | 'merge_append';
|
|
821
841
|
overwrite?: boolean;
|
|
842
|
+
context?: Partial<DatabaseContext>;
|
|
822
843
|
}): Promise<boolean>;
|
|
823
844
|
/**
|
|
824
845
|
* Strict array-extend: `value` must be an array; the stored target at `path` must be an array
|
|
@@ -828,9 +849,11 @@ export declare class XCiteDBClient {
|
|
|
828
849
|
*/
|
|
829
850
|
appendMeta(identifier: string, value: unknown[], path?: string, opts?: {
|
|
830
851
|
overwrite?: boolean;
|
|
852
|
+
context?: Partial<DatabaseContext>;
|
|
831
853
|
}): Promise<boolean>;
|
|
832
854
|
appendMetaByQuery(query: XCiteQuery, value: unknown[], path?: string, firstMatch?: boolean, opts?: {
|
|
833
855
|
overwrite?: boolean;
|
|
856
|
+
context?: Partial<DatabaseContext>;
|
|
834
857
|
}): Promise<boolean>;
|
|
835
858
|
/**
|
|
836
859
|
* Deep-extend: walks the payload and extends any nested arrays found in storage. Scalars and
|
|
@@ -839,17 +862,31 @@ export declare class XCiteDBClient {
|
|
|
839
862
|
*/
|
|
840
863
|
mergeAppendMeta(identifier: string, value: unknown, path?: string, opts?: {
|
|
841
864
|
overwrite?: boolean;
|
|
865
|
+
context?: Partial<DatabaseContext>;
|
|
842
866
|
}): Promise<boolean>;
|
|
843
867
|
mergeAppendMetaByQuery(query: XCiteQuery, value: unknown, path?: string, firstMatch?: boolean, opts?: {
|
|
844
868
|
overwrite?: boolean;
|
|
869
|
+
context?: Partial<DatabaseContext>;
|
|
845
870
|
}): Promise<boolean>;
|
|
846
871
|
/** Convenience: push a single item. Wraps `item` in `[item]` and calls {@link appendMeta}. */
|
|
847
|
-
appendItem(identifier: string, item:
|
|
872
|
+
appendItem<T = unknown>(identifier: string, item: T, path?: string, opts?: {
|
|
848
873
|
overwrite?: boolean;
|
|
874
|
+
context?: Partial<DatabaseContext>;
|
|
875
|
+
}): Promise<boolean>;
|
|
876
|
+
/** Convenience: push a single item via query. Wraps `item` in `[item]` and calls {@link appendMetaByQuery}. */
|
|
877
|
+
appendItemByQuery<T = unknown>(query: XCiteQuery, item: T, path?: string, firstMatch?: boolean, opts?: {
|
|
878
|
+
overwrite?: boolean;
|
|
879
|
+
context?: Partial<DatabaseContext>;
|
|
880
|
+
}): Promise<boolean>;
|
|
881
|
+
queryMeta<T = MetaValue>(identifier: string, path?: string, opts?: {
|
|
882
|
+
context?: Partial<DatabaseContext>;
|
|
883
|
+
}): Promise<T>;
|
|
884
|
+
queryMetaByQuery<T = MetaValue>(query: XCiteQuery, path?: string, opts?: {
|
|
885
|
+
context?: Partial<DatabaseContext>;
|
|
886
|
+
}): Promise<T>;
|
|
887
|
+
clearMeta(query: XCiteQuery, opts?: {
|
|
888
|
+
context?: Partial<DatabaseContext>;
|
|
849
889
|
}): Promise<boolean>;
|
|
850
|
-
queryMeta<T = MetaValue>(identifier: string, path?: string): Promise<T>;
|
|
851
|
-
queryMetaByQuery<T = MetaValue>(query: XCiteQuery, path?: string): Promise<T>;
|
|
852
|
-
clearMeta(query: XCiteQuery): Promise<boolean>;
|
|
853
890
|
/**
|
|
854
891
|
* Acquire a cooperative lock. On exclusive conflict the server returns HTTP 409 with a
|
|
855
892
|
* {@link LockConflictBody} body (thrown as {@link XCiteDBError} with `.status === 409`).
|
|
@@ -902,7 +939,10 @@ export declare class XCiteDBClient {
|
|
|
902
939
|
* );
|
|
903
940
|
* ```
|
|
904
941
|
*/
|
|
905
|
-
unquery<T = UnqueryResult>(query: XCiteQuery, unquery: UnqueryTemplate
|
|
942
|
+
unquery<T = UnqueryResult>(query: XCiteQuery, unquery: UnqueryTemplate, opts?: {
|
|
943
|
+
context?: Partial<DatabaseContext>;
|
|
944
|
+
strict?: boolean;
|
|
945
|
+
}): Promise<T>;
|
|
906
946
|
search(q: TextSearchQuery): Promise<TextSearchResult>;
|
|
907
947
|
reindex(): Promise<{
|
|
908
948
|
status: string;
|
package/dist/client.js
CHANGED
|
@@ -629,6 +629,15 @@ class XCiteDBClient {
|
|
|
629
629
|
if (canonical.startsWith('/users/') && !canonical.startsWith(`${ns}/`) && canonical !== ns) {
|
|
630
630
|
return canonical;
|
|
631
631
|
}
|
|
632
|
+
// Tenant-managed share-alias roots are never under any user namespace; pass them through
|
|
633
|
+
// unchanged so app users can read documents shared with them via group / public / anonymous
|
|
634
|
+
// share aliases (server alias paths produced by UserIsolationService::compute*ShareAliasPath).
|
|
635
|
+
if (canonical.startsWith('/groups/') ||
|
|
636
|
+
canonical.startsWith('/shared/') ||
|
|
637
|
+
canonical.startsWith('/shared-readonly/') ||
|
|
638
|
+
canonical.startsWith('/public/')) {
|
|
639
|
+
return canonical;
|
|
640
|
+
}
|
|
632
641
|
const combined = `${ns}${canonical === '/' ? '/' : canonical}`;
|
|
633
642
|
const finalId = combined.replace(/\/+/g, '/');
|
|
634
643
|
if (!finalId.startsWith(ns) || (finalId.length > ns.length && finalId.charAt(ns.length) !== '/')) {
|
|
@@ -821,11 +830,33 @@ class XCiteDBClient {
|
|
|
821
830
|
this.projectId = projectId;
|
|
822
831
|
}
|
|
823
832
|
setContext(ctx) {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
833
|
+
this.defaultContext = XCiteDBClient.mergeContext(this.defaultContext, ctx);
|
|
834
|
+
}
|
|
835
|
+
static mergeContext(prev, partial) {
|
|
836
|
+
const next = { ...prev, ...partial };
|
|
837
|
+
if (partial.workspace !== undefined) {
|
|
838
|
+
next.branch = partial.workspace;
|
|
827
839
|
}
|
|
828
|
-
|
|
840
|
+
return next;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Create a child client that shares transport/auth wiring with this one but has an
|
|
844
|
+
* independent context. Forking does not mutate the parent; child and parent can be used
|
|
845
|
+
* concurrently with different workspaces, dates, or prefixes. Use this in place of
|
|
846
|
+
* `setContext → do work → setContext(prev)`.
|
|
847
|
+
*
|
|
848
|
+
* Auth tokens (api key, access token, app-user tokens) are copied at fork time. Subsequent
|
|
849
|
+
* token rotation via `setTokens` / `setAppUserTokens` (or callbacks) updates only the
|
|
850
|
+
* instance that triggered it; if you fork long-lived clients, keep token state in sync via
|
|
851
|
+
* the `onSessionTokensUpdated` / `onAppUserTokensUpdated` callbacks supplied at construction.
|
|
852
|
+
*/
|
|
853
|
+
fork(partial) {
|
|
854
|
+
const child = Object.create(XCiteDBClient.prototype);
|
|
855
|
+
Object.assign(child, this);
|
|
856
|
+
child.defaultContext = XCiteDBClient.mergeContext(this.defaultContext, partial ?? {});
|
|
857
|
+
// Context can shift the user-isolation prefix; the parent's cached app-user id no longer applies.
|
|
858
|
+
child.cachedAppUserId = undefined;
|
|
859
|
+
return child;
|
|
829
860
|
}
|
|
830
861
|
setTokens(access, refresh) {
|
|
831
862
|
this.accessToken = access;
|
|
@@ -844,9 +875,9 @@ class XCiteDBClient {
|
|
|
844
875
|
this.appUserRefreshToken = undefined;
|
|
845
876
|
this.clearAppUserIdCache();
|
|
846
877
|
}
|
|
847
|
-
contextHeaders() {
|
|
878
|
+
contextHeaders(override) {
|
|
848
879
|
const h = {};
|
|
849
|
-
const c = this.defaultContext;
|
|
880
|
+
const c = override ? XCiteDBClient.mergeContext(this.defaultContext, override) : this.defaultContext;
|
|
850
881
|
const ws = c.workspace ?? c.branch;
|
|
851
882
|
if (ws)
|
|
852
883
|
h['X-Workspace'] = ws;
|
|
@@ -907,11 +938,12 @@ class XCiteDBClient {
|
|
|
907
938
|
async request(method, path, body, extraHeaders, opts) {
|
|
908
939
|
const no401Retry = opts?.no401Retry === true;
|
|
909
940
|
const suppressTestSessionHeader = opts?.suppressTestSessionHeader === true;
|
|
941
|
+
const contextOverride = opts?.contextOverride;
|
|
910
942
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
911
943
|
const url = joinUrl(this.baseUrl, path);
|
|
912
944
|
const headers = {
|
|
913
945
|
...this.authHeaders(),
|
|
914
|
-
...this.contextHeaders(),
|
|
946
|
+
...this.contextHeaders(contextOverride),
|
|
915
947
|
...(suppressTestSessionHeader ? {} : this.testHeaders()),
|
|
916
948
|
...extraHeaders,
|
|
917
949
|
};
|
|
@@ -2067,25 +2099,18 @@ class XCiteDBClient {
|
|
|
2067
2099
|
* create a checkpoint, then publish back unless {@link options.autoMerge} is `false`.
|
|
2068
2100
|
*/
|
|
2069
2101
|
async withWorkspace(workspaceName, fn, options) {
|
|
2070
|
-
const
|
|
2071
|
-
|
|
2072
|
-
|
|
2102
|
+
const fromWs = options?.fromBranch ?? this.defaultContext.branch ?? this.defaultContext.workspace ?? '';
|
|
2103
|
+
await this.createWorkspace(workspaceName, fromWs || undefined, this.defaultContext.date || undefined);
|
|
2104
|
+
const child = this.fork({ workspace: workspaceName, branch: workspaceName });
|
|
2105
|
+
const result = await fn(child);
|
|
2106
|
+
const checkpoint = await child.createCheckpoint(options?.message ?? `Workspace ${workspaceName}`, options?.author);
|
|
2073
2107
|
let publish;
|
|
2074
|
-
|
|
2075
|
-
await
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
checkpoint = await this.createCheckpoint(options?.message ?? `Workspace ${workspaceName}`, options?.author);
|
|
2079
|
-
if (options?.autoMerge !== false) {
|
|
2080
|
-
publish = await this.publishWorkspace(fromWs, workspaceName, {
|
|
2081
|
-
message: options?.message,
|
|
2082
|
-
});
|
|
2083
|
-
}
|
|
2084
|
-
return { result, checkpoint, publish };
|
|
2085
|
-
}
|
|
2086
|
-
finally {
|
|
2087
|
-
this.setContext(prev);
|
|
2108
|
+
if (options?.autoMerge !== false) {
|
|
2109
|
+
publish = await child.publishWorkspace(fromWs, workspaceName, {
|
|
2110
|
+
message: options?.message,
|
|
2111
|
+
});
|
|
2088
2112
|
}
|
|
2113
|
+
return { result, checkpoint, publish };
|
|
2089
2114
|
}
|
|
2090
2115
|
/** @deprecated Use {@link withWorkspace}. */
|
|
2091
2116
|
async withBranch(branchName, fn, options) {
|
|
@@ -2093,11 +2118,9 @@ class XCiteDBClient {
|
|
|
2093
2118
|
return { result: r.result, commit: r.checkpoint, merge: r.publish };
|
|
2094
2119
|
}
|
|
2095
2120
|
/** Send raw XML body (`Content-Type: application/xml`). For JSON wrapper + options use `writeXmlDocument`. */
|
|
2096
|
-
async writeXML(xml,
|
|
2121
|
+
async writeXML(xml, options) {
|
|
2097
2122
|
const payload = this.isoApplyXmlDbIdentifier(xml);
|
|
2098
|
-
await this.request('POST', '/api/v1/documents', payload, {
|
|
2099
|
-
'Content-Type': 'application/xml',
|
|
2100
|
-
});
|
|
2123
|
+
await this.request('POST', '/api/v1/documents', payload, { 'Content-Type': 'application/xml' }, { contextOverride: options?.context });
|
|
2101
2124
|
}
|
|
2102
2125
|
/**
|
|
2103
2126
|
* Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
|
|
@@ -2114,7 +2137,7 @@ class XCiteDBClient {
|
|
|
2114
2137
|
xml: this.isoApplyXmlDbIdentifier(xml),
|
|
2115
2138
|
is_top: options?.is_top ?? true,
|
|
2116
2139
|
compare_attributes: options?.compare_attributes ?? false,
|
|
2117
|
-
});
|
|
2140
|
+
}, undefined, { contextOverride: options?.context });
|
|
2118
2141
|
}
|
|
2119
2142
|
/**
|
|
2120
2143
|
* Best-effort batch XML writes (`POST /api/v1/documents/batch`). Each item is independent; check `results[].ok`.
|
|
@@ -2194,14 +2217,16 @@ class XCiteDBClient {
|
|
|
2194
2217
|
async writeDocumentJson(xml, options) {
|
|
2195
2218
|
return this.writeXmlDocument(xml, options);
|
|
2196
2219
|
}
|
|
2197
|
-
async queryByIdentifier(identifier, flags, filter, pathFilter) {
|
|
2220
|
+
async queryByIdentifier(identifier, flags, filter, pathFilter, opts) {
|
|
2198
2221
|
const q = buildQuery({
|
|
2199
2222
|
identifier: this.isoPrefixId(identifier),
|
|
2200
2223
|
flags: flags,
|
|
2201
2224
|
filter,
|
|
2202
2225
|
path_filter: pathFilter,
|
|
2203
2226
|
});
|
|
2204
|
-
return this.request('GET', `/api/v1/documents/by-id${q}
|
|
2227
|
+
return this.request('GET', `/api/v1/documents/by-id${q}`, undefined, undefined, {
|
|
2228
|
+
contextOverride: opts?.context,
|
|
2229
|
+
});
|
|
2205
2230
|
}
|
|
2206
2231
|
/**
|
|
2207
2232
|
* Shallow read (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`):
|
|
@@ -2209,8 +2234,8 @@ class XCiteDBClient {
|
|
|
2209
2234
|
* child slots instead of opaque `db:N*` placeholders. Safe to round-trip with {@link writeXmlDocument}.
|
|
2210
2235
|
* For sidebar / AST navigation, pair with {@link listIdentifierChildren} (e.g. `Promise.all` of shallow + children).
|
|
2211
2236
|
*/
|
|
2212
|
-
async queryByIdentifierShallow(identifier, filter, pathFilter) {
|
|
2213
|
-
return this.queryByIdentifier(identifier, 'NoChildren,KeepIndexNodes,FirstMatch', filter, pathFilter);
|
|
2237
|
+
async queryByIdentifierShallow(identifier, filter, pathFilter, opts) {
|
|
2238
|
+
return this.queryByIdentifier(identifier, 'NoChildren,KeepIndexNodes,FirstMatch', filter, pathFilter, opts);
|
|
2214
2239
|
}
|
|
2215
2240
|
/**
|
|
2216
2241
|
* Load a document with all shredded children inlined (`FirstMatch,IncludeChildren` on `GET /by-id`).
|
|
@@ -2218,8 +2243,8 @@ class XCiteDBClient {
|
|
|
2218
2243
|
* Use this for editor round-trips. For navigation without loading the full subtree, use
|
|
2219
2244
|
* {@link queryByIdentifierShallow} plus {@link listIdentifierChildren}.
|
|
2220
2245
|
*/
|
|
2221
|
-
async queryByIdentifierFull(identifier, filter, pathFilter) {
|
|
2222
|
-
return this.queryByIdentifier(identifier, 'FirstMatch,IncludeChildren', filter, pathFilter);
|
|
2246
|
+
async queryByIdentifierFull(identifier, filter, pathFilter, opts) {
|
|
2247
|
+
return this.queryByIdentifier(identifier, 'FirstMatch,IncludeChildren', filter, pathFilter, opts);
|
|
2223
2248
|
}
|
|
2224
2249
|
/** Alias of {@link listIdentifierChildren} — immediate child segments under `parentPath` (identifier hierarchy). */
|
|
2225
2250
|
async listChildIdentifiers(parentPath) {
|
|
@@ -2369,7 +2394,9 @@ class XCiteDBClient {
|
|
|
2369
2394
|
body.mode = opts.mode;
|
|
2370
2395
|
if (opts?.overwrite)
|
|
2371
2396
|
body.overwrite = true;
|
|
2372
|
-
const r = await this.request('POST', '/api/v1/meta', body
|
|
2397
|
+
const r = await this.request('POST', '/api/v1/meta', body, undefined, {
|
|
2398
|
+
contextOverride: opts?.context,
|
|
2399
|
+
});
|
|
2373
2400
|
return r?.ok !== false;
|
|
2374
2401
|
}
|
|
2375
2402
|
async addMetaByQuery(query, value, path = '', firstMatch = false, opts) {
|
|
@@ -2383,7 +2410,9 @@ class XCiteDBClient {
|
|
|
2383
2410
|
body.mode = opts.mode;
|
|
2384
2411
|
if (opts?.overwrite)
|
|
2385
2412
|
body.overwrite = true;
|
|
2386
|
-
const r = await this.request('POST', '/api/v1/meta', body
|
|
2413
|
+
const r = await this.request('POST', '/api/v1/meta', body, undefined, {
|
|
2414
|
+
contextOverride: opts?.context,
|
|
2415
|
+
});
|
|
2387
2416
|
return r?.ok !== false;
|
|
2388
2417
|
}
|
|
2389
2418
|
/**
|
|
@@ -2413,16 +2442,18 @@ class XCiteDBClient {
|
|
|
2413
2442
|
async appendItem(identifier, item, path = '', opts) {
|
|
2414
2443
|
return this.appendMeta(identifier, [item], path, opts);
|
|
2415
2444
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2445
|
+
/** Convenience: push a single item via query. Wraps `item` in `[item]` and calls {@link appendMetaByQuery}. */
|
|
2446
|
+
async appendItemByQuery(query, item, path = '', firstMatch = false, opts) {
|
|
2447
|
+
return this.appendMetaByQuery(query, [item], path, firstMatch, opts);
|
|
2418
2448
|
}
|
|
2419
|
-
async
|
|
2420
|
-
return this.request('
|
|
2449
|
+
async queryMeta(identifier, path = '', opts) {
|
|
2450
|
+
return this.request('GET', `/api/v1/meta${buildQuery({ identifier: this.isoPrefixId(identifier), path })}`, undefined, undefined, { contextOverride: opts?.context });
|
|
2421
2451
|
}
|
|
2422
|
-
async
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2452
|
+
async queryMetaByQuery(query, path = '', opts) {
|
|
2453
|
+
return this.request('POST', '/api/v1/meta/query', { query: this.isoPrefixQuery(query), path }, undefined, { contextOverride: opts?.context });
|
|
2454
|
+
}
|
|
2455
|
+
async clearMeta(query, opts) {
|
|
2456
|
+
const r = await this.request('DELETE', '/api/v1/meta', { query: this.isoPrefixQuery(query) }, undefined, { contextOverride: opts?.context });
|
|
2426
2457
|
return r?.ok !== false;
|
|
2427
2458
|
}
|
|
2428
2459
|
/**
|
|
@@ -2515,8 +2546,9 @@ class XCiteDBClient {
|
|
|
2515
2546
|
* );
|
|
2516
2547
|
* ```
|
|
2517
2548
|
*/
|
|
2518
|
-
async unquery(query, unquery) {
|
|
2519
|
-
|
|
2549
|
+
async unquery(query, unquery, opts) {
|
|
2550
|
+
const extraHeaders = opts?.strict ? { 'X-Unquery-Strict': 'true' } : undefined;
|
|
2551
|
+
return this.request('POST', '/api/v1/unquery', { query: this.isoPrefixQuery(query), unquery }, extraHeaders, { contextOverride: opts?.context });
|
|
2520
2552
|
}
|
|
2521
2553
|
async search(q) {
|
|
2522
2554
|
const body = { query: q.query };
|
package/dist/client.test.js
CHANGED
|
@@ -611,6 +611,285 @@ const types_js_1 = require("./types.js");
|
|
|
611
611
|
}
|
|
612
612
|
});
|
|
613
613
|
});
|
|
614
|
+
(0, node_test_1.describe)('client.fork()', () => {
|
|
615
|
+
(0, node_test_1.it)('child sends overridden workspace; parent keeps original on parallel calls', async () => {
|
|
616
|
+
const seen = [];
|
|
617
|
+
const orig = globalThis.fetch;
|
|
618
|
+
globalThis.fetch = node_test_1.mock.fn(async (input, init) => {
|
|
619
|
+
const h = new Headers(init?.headers);
|
|
620
|
+
seen.push({ url: String(input), workspace: h.get('X-Workspace') });
|
|
621
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
622
|
+
});
|
|
623
|
+
try {
|
|
624
|
+
const parent = new client_js_1.XCiteDBClient({
|
|
625
|
+
baseUrl: 'http://127.0.0.1:9',
|
|
626
|
+
apiKey: 'k',
|
|
627
|
+
context: { workspace: 'main' },
|
|
628
|
+
});
|
|
629
|
+
const child = parent.fork({ workspace: 'feature-x' });
|
|
630
|
+
await Promise.all([
|
|
631
|
+
parent.queryMeta('/d1'),
|
|
632
|
+
child.queryMeta('/d2'),
|
|
633
|
+
parent.queryMeta('/d3'),
|
|
634
|
+
]);
|
|
635
|
+
strict_1.default.equal(seen.length, 3);
|
|
636
|
+
const byUrl = new Map(seen.map((s) => [s.url, s.workspace]));
|
|
637
|
+
strict_1.default.equal([...byUrl.keys()].some((u) => u.includes('%2Fd1')), true);
|
|
638
|
+
const ws = (id) => seen.find((s) => s.url.includes(id)).workspace;
|
|
639
|
+
strict_1.default.equal(ws('%2Fd1'), 'main');
|
|
640
|
+
strict_1.default.equal(ws('%2Fd2'), 'feature-x');
|
|
641
|
+
strict_1.default.equal(ws('%2Fd3'), 'main');
|
|
642
|
+
}
|
|
643
|
+
finally {
|
|
644
|
+
globalThis.fetch = orig;
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
(0, node_test_1.it)('child setContext does not mutate parent', async () => {
|
|
648
|
+
const c = new client_js_1.XCiteDBClient({
|
|
649
|
+
baseUrl: 'http://127.0.0.1:9',
|
|
650
|
+
apiKey: 'k',
|
|
651
|
+
context: { workspace: 'main', date: '2025-01-01' },
|
|
652
|
+
});
|
|
653
|
+
const child = c.fork({ workspace: 'feat' });
|
|
654
|
+
child.setContext({ workspace: 'feat-v2', date: '2025-12-31' });
|
|
655
|
+
// Capture parent's headers via a single request.
|
|
656
|
+
const orig = globalThis.fetch;
|
|
657
|
+
let parentWs = null;
|
|
658
|
+
let parentDate = null;
|
|
659
|
+
globalThis.fetch = node_test_1.mock.fn(async (_i, init) => {
|
|
660
|
+
const h = new Headers(init?.headers);
|
|
661
|
+
parentWs = h.get('X-Workspace');
|
|
662
|
+
parentDate = h.get('X-Date');
|
|
663
|
+
return new Response(JSON.stringify({}), { status: 200 });
|
|
664
|
+
});
|
|
665
|
+
try {
|
|
666
|
+
await c.queryMeta('/p');
|
|
667
|
+
strict_1.default.equal(parentWs, 'main');
|
|
668
|
+
strict_1.default.equal(parentDate, '2025-01-01');
|
|
669
|
+
}
|
|
670
|
+
finally {
|
|
671
|
+
globalThis.fetch = orig;
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
(0, node_test_1.it)('fork() with no arg copies parent context verbatim', async () => {
|
|
675
|
+
const c = new client_js_1.XCiteDBClient({
|
|
676
|
+
baseUrl: 'http://127.0.0.1:9',
|
|
677
|
+
apiKey: 'k',
|
|
678
|
+
context: { workspace: 'w', date: '2025-06-01', prefix: '/p' },
|
|
679
|
+
});
|
|
680
|
+
const child = c.fork();
|
|
681
|
+
let h = null;
|
|
682
|
+
const orig = globalThis.fetch;
|
|
683
|
+
globalThis.fetch = node_test_1.mock.fn(async (_i, init) => {
|
|
684
|
+
h = new Headers(init?.headers);
|
|
685
|
+
return new Response(JSON.stringify({}), { status: 200 });
|
|
686
|
+
});
|
|
687
|
+
try {
|
|
688
|
+
await child.queryMeta('/d');
|
|
689
|
+
strict_1.default.equal(h.get('X-Workspace'), 'w');
|
|
690
|
+
strict_1.default.equal(h.get('X-Date'), '2025-06-01');
|
|
691
|
+
strict_1.default.equal(h.get('X-Prefix'), '/p');
|
|
692
|
+
}
|
|
693
|
+
finally {
|
|
694
|
+
globalThis.fetch = orig;
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
(0, node_test_1.it)('fork({workspace}) mirrors workspace into branch (back-compat alias)', async () => {
|
|
698
|
+
const c = new client_js_1.XCiteDBClient({
|
|
699
|
+
baseUrl: 'http://127.0.0.1:9',
|
|
700
|
+
apiKey: 'k',
|
|
701
|
+
context: { workspace: 'main', branch: 'main' },
|
|
702
|
+
});
|
|
703
|
+
const child = c.fork({ workspace: 'feature-y' });
|
|
704
|
+
// Inspect via a request: server only sees X-Workspace, but the deprecated branch alias
|
|
705
|
+
// must update too so legacy code paths inside the SDK that read `branch` see the new value.
|
|
706
|
+
let h = null;
|
|
707
|
+
const orig = globalThis.fetch;
|
|
708
|
+
globalThis.fetch = node_test_1.mock.fn(async (_i, init) => {
|
|
709
|
+
h = new Headers(init?.headers);
|
|
710
|
+
return new Response(JSON.stringify({}), { status: 200 });
|
|
711
|
+
});
|
|
712
|
+
try {
|
|
713
|
+
await child.queryMeta('/d');
|
|
714
|
+
strict_1.default.equal(h.get('X-Workspace'), 'feature-y');
|
|
715
|
+
}
|
|
716
|
+
finally {
|
|
717
|
+
globalThis.fetch = orig;
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
(0, node_test_1.describe)('per-call opts.context override', () => {
|
|
722
|
+
(0, node_test_1.it)('queryMeta {context: {workspace}} sends overridden X-Workspace', async () => {
|
|
723
|
+
let captured = null;
|
|
724
|
+
const orig = globalThis.fetch;
|
|
725
|
+
globalThis.fetch = node_test_1.mock.fn(async (_i, init) => {
|
|
726
|
+
captured = new Headers(init?.headers);
|
|
727
|
+
return new Response(JSON.stringify({}), { status: 200 });
|
|
728
|
+
});
|
|
729
|
+
try {
|
|
730
|
+
const c = new client_js_1.XCiteDBClient({
|
|
731
|
+
baseUrl: 'http://127.0.0.1:9',
|
|
732
|
+
apiKey: 'k',
|
|
733
|
+
context: { workspace: 'main' },
|
|
734
|
+
});
|
|
735
|
+
await c.queryMeta('/d', '', { context: { workspace: 'feature-x' } });
|
|
736
|
+
strict_1.default.equal(captured.get('X-Workspace'), 'feature-x');
|
|
737
|
+
}
|
|
738
|
+
finally {
|
|
739
|
+
globalThis.fetch = orig;
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
(0, node_test_1.it)('opts.context does NOT mutate the client default context', async () => {
|
|
743
|
+
const seen = [];
|
|
744
|
+
const orig = globalThis.fetch;
|
|
745
|
+
globalThis.fetch = node_test_1.mock.fn(async (_i, init) => {
|
|
746
|
+
const h = new Headers(init?.headers);
|
|
747
|
+
seen.push(h.get('X-Workspace') ?? '');
|
|
748
|
+
return new Response(JSON.stringify({}), { status: 200 });
|
|
749
|
+
});
|
|
750
|
+
try {
|
|
751
|
+
const c = new client_js_1.XCiteDBClient({
|
|
752
|
+
baseUrl: 'http://127.0.0.1:9',
|
|
753
|
+
apiKey: 'k',
|
|
754
|
+
context: { workspace: 'main' },
|
|
755
|
+
});
|
|
756
|
+
await c.queryMeta('/a', '', { context: { workspace: 'one-off' } });
|
|
757
|
+
await c.queryMeta('/b'); // should fall back to default 'main'
|
|
758
|
+
strict_1.default.deepEqual(seen, ['one-off', 'main']);
|
|
759
|
+
}
|
|
760
|
+
finally {
|
|
761
|
+
globalThis.fetch = orig;
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
(0, node_test_1.it)('addMeta opts.context overrides workspace and date', async () => {
|
|
765
|
+
let captured = null;
|
|
766
|
+
const orig = globalThis.fetch;
|
|
767
|
+
globalThis.fetch = node_test_1.mock.fn(async (_i, init) => {
|
|
768
|
+
captured = new Headers(init?.headers);
|
|
769
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
770
|
+
});
|
|
771
|
+
try {
|
|
772
|
+
const c = new client_js_1.XCiteDBClient({
|
|
773
|
+
baseUrl: 'http://127.0.0.1:9',
|
|
774
|
+
apiKey: 'k',
|
|
775
|
+
context: { workspace: 'main', date: '2025-01-01' },
|
|
776
|
+
});
|
|
777
|
+
await c.addMeta('/d', { x: 1 }, '', {
|
|
778
|
+
context: { workspace: 'feat', date: '2025-12-31' },
|
|
779
|
+
});
|
|
780
|
+
strict_1.default.equal(captured.get('X-Workspace'), 'feat');
|
|
781
|
+
strict_1.default.equal(captured.get('X-Date'), '2025-12-31');
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
globalThis.fetch = orig;
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
(0, node_test_1.it)('appendItem opts.context flows through to the wrapped appendMeta call', async () => {
|
|
788
|
+
let captured = null;
|
|
789
|
+
const orig = globalThis.fetch;
|
|
790
|
+
globalThis.fetch = node_test_1.mock.fn(async (_i, init) => {
|
|
791
|
+
captured = new Headers(init?.headers);
|
|
792
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
793
|
+
});
|
|
794
|
+
try {
|
|
795
|
+
const c = new client_js_1.XCiteDBClient({ baseUrl: 'http://127.0.0.1:9', apiKey: 'k' });
|
|
796
|
+
await c.appendItem('/d', 1, 'list', { context: { workspace: 'w-pc' } });
|
|
797
|
+
strict_1.default.equal(captured.get('X-Workspace'), 'w-pc');
|
|
798
|
+
}
|
|
799
|
+
finally {
|
|
800
|
+
globalThis.fetch = orig;
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
(0, node_test_1.it)('unquery opts.context overrides workspace', async () => {
|
|
804
|
+
let captured = null;
|
|
805
|
+
const orig = globalThis.fetch;
|
|
806
|
+
globalThis.fetch = node_test_1.mock.fn(async (_i, init) => {
|
|
807
|
+
captured = new Headers(init?.headers);
|
|
808
|
+
return new Response(JSON.stringify({}), { status: 200 });
|
|
809
|
+
});
|
|
810
|
+
try {
|
|
811
|
+
const c = new client_js_1.XCiteDBClient({
|
|
812
|
+
baseUrl: 'http://127.0.0.1:9',
|
|
813
|
+
apiKey: 'k',
|
|
814
|
+
context: { workspace: 'main' },
|
|
815
|
+
});
|
|
816
|
+
await c.unquery({ match_start: '/x/' }, { id: '$identifier' }, { context: { workspace: 'unq' } });
|
|
817
|
+
strict_1.default.equal(captured.get('X-Workspace'), 'unq');
|
|
818
|
+
}
|
|
819
|
+
finally {
|
|
820
|
+
globalThis.fetch = orig;
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
(0, node_test_1.it)('writeXmlDocument options.context overrides workspace', async () => {
|
|
824
|
+
let captured = null;
|
|
825
|
+
const orig = globalThis.fetch;
|
|
826
|
+
globalThis.fetch = node_test_1.mock.fn(async (_i, init) => {
|
|
827
|
+
captured = new Headers(init?.headers);
|
|
828
|
+
return new Response(JSON.stringify({}), { status: 200 });
|
|
829
|
+
});
|
|
830
|
+
try {
|
|
831
|
+
const c = new client_js_1.XCiteDBClient({
|
|
832
|
+
baseUrl: 'http://127.0.0.1:9',
|
|
833
|
+
apiKey: 'k',
|
|
834
|
+
context: { workspace: 'main' },
|
|
835
|
+
});
|
|
836
|
+
await c.writeXmlDocument('<?xml version="1.0"?><doc xmlns:db="http://www.xcitedb.com/schema" db:identifier="/x"/>', { context: { workspace: 'wxd' } });
|
|
837
|
+
strict_1.default.equal(captured.get('X-Workspace'), 'wxd');
|
|
838
|
+
}
|
|
839
|
+
finally {
|
|
840
|
+
globalThis.fetch = orig;
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
(0, node_test_1.describe)('appendItem / appendItemByQuery', () => {
|
|
845
|
+
(0, node_test_1.it)('appendItem wraps a typed item in [item] and POSTs mode=append', async () => {
|
|
846
|
+
const captured = { value: null };
|
|
847
|
+
const orig = globalThis.fetch;
|
|
848
|
+
globalThis.fetch = node_test_1.mock.fn(async (input, init) => {
|
|
849
|
+
captured.value = {
|
|
850
|
+
url: String(input),
|
|
851
|
+
body: JSON.parse(String(init?.body ?? '{}')),
|
|
852
|
+
};
|
|
853
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
854
|
+
});
|
|
855
|
+
try {
|
|
856
|
+
const c = new client_js_1.XCiteDBClient({ baseUrl: 'http://127.0.0.1:9', apiKey: 'k' });
|
|
857
|
+
const review = { stars: 5, text: 'great' };
|
|
858
|
+
const ok = await c.appendItem('/products/p1', review, 'reviews');
|
|
859
|
+
strict_1.default.equal(ok, true);
|
|
860
|
+
strict_1.default.match(captured.value.url, /\/api\/v1\/meta$/);
|
|
861
|
+
const body = captured.value.body;
|
|
862
|
+
strict_1.default.equal(body.identifier, '/products/p1');
|
|
863
|
+
strict_1.default.equal(body.path, 'reviews');
|
|
864
|
+
strict_1.default.equal(body.mode, 'append');
|
|
865
|
+
strict_1.default.deepEqual(body.value, [review]);
|
|
866
|
+
}
|
|
867
|
+
finally {
|
|
868
|
+
globalThis.fetch = orig;
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
(0, node_test_1.it)('appendItemByQuery wraps a typed item in [item] and includes first_match', async () => {
|
|
872
|
+
const captured = { value: null };
|
|
873
|
+
const orig = globalThis.fetch;
|
|
874
|
+
globalThis.fetch = node_test_1.mock.fn(async (_i, init) => {
|
|
875
|
+
captured.value = { body: JSON.parse(String(init?.body ?? '{}')) };
|
|
876
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
877
|
+
});
|
|
878
|
+
try {
|
|
879
|
+
const c = new client_js_1.XCiteDBClient({ baseUrl: 'http://127.0.0.1:9', apiKey: 'k' });
|
|
880
|
+
const ok = await c.appendItemByQuery({ match_start: '/products/' }, 42, 'tags', true);
|
|
881
|
+
strict_1.default.equal(ok, true);
|
|
882
|
+
const body = captured.value.body;
|
|
883
|
+
strict_1.default.equal(body.path, 'tags');
|
|
884
|
+
strict_1.default.equal(body.first_match, true);
|
|
885
|
+
strict_1.default.equal(body.mode, 'append');
|
|
886
|
+
strict_1.default.deepEqual(body.value, [42]);
|
|
887
|
+
}
|
|
888
|
+
finally {
|
|
889
|
+
globalThis.fetch = orig;
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
});
|
|
614
893
|
(0, node_test_1.describe)('listTriggerEvents', () => {
|
|
615
894
|
(0, node_test_1.it)('passes limit and parses events', async () => {
|
|
616
895
|
let url = '';
|
package/dist/types.d.ts
CHANGED
|
@@ -569,6 +569,8 @@ export interface DatabaseContext {
|
|
|
569
569
|
export interface WriteDocumentOptions {
|
|
570
570
|
is_top?: boolean;
|
|
571
571
|
compare_attributes?: boolean;
|
|
572
|
+
/** Per-call context override. Merged on top of the client's default context for this request only. */
|
|
573
|
+
context?: Partial<DatabaseContext>;
|
|
572
574
|
}
|
|
573
575
|
/** Supported source formats for `POST /api/v1/documents/import` (server detects from bytes / filename). */
|
|
574
576
|
export type DocumentImportFormat = 'docx' | 'odt' | 'rtf' | 'pdf' | 'txt' | 'md' | 'adoc';
|
|
@@ -118,6 +118,8 @@ wd('user isolation (wet)', () => {
|
|
|
118
118
|
const slug = `js-iso-doc-${suffix}`;
|
|
119
119
|
try {
|
|
120
120
|
await admin.setUserIsolationConfig({ enabled: true, namespace_pattern: '/users/${user.id}' });
|
|
121
|
+
// Default-deny ABAC requires bypass for the platform admin to do data-doc ops in this project.
|
|
122
|
+
await admin.updateSecurityConfig({ developer_bypass: true });
|
|
121
123
|
const u = await admin.createAppUser(email, password, undefined, [
|
|
122
124
|
client_js_1.XCiteDBClient.buildProjectGroup(e.tenantId, 'editor'),
|
|
123
125
|
]);
|
|
@@ -134,6 +136,7 @@ wd('user isolation (wet)', () => {
|
|
|
134
136
|
await admin.deleteAppUser(u.user_id);
|
|
135
137
|
}
|
|
136
138
|
finally {
|
|
139
|
+
await admin.updateSecurityConfig({ developer_bypass: false }).catch(() => { });
|
|
137
140
|
await admin.disableUserIsolation().catch(() => { });
|
|
138
141
|
}
|
|
139
142
|
});
|
|
@@ -153,6 +156,7 @@ wd('user isolation (wet)', () => {
|
|
|
153
156
|
namespace_pattern: '/users/${user.id}',
|
|
154
157
|
shared_read_paths: [sharedPath],
|
|
155
158
|
});
|
|
159
|
+
await admin.updateSecurityConfig({ developer_bypass: true });
|
|
156
160
|
await admin.writeJsonDocument(docPath, { _xcite_json_doc: true, tag: 'shared' });
|
|
157
161
|
const created = await admin.createAppUser(email, password, undefined, [
|
|
158
162
|
client_js_1.XCiteDBClient.buildProjectGroup(e.tenantId, 'editor'),
|
|
@@ -169,6 +173,7 @@ wd('user isolation (wet)', () => {
|
|
|
169
173
|
await admin.deleteAppUser(created.user_id);
|
|
170
174
|
}
|
|
171
175
|
finally {
|
|
176
|
+
await admin.updateSecurityConfig({ developer_bypass: false }).catch(() => { });
|
|
172
177
|
await admin.disableUserIsolation().catch(() => { });
|
|
173
178
|
}
|
|
174
179
|
});
|
|
@@ -198,6 +203,7 @@ wd('user isolation (wet)', () => {
|
|
|
198
203
|
enabled: true,
|
|
199
204
|
namespace_pattern: '/users/${user.id}',
|
|
200
205
|
});
|
|
206
|
+
await admin.updateSecurityConfig({ developer_bypass: true });
|
|
201
207
|
userA = await admin.createAppUser(emailA, password, undefined, [
|
|
202
208
|
client_js_1.XCiteDBClient.buildProjectGroup(e.tenantId, 'editor'),
|
|
203
209
|
]);
|
|
@@ -249,6 +255,7 @@ wd('user isolation (wet)', () => {
|
|
|
249
255
|
await admin.deleteAppUser(userB.user_id).catch(() => { });
|
|
250
256
|
if (userA)
|
|
251
257
|
await admin.deleteAppUser(userA.user_id).catch(() => { });
|
|
258
|
+
await admin.updateSecurityConfig({ developer_bypass: false }).catch(() => { });
|
|
252
259
|
await admin.disableUserIsolation().catch(() => { });
|
|
253
260
|
}
|
|
254
261
|
});
|