@xcitedbs/client 0.3.8 → 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 +55 -9
- package/dist/client.js +87 -47
- package/dist/client.test.js +279 -0
- package/dist/types.d.ts +10 -0
- package/dist/user-isolation.test.js +19 -1
- 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;
|
|
@@ -541,6 +554,12 @@ export declare class XCiteDBClient {
|
|
|
541
554
|
getGroup(id: string): Promise<AppUserGroup>;
|
|
542
555
|
/** Delete a group (`DELETE /api/v1/security/user-isolation/groups/{id}`). Owner or admin only. */
|
|
543
556
|
deleteGroup(id: string): Promise<void>;
|
|
557
|
+
/**
|
|
558
|
+
* Rename a group (`PATCH /api/v1/security/user-isolation/groups/{id}`). Owner or admin only.
|
|
559
|
+
* Returns the updated group record. The `group_id` is stable across renames, so existing shares
|
|
560
|
+
* targeted at this group continue to work.
|
|
561
|
+
*/
|
|
562
|
+
renameGroup(id: string, name: string): Promise<AppUserGroup>;
|
|
544
563
|
/**
|
|
545
564
|
* Add an app user to a group (`POST /api/v1/security/user-isolation/groups/{id}/members`).
|
|
546
565
|
* Owner or admin only. Returns the updated group record.
|
|
@@ -745,7 +764,7 @@ export declare class XCiteDBClient {
|
|
|
745
764
|
merge?: MergeResult;
|
|
746
765
|
}>;
|
|
747
766
|
/** Send raw XML body (`Content-Type: application/xml`). For JSON wrapper + options use `writeXmlDocument`. */
|
|
748
|
-
writeXML(xml: string,
|
|
767
|
+
writeXML(xml: string, options?: WriteDocumentOptions): Promise<void>;
|
|
749
768
|
/**
|
|
750
769
|
* Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
|
|
751
770
|
* For storing JSON data by key, use `writeJsonDocument`.
|
|
@@ -779,21 +798,27 @@ export declare class XCiteDBClient {
|
|
|
779
798
|
* @deprecated Use {@link writeXmlDocument}. This name was misleading: it writes **XML** via a JSON wrapper, not a JSON document.
|
|
780
799
|
*/
|
|
781
800
|
writeDocumentJson(xml: string, options?: WriteDocumentOptions): Promise<void>;
|
|
782
|
-
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[]>;
|
|
783
804
|
/**
|
|
784
805
|
* Shallow read (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`):
|
|
785
806
|
* root element with inline leaves plus **identifier-bearing stub elements** (`db:stub="true"`) for shredded
|
|
786
807
|
* child slots instead of opaque `db:N*` placeholders. Safe to round-trip with {@link writeXmlDocument}.
|
|
787
808
|
* For sidebar / AST navigation, pair with {@link listIdentifierChildren} (e.g. `Promise.all` of shallow + children).
|
|
788
809
|
*/
|
|
789
|
-
queryByIdentifierShallow(identifier: string, filter?: string, pathFilter?: string
|
|
810
|
+
queryByIdentifierShallow(identifier: string, filter?: string, pathFilter?: string, opts?: {
|
|
811
|
+
context?: Partial<DatabaseContext>;
|
|
812
|
+
}): Promise<string[]>;
|
|
790
813
|
/**
|
|
791
814
|
* Load a document with all shredded children inlined (`FirstMatch,IncludeChildren` on `GET /by-id`).
|
|
792
815
|
* The returned array has **length 0 or 1**; when present, index `0` is the full XML string.
|
|
793
816
|
* Use this for editor round-trips. For navigation without loading the full subtree, use
|
|
794
817
|
* {@link queryByIdentifierShallow} plus {@link listIdentifierChildren}.
|
|
795
818
|
*/
|
|
796
|
-
queryByIdentifierFull(identifier: string, filter?: string, pathFilter?: string
|
|
819
|
+
queryByIdentifierFull(identifier: string, filter?: string, pathFilter?: string, opts?: {
|
|
820
|
+
context?: Partial<DatabaseContext>;
|
|
821
|
+
}): Promise<string[]>;
|
|
797
822
|
/** Alias of {@link listIdentifierChildren} — immediate child segments under `parentPath` (identifier hierarchy). */
|
|
798
823
|
listChildIdentifiers(parentPath?: string): Promise<ListIdentifierChildrenResult>;
|
|
799
824
|
queryDocuments(query: XCiteQuery, flags?: Flags, filter?: string, pathFilter?: string): Promise<string[]>;
|
|
@@ -809,10 +834,12 @@ export declare class XCiteDBClient {
|
|
|
809
834
|
addMeta(identifier: string, value: unknown, path?: string, opts?: {
|
|
810
835
|
mode?: 'set' | 'append' | 'merge_append';
|
|
811
836
|
overwrite?: boolean;
|
|
837
|
+
context?: Partial<DatabaseContext>;
|
|
812
838
|
}): Promise<boolean>;
|
|
813
839
|
addMetaByQuery(query: XCiteQuery, value: unknown, path?: string, firstMatch?: boolean, opts?: {
|
|
814
840
|
mode?: 'set' | 'append' | 'merge_append';
|
|
815
841
|
overwrite?: boolean;
|
|
842
|
+
context?: Partial<DatabaseContext>;
|
|
816
843
|
}): Promise<boolean>;
|
|
817
844
|
/**
|
|
818
845
|
* Strict array-extend: `value` must be an array; the stored target at `path` must be an array
|
|
@@ -822,9 +849,11 @@ export declare class XCiteDBClient {
|
|
|
822
849
|
*/
|
|
823
850
|
appendMeta(identifier: string, value: unknown[], path?: string, opts?: {
|
|
824
851
|
overwrite?: boolean;
|
|
852
|
+
context?: Partial<DatabaseContext>;
|
|
825
853
|
}): Promise<boolean>;
|
|
826
854
|
appendMetaByQuery(query: XCiteQuery, value: unknown[], path?: string, firstMatch?: boolean, opts?: {
|
|
827
855
|
overwrite?: boolean;
|
|
856
|
+
context?: Partial<DatabaseContext>;
|
|
828
857
|
}): Promise<boolean>;
|
|
829
858
|
/**
|
|
830
859
|
* Deep-extend: walks the payload and extends any nested arrays found in storage. Scalars and
|
|
@@ -833,17 +862,31 @@ export declare class XCiteDBClient {
|
|
|
833
862
|
*/
|
|
834
863
|
mergeAppendMeta(identifier: string, value: unknown, path?: string, opts?: {
|
|
835
864
|
overwrite?: boolean;
|
|
865
|
+
context?: Partial<DatabaseContext>;
|
|
836
866
|
}): Promise<boolean>;
|
|
837
867
|
mergeAppendMetaByQuery(query: XCiteQuery, value: unknown, path?: string, firstMatch?: boolean, opts?: {
|
|
838
868
|
overwrite?: boolean;
|
|
869
|
+
context?: Partial<DatabaseContext>;
|
|
839
870
|
}): Promise<boolean>;
|
|
840
871
|
/** Convenience: push a single item. Wraps `item` in `[item]` and calls {@link appendMeta}. */
|
|
841
|
-
appendItem(identifier: string, item:
|
|
872
|
+
appendItem<T = unknown>(identifier: string, item: T, path?: string, opts?: {
|
|
842
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>;
|
|
843
889
|
}): Promise<boolean>;
|
|
844
|
-
queryMeta<T = MetaValue>(identifier: string, path?: string): Promise<T>;
|
|
845
|
-
queryMetaByQuery<T = MetaValue>(query: XCiteQuery, path?: string): Promise<T>;
|
|
846
|
-
clearMeta(query: XCiteQuery): Promise<boolean>;
|
|
847
890
|
/**
|
|
848
891
|
* Acquire a cooperative lock. On exclusive conflict the server returns HTTP 409 with a
|
|
849
892
|
* {@link LockConflictBody} body (thrown as {@link XCiteDBError} with `.status === 409`).
|
|
@@ -896,7 +939,10 @@ export declare class XCiteDBClient {
|
|
|
896
939
|
* );
|
|
897
940
|
* ```
|
|
898
941
|
*/
|
|
899
|
-
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>;
|
|
900
946
|
search(q: TextSearchQuery): Promise<TextSearchResult>;
|
|
901
947
|
reindex(): Promise<{
|
|
902
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
|
};
|
|
@@ -1773,6 +1805,14 @@ class XCiteDBClient {
|
|
|
1773
1805
|
async deleteGroup(id) {
|
|
1774
1806
|
await this.request('DELETE', `/api/v1/security/user-isolation/groups/${encodeURIComponent(id)}`);
|
|
1775
1807
|
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Rename a group (`PATCH /api/v1/security/user-isolation/groups/{id}`). Owner or admin only.
|
|
1810
|
+
* Returns the updated group record. The `group_id` is stable across renames, so existing shares
|
|
1811
|
+
* targeted at this group continue to work.
|
|
1812
|
+
*/
|
|
1813
|
+
async renameGroup(id, name) {
|
|
1814
|
+
return this.request('PATCH', `/api/v1/security/user-isolation/groups/${encodeURIComponent(id)}`, { name });
|
|
1815
|
+
}
|
|
1776
1816
|
/**
|
|
1777
1817
|
* Add an app user to a group (`POST /api/v1/security/user-isolation/groups/{id}/members`).
|
|
1778
1818
|
* Owner or admin only. Returns the updated group record.
|
|
@@ -2059,25 +2099,18 @@ class XCiteDBClient {
|
|
|
2059
2099
|
* create a checkpoint, then publish back unless {@link options.autoMerge} is `false`.
|
|
2060
2100
|
*/
|
|
2061
2101
|
async withWorkspace(workspaceName, fn, options) {
|
|
2062
|
-
const
|
|
2063
|
-
|
|
2064
|
-
|
|
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);
|
|
2065
2107
|
let publish;
|
|
2066
|
-
|
|
2067
|
-
await
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
checkpoint = await this.createCheckpoint(options?.message ?? `Workspace ${workspaceName}`, options?.author);
|
|
2071
|
-
if (options?.autoMerge !== false) {
|
|
2072
|
-
publish = await this.publishWorkspace(fromWs, workspaceName, {
|
|
2073
|
-
message: options?.message,
|
|
2074
|
-
});
|
|
2075
|
-
}
|
|
2076
|
-
return { result, checkpoint, publish };
|
|
2077
|
-
}
|
|
2078
|
-
finally {
|
|
2079
|
-
this.setContext(prev);
|
|
2108
|
+
if (options?.autoMerge !== false) {
|
|
2109
|
+
publish = await child.publishWorkspace(fromWs, workspaceName, {
|
|
2110
|
+
message: options?.message,
|
|
2111
|
+
});
|
|
2080
2112
|
}
|
|
2113
|
+
return { result, checkpoint, publish };
|
|
2081
2114
|
}
|
|
2082
2115
|
/** @deprecated Use {@link withWorkspace}. */
|
|
2083
2116
|
async withBranch(branchName, fn, options) {
|
|
@@ -2085,11 +2118,9 @@ class XCiteDBClient {
|
|
|
2085
2118
|
return { result: r.result, commit: r.checkpoint, merge: r.publish };
|
|
2086
2119
|
}
|
|
2087
2120
|
/** Send raw XML body (`Content-Type: application/xml`). For JSON wrapper + options use `writeXmlDocument`. */
|
|
2088
|
-
async writeXML(xml,
|
|
2121
|
+
async writeXML(xml, options) {
|
|
2089
2122
|
const payload = this.isoApplyXmlDbIdentifier(xml);
|
|
2090
|
-
await this.request('POST', '/api/v1/documents', payload, {
|
|
2091
|
-
'Content-Type': 'application/xml',
|
|
2092
|
-
});
|
|
2123
|
+
await this.request('POST', '/api/v1/documents', payload, { 'Content-Type': 'application/xml' }, { contextOverride: options?.context });
|
|
2093
2124
|
}
|
|
2094
2125
|
/**
|
|
2095
2126
|
* Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
|
|
@@ -2106,7 +2137,7 @@ class XCiteDBClient {
|
|
|
2106
2137
|
xml: this.isoApplyXmlDbIdentifier(xml),
|
|
2107
2138
|
is_top: options?.is_top ?? true,
|
|
2108
2139
|
compare_attributes: options?.compare_attributes ?? false,
|
|
2109
|
-
});
|
|
2140
|
+
}, undefined, { contextOverride: options?.context });
|
|
2110
2141
|
}
|
|
2111
2142
|
/**
|
|
2112
2143
|
* Best-effort batch XML writes (`POST /api/v1/documents/batch`). Each item is independent; check `results[].ok`.
|
|
@@ -2186,14 +2217,16 @@ class XCiteDBClient {
|
|
|
2186
2217
|
async writeDocumentJson(xml, options) {
|
|
2187
2218
|
return this.writeXmlDocument(xml, options);
|
|
2188
2219
|
}
|
|
2189
|
-
async queryByIdentifier(identifier, flags, filter, pathFilter) {
|
|
2220
|
+
async queryByIdentifier(identifier, flags, filter, pathFilter, opts) {
|
|
2190
2221
|
const q = buildQuery({
|
|
2191
2222
|
identifier: this.isoPrefixId(identifier),
|
|
2192
2223
|
flags: flags,
|
|
2193
2224
|
filter,
|
|
2194
2225
|
path_filter: pathFilter,
|
|
2195
2226
|
});
|
|
2196
|
-
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
|
+
});
|
|
2197
2230
|
}
|
|
2198
2231
|
/**
|
|
2199
2232
|
* Shallow read (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`):
|
|
@@ -2201,8 +2234,8 @@ class XCiteDBClient {
|
|
|
2201
2234
|
* child slots instead of opaque `db:N*` placeholders. Safe to round-trip with {@link writeXmlDocument}.
|
|
2202
2235
|
* For sidebar / AST navigation, pair with {@link listIdentifierChildren} (e.g. `Promise.all` of shallow + children).
|
|
2203
2236
|
*/
|
|
2204
|
-
async queryByIdentifierShallow(identifier, filter, pathFilter) {
|
|
2205
|
-
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);
|
|
2206
2239
|
}
|
|
2207
2240
|
/**
|
|
2208
2241
|
* Load a document with all shredded children inlined (`FirstMatch,IncludeChildren` on `GET /by-id`).
|
|
@@ -2210,8 +2243,8 @@ class XCiteDBClient {
|
|
|
2210
2243
|
* Use this for editor round-trips. For navigation without loading the full subtree, use
|
|
2211
2244
|
* {@link queryByIdentifierShallow} plus {@link listIdentifierChildren}.
|
|
2212
2245
|
*/
|
|
2213
|
-
async queryByIdentifierFull(identifier, filter, pathFilter) {
|
|
2214
|
-
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);
|
|
2215
2248
|
}
|
|
2216
2249
|
/** Alias of {@link listIdentifierChildren} — immediate child segments under `parentPath` (identifier hierarchy). */
|
|
2217
2250
|
async listChildIdentifiers(parentPath) {
|
|
@@ -2361,7 +2394,9 @@ class XCiteDBClient {
|
|
|
2361
2394
|
body.mode = opts.mode;
|
|
2362
2395
|
if (opts?.overwrite)
|
|
2363
2396
|
body.overwrite = true;
|
|
2364
|
-
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
|
+
});
|
|
2365
2400
|
return r?.ok !== false;
|
|
2366
2401
|
}
|
|
2367
2402
|
async addMetaByQuery(query, value, path = '', firstMatch = false, opts) {
|
|
@@ -2375,7 +2410,9 @@ class XCiteDBClient {
|
|
|
2375
2410
|
body.mode = opts.mode;
|
|
2376
2411
|
if (opts?.overwrite)
|
|
2377
2412
|
body.overwrite = true;
|
|
2378
|
-
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
|
+
});
|
|
2379
2416
|
return r?.ok !== false;
|
|
2380
2417
|
}
|
|
2381
2418
|
/**
|
|
@@ -2405,16 +2442,18 @@ class XCiteDBClient {
|
|
|
2405
2442
|
async appendItem(identifier, item, path = '', opts) {
|
|
2406
2443
|
return this.appendMeta(identifier, [item], path, opts);
|
|
2407
2444
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
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);
|
|
2410
2448
|
}
|
|
2411
|
-
async
|
|
2412
|
-
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 });
|
|
2413
2451
|
}
|
|
2414
|
-
async
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
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 });
|
|
2418
2457
|
return r?.ok !== false;
|
|
2419
2458
|
}
|
|
2420
2459
|
/**
|
|
@@ -2507,8 +2546,9 @@ class XCiteDBClient {
|
|
|
2507
2546
|
* );
|
|
2508
2547
|
* ```
|
|
2509
2548
|
*/
|
|
2510
|
-
async unquery(query, unquery) {
|
|
2511
|
-
|
|
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 });
|
|
2512
2552
|
}
|
|
2513
2553
|
async search(q) {
|
|
2514
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';
|
|
@@ -699,6 +701,14 @@ export interface UserIsolationShareResult {
|
|
|
699
701
|
export interface UserIsolationShareListEntry {
|
|
700
702
|
identifier: string;
|
|
701
703
|
mode: UserIsolationShareMode;
|
|
704
|
+
/** App user id of the user who created this share, parsed from the alias path. */
|
|
705
|
+
granter_user_id?: string;
|
|
706
|
+
/** Display name of the granter, looked up at list time. */
|
|
707
|
+
granter_display_name?: string;
|
|
708
|
+
/** Email of the granter, looked up at list time. */
|
|
709
|
+
granter_email?: string;
|
|
710
|
+
/** Epoch seconds. Omitted for shares created before share-meta storage existed. */
|
|
711
|
+
created_at?: number;
|
|
702
712
|
}
|
|
703
713
|
/** Response from `GET /api/v1/security/user-isolation/shares?direction=…`. */
|
|
704
714
|
export interface UserIsolationShareListResponse {
|
|
@@ -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
|
});
|
|
@@ -310,6 +317,13 @@ wd('app-user groups + group-aware shares (wet)', () => {
|
|
|
310
317
|
const status = err.status;
|
|
311
318
|
return status === 403;
|
|
312
319
|
}, 'non-owner deleteGroup must be rejected with 403');
|
|
320
|
+
// Rename: owner can; non-owner cannot.
|
|
321
|
+
const newName = `g-${suffix}-renamed`;
|
|
322
|
+
const renamed = await ownerClient.renameGroup(groupId, newName);
|
|
323
|
+
strict_1.default.equal(renamed.name, newName);
|
|
324
|
+
strict_1.default.equal(renamed.group_id, groupId, 'group_id must be stable across rename');
|
|
325
|
+
strict_1.default.ok(renamed.updated_at >= created.updated_at, 'updated_at should advance on rename');
|
|
326
|
+
await strict_1.default.rejects(() => memberClient.renameGroup(groupId, `g-${suffix}-hijack`), (err) => err.status === 403, 'non-owner renameGroup must be rejected with 403');
|
|
313
327
|
await ownerClient.removeGroupMember(groupId, member.user_id);
|
|
314
328
|
const afterRemove = await ownerClient.getGroup(groupId);
|
|
315
329
|
strict_1.default.ok(!afterRemove.member_ids.includes(member.user_id), 'member_ids should not include removed user');
|
|
@@ -378,7 +392,11 @@ wd('app-user groups + group-aware shares (wet)', () => {
|
|
|
378
392
|
});
|
|
379
393
|
await memberClient.loginAppUser(memberEmail, password);
|
|
380
394
|
const incoming = await memberClient.listUserIsolationShares({ direction: 'incoming' });
|
|
381
|
-
|
|
395
|
+
const matched = incoming.identifiers.find((row) => row.identifier === share.alias);
|
|
396
|
+
strict_1.default.ok(matched, `incoming list should include alias ${share.alias}; got ${JSON.stringify(incoming.identifiers)}`);
|
|
397
|
+
strict_1.default.equal(matched.granter_user_id, owner.user_id, 'granter_user_id should match the sharer');
|
|
398
|
+
strict_1.default.equal(matched.granter_email, ownerEmail, 'granter_email should be the sharer email');
|
|
399
|
+
strict_1.default.ok(typeof matched.created_at === 'number' && matched.created_at > 0, `created_at should be a positive number; got ${matched.created_at}`);
|
|
382
400
|
const doc = await memberClient.readJsonDocument(share.alias);
|
|
383
401
|
strict_1.default.equal(doc.who, 'owner');
|
|
384
402
|
strict_1.default.equal(doc.v, 1);
|