@sanity-labs/client-fake-for-test 0.5.0 → 0.6.0
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/README.md +15 -7
- package/dist/index.cjs +75 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +92 -15
- package/dist/index.d.ts +92 -15
- package/dist/index.js +75 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -110,6 +110,37 @@ export declare interface CreateTestClientOptions {
|
|
|
110
110
|
* unconditionally — the legacy behavior.
|
|
111
111
|
*/
|
|
112
112
|
accessControl?: TestClientAccessControl;
|
|
113
|
+
/**
|
|
114
|
+
* Token→identity registry, shared by the whole `withConfig` family:
|
|
115
|
+
* multiple identities over one store, keyed by the token a handle is
|
|
116
|
+
* bound to — the token determines identity, exactly like production.
|
|
117
|
+
*
|
|
118
|
+
* ```ts
|
|
119
|
+
* const client = createTestClient({
|
|
120
|
+
* token: "t-admin",
|
|
121
|
+
* identities: {
|
|
122
|
+
* "t-admin": { user: { id: "admin", roles: [{ name: "administrator" }] }, grants: WILDCARD },
|
|
123
|
+
* "t-bob": { user: { id: "bob", roles: [{ name: "editor" }] }, grants: READ_ONLY },
|
|
124
|
+
* },
|
|
125
|
+
* });
|
|
126
|
+
* const bob = client.withConfig({ token: "t-bob" });
|
|
127
|
+
* // same store; bob's /users/me → bob; bob's ACL path → bob's grants
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* A token-bound handle behaves exactly like one configured with
|
|
131
|
+
* `accessControl` derived from its identity (identity serving + grant
|
|
132
|
+
* gating; the ACL path defaults per `(projectId, dataset)`). Register
|
|
133
|
+
* more identities later with `addIdentity`.
|
|
134
|
+
*/
|
|
135
|
+
identities?: Record<string, TestClientIdentity>;
|
|
136
|
+
/**
|
|
137
|
+
* Token the root handle is bound to — must name an entry in
|
|
138
|
+
* `identities` (unknown tokens throw). Omit to leave the root handle
|
|
139
|
+
* identity-free (a bare factory for token-bound siblings): its
|
|
140
|
+
* `request` is undefined and its reads/writes are ungated. Mutually
|
|
141
|
+
* exclusive with `accessControl`.
|
|
142
|
+
*/
|
|
143
|
+
token?: string;
|
|
113
144
|
/**
|
|
114
145
|
* Pluggable write interceptor — runs on each write through the document
|
|
115
146
|
* mutation API (create/patch/delete/transaction; not the release/version
|
|
@@ -659,7 +690,7 @@ export declare const SUPPORTED_SURFACE: readonly [
|
|
|
659
690
|
"releases",
|
|
660
691
|
"createVersion",
|
|
661
692
|
"unpublishVersion",
|
|
662
|
-
"request (only with accessControl)",
|
|
693
|
+
"request (only with accessControl or a bound token)",
|
|
663
694
|
"config",
|
|
664
695
|
"withConfig",
|
|
665
696
|
];
|
|
@@ -685,16 +716,12 @@ export declare interface TestClient {
|
|
|
685
716
|
options?: FetchOptions,
|
|
686
717
|
) => Promise<(T | null)[]>;
|
|
687
718
|
/**
|
|
688
|
-
* Sanity-style HTTP request hook. Present iff the
|
|
689
|
-
* created with `accessControl
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
* Test cases that want to exercise the engine's token-resolution
|
|
695
|
-
* path should configure `accessControl`; cases that want bench-style
|
|
696
|
-
* always-allow should leave it off and pass `access` through the
|
|
697
|
-
* bench.
|
|
719
|
+
* Sanity-style HTTP request hook. Present iff the handle has an
|
|
720
|
+
* acting identity — created with `accessControl`, or bound to a
|
|
721
|
+
* registered token (`identities` + `token`). The engine's access
|
|
722
|
+
* resolver pokes `/users/me` and the configured ACL path through
|
|
723
|
+
* this — without it the engine throws on any verb that needs an
|
|
724
|
+
* actor.
|
|
698
725
|
*/
|
|
699
726
|
request?: <T>(opts: {
|
|
700
727
|
url?: string;
|
|
@@ -752,11 +779,15 @@ export declare interface TestClient {
|
|
|
752
779
|
releaseId: string;
|
|
753
780
|
publishedId: string;
|
|
754
781
|
}) => Promise<ActionResult>;
|
|
755
|
-
/**
|
|
782
|
+
/**
|
|
783
|
+
* Current `(projectId, dataset)` and active perspective. `token` is
|
|
784
|
+
* present iff the handle is bound to a registered identity.
|
|
785
|
+
*/
|
|
756
786
|
config: () => {
|
|
757
787
|
projectId: string;
|
|
758
788
|
dataset: string;
|
|
759
789
|
perspective: Perspective;
|
|
790
|
+
token?: string;
|
|
760
791
|
};
|
|
761
792
|
/**
|
|
762
793
|
* Return a new client handle pointing at a different `(projectId, dataset)`
|
|
@@ -773,8 +804,18 @@ export declare interface TestClient {
|
|
|
773
804
|
* Rebind the acting identity (and its grants) on a sibling that shares
|
|
774
805
|
* the same store — the test analogue of a token-scoped client. Lets two
|
|
775
806
|
* actors write the same dataset, e.g. for `identity()`-based guards.
|
|
807
|
+
* Mutually exclusive with `token`.
|
|
776
808
|
*/
|
|
777
809
|
accessControl: TestClientAccessControl;
|
|
810
|
+
/**
|
|
811
|
+
* Bind the sibling to a token registered in the family's identity
|
|
812
|
+
* registry (`createTestClient({ identities })` / `addIdentity`): its
|
|
813
|
+
* `/users/me` serves that identity's user, its ACL path serves that
|
|
814
|
+
* identity's grants, and reads/writes are gated through them — the
|
|
815
|
+
* token determines identity, exactly like production. Unknown tokens
|
|
816
|
+
* throw. Mutually exclusive with `accessControl`.
|
|
817
|
+
*/
|
|
818
|
+
token: string;
|
|
778
819
|
}>,
|
|
779
820
|
) => TestClient;
|
|
780
821
|
/** Replace the current dataset with the given documents. */
|
|
@@ -791,9 +832,21 @@ export declare interface TestClient {
|
|
|
791
832
|
* Simulate `count` concurrent edits to `id` in the current dataset: the
|
|
792
833
|
* next `count` revision-guarded mutations (those passing `ifRevisionId`)
|
|
793
834
|
* to that document throw a 409-shaped conflict, as if another client
|
|
794
|
-
* committed between the caller's read and write.
|
|
795
|
-
*
|
|
796
|
-
*
|
|
835
|
+
* committed between the caller's read and write. Mutations that don't
|
|
836
|
+
* pass `ifRevisionId` are unaffected.
|
|
837
|
+
*
|
|
838
|
+
* Reach for this when the code under test reads a document's `_rev` and
|
|
839
|
+
* commits with `ifRevisionId` internally — so you can't stage the race
|
|
840
|
+
* by mutating the document first (the code would just re-read the new
|
|
841
|
+
* revision and its commit would match). The counter is consumed one per
|
|
842
|
+
* guarded commit, so `count` lets a test drive a recover-after-N-losses
|
|
843
|
+
* path or exhaust a retry budget, deterministically and without relying
|
|
844
|
+
* on promise interleaving.
|
|
845
|
+
*
|
|
846
|
+
* Note: this forces the conflict *outcome* — it short-circuits the
|
|
847
|
+
* revision comparison rather than changing the stored `_rev`. The code
|
|
848
|
+
* under test sees the identical 409 error a real lost race produces; the
|
|
849
|
+
* stored revision is left untouched.
|
|
797
850
|
*/
|
|
798
851
|
simulateConcurrentEdit: (id: string, count?: number) => void;
|
|
799
852
|
/** List `(projectId, dataset)` pairs known in this registry. */
|
|
@@ -811,6 +864,16 @@ export declare interface TestClient {
|
|
|
811
864
|
stub: (name: string, handler: unknown) => void;
|
|
812
865
|
/** The currently-registered stubs, keyed by member name. */
|
|
813
866
|
stubs: () => Record<string, unknown>;
|
|
867
|
+
/**
|
|
868
|
+
* Register (or upsert) a token→identity entry in the family's shared
|
|
869
|
+
* registry. Siblings already bound to `token` resolve lazily, so a
|
|
870
|
+
* re-registration takes effect on them immediately. Registering an
|
|
871
|
+
* identity does not bind it — pass `token` at `createTestClient` or
|
|
872
|
+
* `withConfig({ token })` to act as it.
|
|
873
|
+
*/
|
|
874
|
+
addIdentity: (token: string, identity: TestClientIdentity) => void;
|
|
875
|
+
/** Snapshot of the registered token→identity entries. */
|
|
876
|
+
identities: () => Record<string, TestClientIdentity>;
|
|
814
877
|
/**
|
|
815
878
|
* Every mutation committed through this client's registry, flattened
|
|
816
879
|
* and in order — `create` / `createOrReplace` / `createIfNotExists` /
|
|
@@ -855,6 +918,20 @@ export declare interface TestClientAccessControl {
|
|
|
855
918
|
aclPath?: string;
|
|
856
919
|
}
|
|
857
920
|
|
|
921
|
+
/**
|
|
922
|
+
* One entry in the token→identity registry (`createTestClient({ identities })`).
|
|
923
|
+
* A handle bound to the entry's token behaves exactly like one configured
|
|
924
|
+
* with `accessControl: { currentUser: user, grants }` — `/users/me` serves
|
|
925
|
+
* `user`, the default ACL path serves `grants`, and reads/writes are gated
|
|
926
|
+
* through them.
|
|
927
|
+
*/
|
|
928
|
+
export declare interface TestClientIdentity {
|
|
929
|
+
/** What `/users/me` returns for clients bound to this token. */
|
|
930
|
+
user: CurrentUser;
|
|
931
|
+
/** Grants served at the ACL path and enforced on reads/writes. */
|
|
932
|
+
grants: Grant[];
|
|
933
|
+
}
|
|
934
|
+
|
|
858
935
|
/**
|
|
859
936
|
* In-memory document store backing a TestClient. Closure-scoped — created
|
|
860
937
|
* fresh per test, no global state.
|
package/dist/index.d.ts
CHANGED
|
@@ -110,6 +110,37 @@ export declare interface CreateTestClientOptions {
|
|
|
110
110
|
* unconditionally — the legacy behavior.
|
|
111
111
|
*/
|
|
112
112
|
accessControl?: TestClientAccessControl;
|
|
113
|
+
/**
|
|
114
|
+
* Token→identity registry, shared by the whole `withConfig` family:
|
|
115
|
+
* multiple identities over one store, keyed by the token a handle is
|
|
116
|
+
* bound to — the token determines identity, exactly like production.
|
|
117
|
+
*
|
|
118
|
+
* ```ts
|
|
119
|
+
* const client = createTestClient({
|
|
120
|
+
* token: "t-admin",
|
|
121
|
+
* identities: {
|
|
122
|
+
* "t-admin": { user: { id: "admin", roles: [{ name: "administrator" }] }, grants: WILDCARD },
|
|
123
|
+
* "t-bob": { user: { id: "bob", roles: [{ name: "editor" }] }, grants: READ_ONLY },
|
|
124
|
+
* },
|
|
125
|
+
* });
|
|
126
|
+
* const bob = client.withConfig({ token: "t-bob" });
|
|
127
|
+
* // same store; bob's /users/me → bob; bob's ACL path → bob's grants
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* A token-bound handle behaves exactly like one configured with
|
|
131
|
+
* `accessControl` derived from its identity (identity serving + grant
|
|
132
|
+
* gating; the ACL path defaults per `(projectId, dataset)`). Register
|
|
133
|
+
* more identities later with `addIdentity`.
|
|
134
|
+
*/
|
|
135
|
+
identities?: Record<string, TestClientIdentity>;
|
|
136
|
+
/**
|
|
137
|
+
* Token the root handle is bound to — must name an entry in
|
|
138
|
+
* `identities` (unknown tokens throw). Omit to leave the root handle
|
|
139
|
+
* identity-free (a bare factory for token-bound siblings): its
|
|
140
|
+
* `request` is undefined and its reads/writes are ungated. Mutually
|
|
141
|
+
* exclusive with `accessControl`.
|
|
142
|
+
*/
|
|
143
|
+
token?: string;
|
|
113
144
|
/**
|
|
114
145
|
* Pluggable write interceptor — runs on each write through the document
|
|
115
146
|
* mutation API (create/patch/delete/transaction; not the release/version
|
|
@@ -659,7 +690,7 @@ export declare const SUPPORTED_SURFACE: readonly [
|
|
|
659
690
|
"releases",
|
|
660
691
|
"createVersion",
|
|
661
692
|
"unpublishVersion",
|
|
662
|
-
"request (only with accessControl)",
|
|
693
|
+
"request (only with accessControl or a bound token)",
|
|
663
694
|
"config",
|
|
664
695
|
"withConfig",
|
|
665
696
|
];
|
|
@@ -685,16 +716,12 @@ export declare interface TestClient {
|
|
|
685
716
|
options?: FetchOptions,
|
|
686
717
|
) => Promise<(T | null)[]>;
|
|
687
718
|
/**
|
|
688
|
-
* Sanity-style HTTP request hook. Present iff the
|
|
689
|
-
* created with `accessControl
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
* Test cases that want to exercise the engine's token-resolution
|
|
695
|
-
* path should configure `accessControl`; cases that want bench-style
|
|
696
|
-
* always-allow should leave it off and pass `access` through the
|
|
697
|
-
* bench.
|
|
719
|
+
* Sanity-style HTTP request hook. Present iff the handle has an
|
|
720
|
+
* acting identity — created with `accessControl`, or bound to a
|
|
721
|
+
* registered token (`identities` + `token`). The engine's access
|
|
722
|
+
* resolver pokes `/users/me` and the configured ACL path through
|
|
723
|
+
* this — without it the engine throws on any verb that needs an
|
|
724
|
+
* actor.
|
|
698
725
|
*/
|
|
699
726
|
request?: <T>(opts: {
|
|
700
727
|
url?: string;
|
|
@@ -752,11 +779,15 @@ export declare interface TestClient {
|
|
|
752
779
|
releaseId: string;
|
|
753
780
|
publishedId: string;
|
|
754
781
|
}) => Promise<ActionResult>;
|
|
755
|
-
/**
|
|
782
|
+
/**
|
|
783
|
+
* Current `(projectId, dataset)` and active perspective. `token` is
|
|
784
|
+
* present iff the handle is bound to a registered identity.
|
|
785
|
+
*/
|
|
756
786
|
config: () => {
|
|
757
787
|
projectId: string;
|
|
758
788
|
dataset: string;
|
|
759
789
|
perspective: Perspective;
|
|
790
|
+
token?: string;
|
|
760
791
|
};
|
|
761
792
|
/**
|
|
762
793
|
* Return a new client handle pointing at a different `(projectId, dataset)`
|
|
@@ -773,8 +804,18 @@ export declare interface TestClient {
|
|
|
773
804
|
* Rebind the acting identity (and its grants) on a sibling that shares
|
|
774
805
|
* the same store — the test analogue of a token-scoped client. Lets two
|
|
775
806
|
* actors write the same dataset, e.g. for `identity()`-based guards.
|
|
807
|
+
* Mutually exclusive with `token`.
|
|
776
808
|
*/
|
|
777
809
|
accessControl: TestClientAccessControl;
|
|
810
|
+
/**
|
|
811
|
+
* Bind the sibling to a token registered in the family's identity
|
|
812
|
+
* registry (`createTestClient({ identities })` / `addIdentity`): its
|
|
813
|
+
* `/users/me` serves that identity's user, its ACL path serves that
|
|
814
|
+
* identity's grants, and reads/writes are gated through them — the
|
|
815
|
+
* token determines identity, exactly like production. Unknown tokens
|
|
816
|
+
* throw. Mutually exclusive with `accessControl`.
|
|
817
|
+
*/
|
|
818
|
+
token: string;
|
|
778
819
|
}>,
|
|
779
820
|
) => TestClient;
|
|
780
821
|
/** Replace the current dataset with the given documents. */
|
|
@@ -791,9 +832,21 @@ export declare interface TestClient {
|
|
|
791
832
|
* Simulate `count` concurrent edits to `id` in the current dataset: the
|
|
792
833
|
* next `count` revision-guarded mutations (those passing `ifRevisionId`)
|
|
793
834
|
* to that document throw a 409-shaped conflict, as if another client
|
|
794
|
-
* committed between the caller's read and write.
|
|
795
|
-
*
|
|
796
|
-
*
|
|
835
|
+
* committed between the caller's read and write. Mutations that don't
|
|
836
|
+
* pass `ifRevisionId` are unaffected.
|
|
837
|
+
*
|
|
838
|
+
* Reach for this when the code under test reads a document's `_rev` and
|
|
839
|
+
* commits with `ifRevisionId` internally — so you can't stage the race
|
|
840
|
+
* by mutating the document first (the code would just re-read the new
|
|
841
|
+
* revision and its commit would match). The counter is consumed one per
|
|
842
|
+
* guarded commit, so `count` lets a test drive a recover-after-N-losses
|
|
843
|
+
* path or exhaust a retry budget, deterministically and without relying
|
|
844
|
+
* on promise interleaving.
|
|
845
|
+
*
|
|
846
|
+
* Note: this forces the conflict *outcome* — it short-circuits the
|
|
847
|
+
* revision comparison rather than changing the stored `_rev`. The code
|
|
848
|
+
* under test sees the identical 409 error a real lost race produces; the
|
|
849
|
+
* stored revision is left untouched.
|
|
797
850
|
*/
|
|
798
851
|
simulateConcurrentEdit: (id: string, count?: number) => void;
|
|
799
852
|
/** List `(projectId, dataset)` pairs known in this registry. */
|
|
@@ -811,6 +864,16 @@ export declare interface TestClient {
|
|
|
811
864
|
stub: (name: string, handler: unknown) => void;
|
|
812
865
|
/** The currently-registered stubs, keyed by member name. */
|
|
813
866
|
stubs: () => Record<string, unknown>;
|
|
867
|
+
/**
|
|
868
|
+
* Register (or upsert) a token→identity entry in the family's shared
|
|
869
|
+
* registry. Siblings already bound to `token` resolve lazily, so a
|
|
870
|
+
* re-registration takes effect on them immediately. Registering an
|
|
871
|
+
* identity does not bind it — pass `token` at `createTestClient` or
|
|
872
|
+
* `withConfig({ token })` to act as it.
|
|
873
|
+
*/
|
|
874
|
+
addIdentity: (token: string, identity: TestClientIdentity) => void;
|
|
875
|
+
/** Snapshot of the registered token→identity entries. */
|
|
876
|
+
identities: () => Record<string, TestClientIdentity>;
|
|
814
877
|
/**
|
|
815
878
|
* Every mutation committed through this client's registry, flattened
|
|
816
879
|
* and in order — `create` / `createOrReplace` / `createIfNotExists` /
|
|
@@ -855,6 +918,20 @@ export declare interface TestClientAccessControl {
|
|
|
855
918
|
aclPath?: string;
|
|
856
919
|
}
|
|
857
920
|
|
|
921
|
+
/**
|
|
922
|
+
* One entry in the token→identity registry (`createTestClient({ identities })`).
|
|
923
|
+
* A handle bound to the entry's token behaves exactly like one configured
|
|
924
|
+
* with `accessControl: { currentUser: user, grants }` — `/users/me` serves
|
|
925
|
+
* `user`, the default ACL path serves `grants`, and reads/writes are gated
|
|
926
|
+
* through them.
|
|
927
|
+
*/
|
|
928
|
+
export declare interface TestClientIdentity {
|
|
929
|
+
/** What `/users/me` returns for clients bound to this token. */
|
|
930
|
+
user: CurrentUser;
|
|
931
|
+
/** Grants served at the ACL path and enforced on reads/writes. */
|
|
932
|
+
grants: Grant[];
|
|
933
|
+
}
|
|
934
|
+
|
|
858
935
|
/**
|
|
859
936
|
* In-memory document store backing a TestClient. Closure-scoped — created
|
|
860
937
|
* fresh per test, no global state.
|
package/dist/index.js
CHANGED
|
@@ -59,16 +59,15 @@ async function enforceWriteAccess(args) {
|
|
|
59
59
|
function defaultAclPath(projectId, dataset) {
|
|
60
60
|
return `/projects/${projectId}/datasets/${dataset}/acl`;
|
|
61
61
|
}
|
|
62
|
-
function makeRequestImpl(
|
|
63
|
-
const aclPath = accessControl.aclPath ?? defaultAclPath(projectId, dataset);
|
|
62
|
+
function makeRequestImpl(getAccess, projectId, dataset) {
|
|
64
63
|
return async (opts) => {
|
|
65
|
-
const path = opts.uri ?? opts.url;
|
|
64
|
+
const accessControl = getAccess(), aclPath = accessControl.aclPath ?? defaultAclPath(projectId, dataset), path = opts.uri ?? opts.url;
|
|
66
65
|
if (path === "/users/me")
|
|
67
66
|
return accessControl.currentUser;
|
|
68
67
|
if (path === aclPath)
|
|
69
68
|
return accessControl.grants;
|
|
70
69
|
throw fakeError(
|
|
71
|
-
`request(): no handler for path "${path}". With
|
|
70
|
+
`request(): no handler for path "${path}". With access control configured, the fake only serves "/users/me" and "${aclPath}". For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
|
|
72
71
|
);
|
|
73
72
|
};
|
|
74
73
|
}
|
|
@@ -688,7 +687,7 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
688
687
|
"releases",
|
|
689
688
|
"createVersion",
|
|
690
689
|
"unpublishVersion",
|
|
691
|
-
"request (only with accessControl)",
|
|
690
|
+
"request (only with accessControl or a bound token)",
|
|
692
691
|
"config",
|
|
693
692
|
"withConfig"
|
|
694
693
|
], UNSUPPORTED_MEMBERS = {
|
|
@@ -737,8 +736,8 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
737
736
|
workaround: "Out of scope \u2014 seed documents directly or stub the call site."
|
|
738
737
|
},
|
|
739
738
|
request: {
|
|
740
|
-
what: "generic HTTP request hook \u2014 only wired up when
|
|
741
|
-
workaround: "Create the client with `accessControl` to enable request() (it serves /users/me + the ACL path)
|
|
739
|
+
what: "generic HTTP request hook \u2014 only wired up when the handle has an acting identity",
|
|
740
|
+
workaround: "Create the client with `accessControl`, or bind a registered token (`identities` + `token` / `withConfig({ token })`), to enable request() (it serves /users/me + the ACL path). Or register a stub."
|
|
742
741
|
},
|
|
743
742
|
dryRun: {
|
|
744
743
|
what: "not a method \u2014 did you mean the `{ dryRun }` mutation option?",
|
|
@@ -854,7 +853,21 @@ const RECORDED_METHODS = /* @__PURE__ */ new Set([
|
|
|
854
853
|
"createVersion",
|
|
855
854
|
"unpublishVersion",
|
|
856
855
|
"request"
|
|
857
|
-
]), DEFAULT_PROJECT_ID = "test", DEFAULT_DATASET = "test"
|
|
856
|
+
]), DEFAULT_PROJECT_ID = "test", DEFAULT_DATASET = "test";
|
|
857
|
+
function requireIdentity(backend, token, site) {
|
|
858
|
+
if (backend.identities.has(token)) return;
|
|
859
|
+
const known = [...backend.identities.keys()];
|
|
860
|
+
throw fakeError(
|
|
861
|
+
`${site}: unknown token "${token}" \u2014 no identity is registered for it. ` + (known.length > 0 ? `Registered tokens: ${known.map((t) => `"${t}"`).join(", ")}. ` : "No identities are registered. ") + "Register identities via createTestClient({ identities }) or client.addIdentity(token, identity)."
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
function rejectAmbiguousIdentity(site, config) {
|
|
865
|
+
if (config.token !== void 0 && config.accessControl !== void 0)
|
|
866
|
+
throw fakeError(
|
|
867
|
+
`${site}: \`token\` and \`accessControl\` are mutually exclusive \u2014 a handle acts either as a registered identity (token) or as an inline accessControl config, not both.`
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
const registryKey = (projectId, dataset) => `${projectId}:${dataset}`, parseRegistryKey = (key) => {
|
|
858
871
|
const [projectId, dataset] = key.split(":");
|
|
859
872
|
return { projectId, dataset };
|
|
860
873
|
};
|
|
@@ -864,8 +877,11 @@ function createTestClient(options = {}) {
|
|
|
864
877
|
log: [],
|
|
865
878
|
stubs: /* @__PURE__ */ new Map(),
|
|
866
879
|
calls: [],
|
|
867
|
-
concurrentEdits: /* @__PURE__ */ new Map()
|
|
868
|
-
|
|
880
|
+
concurrentEdits: /* @__PURE__ */ new Map(),
|
|
881
|
+
identities: new Map(Object.entries(options.identities ?? {}))
|
|
882
|
+
}, registry = backend.registry;
|
|
883
|
+
rejectAmbiguousIdentity("createTestClient()", options), options.token !== void 0 && requireIdentity(backend, options.token, "createTestClient()");
|
|
884
|
+
const ensureStore = (projectId, dataset) => {
|
|
869
885
|
const key = registryKey(projectId, dataset);
|
|
870
886
|
let store = registry.get(key);
|
|
871
887
|
return store === void 0 && (store = createTestStore(), registry.set(key, store)), store;
|
|
@@ -881,7 +897,8 @@ function createTestClient(options = {}) {
|
|
|
881
897
|
defaultDataset,
|
|
882
898
|
options.accessControl,
|
|
883
899
|
options.perspective ?? "raw",
|
|
884
|
-
options.mutationGuard
|
|
900
|
+
options.mutationGuard,
|
|
901
|
+
options.token
|
|
885
902
|
);
|
|
886
903
|
}
|
|
887
904
|
function dedupeIds(results) {
|
|
@@ -934,8 +951,19 @@ function toTxnMutations(operations) {
|
|
|
934
951
|
}
|
|
935
952
|
];
|
|
936
953
|
}
|
|
937
|
-
function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard) {
|
|
938
|
-
const { registry } = backend,
|
|
954
|
+
function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard, token) {
|
|
955
|
+
const { registry } = backend, activeAccess = () => {
|
|
956
|
+
if (token === void 0) return accessControl;
|
|
957
|
+
const identity = backend.identities.get(token);
|
|
958
|
+
if (identity === void 0)
|
|
959
|
+
throw fakeError(`no identity is registered for token "${token}" (was it removed?).`);
|
|
960
|
+
return { currentUser: identity.user, grants: identity.grants };
|
|
961
|
+
}, requireActiveAccess = () => {
|
|
962
|
+
const access = activeAccess();
|
|
963
|
+
if (access === void 0)
|
|
964
|
+
throw fakeError("this handle has no acting identity (no token or accessControl bound).");
|
|
965
|
+
return access;
|
|
966
|
+
}, store = (() => {
|
|
939
967
|
const key = registryKey(projectId, dataset);
|
|
940
968
|
let s = registry.get(key);
|
|
941
969
|
return s === void 0 && (s = createTestStore(), registry.set(key, s)), s;
|
|
@@ -982,20 +1010,24 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
|
|
|
982
1010
|
`ifRevisionId check failed: document "${id}" is at _rev="${existing._rev}" but the mutation required _rev="${ifRevisionId}". The document changed since you read it \u2014 re-read it and retry. (This mirrors Sanity's optimistic-locking 409.)`
|
|
983
1011
|
);
|
|
984
1012
|
}, gate = async (document, permission) => {
|
|
985
|
-
|
|
1013
|
+
const access = activeAccess();
|
|
1014
|
+
access !== void 0 && await enforceWriteAccess({
|
|
986
1015
|
document,
|
|
987
|
-
grants:
|
|
1016
|
+
grants: access.grants,
|
|
988
1017
|
permission,
|
|
989
|
-
userId:
|
|
1018
|
+
userId: access.currentUser.id
|
|
990
1019
|
});
|
|
991
1020
|
}, guardGate = async (action, before, after) => {
|
|
992
|
-
mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity:
|
|
993
|
-
}, canRead = async (document) =>
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1021
|
+
mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: activeAccess()?.currentUser.id });
|
|
1022
|
+
}, canRead = async (document) => {
|
|
1023
|
+
const access = activeAccess();
|
|
1024
|
+
return access === void 0 ? !0 : canAccess({
|
|
1025
|
+
document,
|
|
1026
|
+
grants: access.grants,
|
|
1027
|
+
permission: "read",
|
|
1028
|
+
userId: access.currentUser.id
|
|
1029
|
+
});
|
|
1030
|
+
}, applyPatch = async (documentId, ops, ifRev) => {
|
|
999
1031
|
const existing = store.get(documentId);
|
|
1000
1032
|
if (existing === void 0)
|
|
1001
1033
|
throw fakeError(
|
|
@@ -1071,7 +1103,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
|
|
|
1071
1103
|
);
|
|
1072
1104
|
}, accessVisibleDocs = async () => {
|
|
1073
1105
|
const all = store.all();
|
|
1074
|
-
if (
|
|
1106
|
+
if (activeAccess() === void 0) return all;
|
|
1075
1107
|
const out = [];
|
|
1076
1108
|
for (const doc of all)
|
|
1077
1109
|
await canRead(doc) && out.push(doc);
|
|
@@ -1226,16 +1258,21 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
|
|
|
1226
1258
|
versionId: `versions.${releaseId}.${publishedId}`,
|
|
1227
1259
|
publishedId
|
|
1228
1260
|
}),
|
|
1229
|
-
...accessControl !== void 0 ? { request: makeRequestImpl(
|
|
1230
|
-
config: () => ({ projectId, dataset, perspective }),
|
|
1231
|
-
withConfig: (config) =>
|
|
1232
|
-
backend,
|
|
1233
|
-
config.
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1261
|
+
...token !== void 0 || accessControl !== void 0 ? { request: makeRequestImpl(requireActiveAccess, projectId, dataset) } : {},
|
|
1262
|
+
config: () => ({ projectId, dataset, perspective, ...token !== void 0 ? { token } : {} }),
|
|
1263
|
+
withConfig: (config) => {
|
|
1264
|
+
rejectAmbiguousIdentity("withConfig()", config), config.token !== void 0 && requireIdentity(backend, config.token, "withConfig()");
|
|
1265
|
+
const rebinds = config.token !== void 0 || config.accessControl !== void 0;
|
|
1266
|
+
return makeHandle(
|
|
1267
|
+
backend,
|
|
1268
|
+
config.projectId ?? projectId,
|
|
1269
|
+
config.dataset ?? dataset,
|
|
1270
|
+
rebinds ? config.accessControl : accessControl,
|
|
1271
|
+
config.perspective ?? perspective,
|
|
1272
|
+
mutationGuard,
|
|
1273
|
+
rebinds ? config.token : token
|
|
1274
|
+
);
|
|
1275
|
+
},
|
|
1239
1276
|
seed: (documents) => store.seed(documents),
|
|
1240
1277
|
add: (documents) => store.add(documents),
|
|
1241
1278
|
reset: () => store.reset(),
|
|
@@ -1250,6 +1287,10 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
|
|
|
1250
1287
|
backend.stubs.set(name, handler);
|
|
1251
1288
|
},
|
|
1252
1289
|
stubs: () => Object.fromEntries(backend.stubs),
|
|
1290
|
+
addIdentity: (identityToken, identity) => {
|
|
1291
|
+
backend.identities.set(identityToken, identity);
|
|
1292
|
+
},
|
|
1293
|
+
identities: () => Object.fromEntries(backend.identities),
|
|
1253
1294
|
mutations: () => backend.log.flatMap((entry) => entry.mutations),
|
|
1254
1295
|
transactions: () => [...backend.log],
|
|
1255
1296
|
clearLog: () => {
|