@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 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, _options?: WriteDocumentOptions): Promise<void>;
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): Promise<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): Promise<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): Promise<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: unknown, path?: string, opts?: {
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): Promise<T>;
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
- const next = { ...this.defaultContext, ...ctx };
825
- if (ctx.workspace !== undefined) {
826
- next.branch = ctx.workspace;
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
- this.defaultContext = next;
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 prev = { ...this.defaultContext };
2071
- const fromWs = options?.fromBranch ?? prev.branch ?? prev.workspace ?? '';
2072
- let checkpoint;
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
- try {
2075
- await this.createWorkspace(workspaceName, fromWs || undefined, prev.date || undefined);
2076
- this.setContext({ workspace: workspaceName, branch: workspaceName });
2077
- const result = await fn(this);
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, _options) {
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
- async queryMeta(identifier, path = '') {
2417
- return this.request('GET', `/api/v1/meta${buildQuery({ identifier: this.isoPrefixId(identifier), path })}`);
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 queryMetaByQuery(query, path = '') {
2420
- return this.request('POST', '/api/v1/meta/query', { query: this.isoPrefixQuery(query), path });
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 clearMeta(query) {
2423
- const r = await this.request('DELETE', '/api/v1/meta', {
2424
- query: this.isoPrefixQuery(query),
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
- return this.request('POST', '/api/v1/unquery', { query: this.isoPrefixQuery(query), unquery });
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 };
@@ -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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcitedbs/client",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "XCiteDB BaaS client SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",