@sanity-labs/client-fake-for-test 0.8.0 → 0.9.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 +29 -5
- package/dist/index.cjs +100 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -13
- package/dist/index.d.ts +79 -13
- package/dist/index.js +100 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,10 +77,10 @@ More patterns — passing the fake to code typed against `SanityClient`, asserti
|
|
|
77
77
|
| Transaction | `transaction()` chain → `.commit()` | Mutations: `.create`, `.createOrReplace`, `.createIfNotExists`, `.delete`, `.patch(id, recipe)`. Atomic — any throw rolls back the whole batch. Default result is a `MultipleMutationResult` (`{ transactionId, documentIds, results }`) |
|
|
78
78
|
| Actions | `action(action \| action[])` | Content Lake release / version actions, applied atomically |
|
|
79
79
|
| Releases | `releases.*`, `createVersion`, `unpublishVersion` | Content Releases lifecycle — create / edit / schedule / publish / archive / delete, plus version create / replace / discard / unpublish |
|
|
80
|
-
| Config | `config()` | Returns `{ projectId, dataset, perspective, token
|
|
81
|
-
| Config | `withConfig({ projectId?, dataset?, perspective?, accessControl?, token? })` | Sibling handle into the same multi-dataset registry; `token` binds a registered identity (see the token→identity registry below), `accessControl` rebinds it inline — the two are mutually exclusive |
|
|
80
|
+
| Config | `config()` | Returns `{ projectId, dataset, perspective, token?, useProjectHostname }` |
|
|
81
|
+
| Config | `withConfig({ projectId?, dataset?, perspective?, accessControl?, token?, useProjectHostname? })` | Sibling handle into the same multi-dataset registry; `token` binds a registered identity (see the token→identity registry below), `accessControl` rebinds it inline — the two are mutually exclusive; `useProjectHostname: false` mints a global-hosted sibling |
|
|
82
82
|
| Perspectives | `raw` / `published` / `drafts` / release stacks | Per-client default or per-`fetch` override; draft/version layering with `_originalId` projection |
|
|
83
|
-
| HTTP | `request({ url? \| uri? })` | Present **only** when the handle has an acting identity (`accessControl`, or a bound `token`). Serves `/users/me`
|
|
83
|
+
| HTTP | `request({ url? \| uri? })` | Present **only** when the handle has an acting identity (`accessControl`, or a bound `token`). Serves `/users/me` (host-aware: project-scoped record on the project host, `globalUser` on a global-hosted handle), the configured ACL path, and `/projects/<projectId>/users/<ids>` (project-user records with `id` + `sanityUserId`). Calling it without one throws a guidance error. |
|
|
84
84
|
|
|
85
85
|
#### Mutation return shapes
|
|
86
86
|
|
|
@@ -117,6 +117,9 @@ await client.patch("article-1").set({ title: "ok" }).commit(); // allowed
|
|
|
117
117
|
await expect(client.patch("article-1").set({ body: "no" }).commit()).rejects.toThrow();
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
> [!NOTE]
|
|
121
|
+
> Treat `before` / `after` as read-only — they are the live store images, not copies; mutating them corrupts the store.
|
|
122
|
+
|
|
120
123
|
#### Identity is the token
|
|
121
124
|
|
|
122
125
|
`identity` is the id of the handle's acting identity. To act as different users against the **same** store (e.g. to exercise `identity()`-based guards), register a token→identity registry and mint token-scoped siblings — the token determines identity, exactly like production:
|
|
@@ -137,8 +140,29 @@ const bob = alice.withConfig({ token: "t-bob" });
|
|
|
137
140
|
|
|
138
141
|
Register more identities after creation with `client.addIdentity(token, { user, grants })` — an upsert into the family-shared registry; already-bound siblings resolve lazily and pick the change up immediately. For a one-off identity without a registry, `withConfig({ accessControl })` still works (mutually exclusive with `token` in the same call).
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
#### Dual-namespace identities: global vs project user ids
|
|
144
|
+
|
|
145
|
+
In production the same token resolves to a **different project-scoped user id in every project** (what lake `identity()` returns) and **one account-global id** (what the global API host's `/users/me` returns); robot tokens carry a single id on every host. An identity can model that split with `globalUser` (served by `/users/me` on a `useProjectHostname: false` handle) and `usersByProject` (per-project records; the handle's project falls back to `user`):
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const client = createTestClient({
|
|
149
|
+
projectId: "alpha",
|
|
150
|
+
token: "t-snorre",
|
|
151
|
+
identities: {
|
|
152
|
+
"t-snorre": {
|
|
153
|
+
user: { id: "pDefault" },
|
|
154
|
+
globalUser: { id: "gSnorre" },
|
|
155
|
+
usersByProject: { alpha: { id: "pAlpha" }, beta: { id: "pBeta" } },
|
|
156
|
+
grants,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
await client.request({ uri: "/users/me" }); // { id: "pAlpha", ... }
|
|
161
|
+
await client.withConfig({ useProjectHostname: false }).request({ uri: "/users/me" }); // { id: "gSnorre", ... }
|
|
162
|
+
await client.request({ uri: "/projects/alpha/users/pAlpha" }); // { id: "pAlpha", sanityUserId: "gSnorre", ... } (ProjectUserRecord)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Grant-filter and mutation-guard `identity()` stay bound to the **project-scoped** record, matching the lake. Identities without `globalUser` serve one record everywhere — the production robot-token shape.
|
|
142
166
|
|
|
143
167
|
### Test-only helpers (not on real `SanityClient`)
|
|
144
168
|
|
package/dist/index.cjs
CHANGED
|
@@ -61,15 +61,62 @@ async function enforceWriteAccess(args) {
|
|
|
61
61
|
function defaultAclPath(projectId, dataset) {
|
|
62
62
|
return `/projects/${projectId}/datasets/${dataset}/acl`;
|
|
63
63
|
}
|
|
64
|
-
function
|
|
64
|
+
function projectUserOf(identity, projectId) {
|
|
65
|
+
return identity.usersByProject?.[projectId] ?? identity.user;
|
|
66
|
+
}
|
|
67
|
+
function serveProjectUsers(args) {
|
|
68
|
+
const { requestedIds, pathProjectId, identities } = args, toRecord = (identity) => {
|
|
69
|
+
const projectUser = projectUserOf(identity, pathProjectId);
|
|
70
|
+
return {
|
|
71
|
+
id: projectUser.id,
|
|
72
|
+
sanityUserId: identity.globalUser?.id ?? projectUser.id,
|
|
73
|
+
displayName: projectUser.name ?? projectUser.id,
|
|
74
|
+
...projectUser.email !== void 0 ? { email: projectUser.email } : {}
|
|
75
|
+
};
|
|
76
|
+
}, byId = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const identity of identities) {
|
|
78
|
+
const resolvedId = projectUserOf(identity, pathProjectId).id, existing = byId.get(resolvedId);
|
|
79
|
+
if (existing !== void 0 && JSON.stringify(toRecord(existing)) !== JSON.stringify(toRecord(identity)))
|
|
80
|
+
throw fakeError(
|
|
81
|
+
`request(): two registered identities resolve to project user "${resolvedId}" in project "${pathProjectId}" with different records. The directory cannot answer unambiguously \u2014 fix the identity registrations so duplicate ids carry identical records.`
|
|
82
|
+
);
|
|
83
|
+
byId.set(resolvedId, identity);
|
|
84
|
+
}
|
|
85
|
+
const ids = requestedIds.split(","), [firstId] = ids;
|
|
86
|
+
if (ids.length === 1 && firstId !== void 0) {
|
|
87
|
+
const identity = byId.get(firstId);
|
|
88
|
+
if (identity === void 0)
|
|
89
|
+
throw Object.assign(
|
|
90
|
+
fakeError(
|
|
91
|
+
`request(): no registered identity resolves to project user "${firstId}" in project "${pathProjectId}". Register it via createTestClient({ identities }) or addIdentity().`
|
|
92
|
+
),
|
|
93
|
+
{ statusCode: 404 }
|
|
94
|
+
);
|
|
95
|
+
return toRecord(identity);
|
|
96
|
+
}
|
|
97
|
+
return ids.flatMap((id) => {
|
|
98
|
+
const identity = byId.get(id);
|
|
99
|
+
return identity === void 0 ? [] : [toRecord(identity)];
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const PROJECT_USERS_PATH = /^\/projects\/([^/]+)\/users\/([^/]+)$/;
|
|
103
|
+
function makeRequestImpl(ctx) {
|
|
104
|
+
const { getAccess, projectId, dataset, useProjectHostname, listIdentities } = ctx;
|
|
65
105
|
return async (opts) => {
|
|
66
106
|
const accessControl = getAccess(), aclPath = accessControl.aclPath ?? defaultAclPath(projectId, dataset), path = opts.uri ?? opts.url;
|
|
67
107
|
if (path === "/users/me")
|
|
68
|
-
return accessControl.currentUser;
|
|
108
|
+
return useProjectHostname ? accessControl.currentUser : accessControl.globalUser ?? accessControl.currentUser;
|
|
69
109
|
if (path === aclPath)
|
|
70
110
|
return accessControl.grants;
|
|
111
|
+
const projectUsers = path?.match(PROJECT_USERS_PATH), [, pathProjectId, requestedIds] = projectUsers ?? [];
|
|
112
|
+
if (pathProjectId !== void 0 && requestedIds !== void 0)
|
|
113
|
+
return serveProjectUsers({
|
|
114
|
+
requestedIds,
|
|
115
|
+
pathProjectId,
|
|
116
|
+
identities: listIdentities()
|
|
117
|
+
});
|
|
71
118
|
throw fakeError(
|
|
72
|
-
`request(): no handler for path "${path}". With access control configured, the fake
|
|
119
|
+
`request(): no handler for path "${path}". With access control configured, the fake serves "/users/me", "${aclPath}", and "/projects/<projectId>/users/<ids>". For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
|
|
73
120
|
);
|
|
74
121
|
};
|
|
75
122
|
}
|
|
@@ -780,7 +827,7 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
780
827
|
},
|
|
781
828
|
request: {
|
|
782
829
|
what: "generic HTTP request hook \u2014 only wired up when the handle has an acting identity",
|
|
783
|
-
workaround: "Create the client with `accessControl`, or bind a registered token (`identities` + `token` / `withConfig({ token })`), to enable request() (it serves /users/me
|
|
830
|
+
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, and /projects/<projectId>/users/<ids>). Or register a stub."
|
|
784
831
|
},
|
|
785
832
|
dryRun: {
|
|
786
833
|
what: "not a method \u2014 did you mean the `{ dryRun }` mutation option?",
|
|
@@ -963,15 +1010,16 @@ function createTestClient(options = {}) {
|
|
|
963
1010
|
const { projectId, dataset } = parseRegistryKey(key);
|
|
964
1011
|
ensureStore(projectId, dataset).seed(docs);
|
|
965
1012
|
}
|
|
966
|
-
return makeHandle(
|
|
1013
|
+
return makeHandle({
|
|
967
1014
|
backend,
|
|
968
|
-
defaultProjectId,
|
|
969
|
-
defaultDataset,
|
|
970
|
-
options.accessControl,
|
|
971
|
-
options.perspective ?? "raw",
|
|
972
|
-
options.mutationGuard,
|
|
973
|
-
options.token
|
|
974
|
-
|
|
1015
|
+
projectId: defaultProjectId,
|
|
1016
|
+
dataset: defaultDataset,
|
|
1017
|
+
accessControl: options.accessControl,
|
|
1018
|
+
perspective: options.perspective ?? "raw",
|
|
1019
|
+
mutationGuard: options.mutationGuard,
|
|
1020
|
+
token: options.token,
|
|
1021
|
+
useProjectHostname: options.useProjectHostname ?? !0
|
|
1022
|
+
});
|
|
975
1023
|
}
|
|
976
1024
|
function dedupeIds(results) {
|
|
977
1025
|
const seen = /* @__PURE__ */ new Set(), out = [];
|
|
@@ -1023,13 +1071,26 @@ function toTxnMutations(operations) {
|
|
|
1023
1071
|
}
|
|
1024
1072
|
];
|
|
1025
1073
|
}
|
|
1026
|
-
function makeHandle(
|
|
1027
|
-
const {
|
|
1074
|
+
function makeHandle(handleArgs) {
|
|
1075
|
+
const {
|
|
1076
|
+
backend,
|
|
1077
|
+
projectId,
|
|
1078
|
+
dataset,
|
|
1079
|
+
accessControl,
|
|
1080
|
+
perspective,
|
|
1081
|
+
mutationGuard,
|
|
1082
|
+
token,
|
|
1083
|
+
useProjectHostname
|
|
1084
|
+
} = handleArgs, { registry } = backend, activeAccess = () => {
|
|
1028
1085
|
if (token === void 0) return accessControl;
|
|
1029
1086
|
const identity = backend.identities.get(token);
|
|
1030
1087
|
if (identity === void 0)
|
|
1031
1088
|
throw fakeError(`no identity is registered for token "${token}" (was it removed?).`);
|
|
1032
|
-
return {
|
|
1089
|
+
return {
|
|
1090
|
+
currentUser: projectUserOf(identity, projectId),
|
|
1091
|
+
...identity.globalUser !== void 0 ? { globalUser: identity.globalUser } : {},
|
|
1092
|
+
grants: identity.grants
|
|
1093
|
+
};
|
|
1033
1094
|
}, requireActiveAccess = () => {
|
|
1034
1095
|
const access = activeAccess();
|
|
1035
1096
|
if (access === void 0)
|
|
@@ -1350,20 +1411,35 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
|
|
|
1350
1411
|
versionId: `versions.${releaseId}.${publishedId}`,
|
|
1351
1412
|
publishedId
|
|
1352
1413
|
}),
|
|
1353
|
-
...token !== void 0 || accessControl !== void 0 ? {
|
|
1354
|
-
|
|
1414
|
+
...token !== void 0 || accessControl !== void 0 ? {
|
|
1415
|
+
request: makeRequestImpl({
|
|
1416
|
+
getAccess: requireActiveAccess,
|
|
1417
|
+
projectId,
|
|
1418
|
+
dataset,
|
|
1419
|
+
useProjectHostname,
|
|
1420
|
+
listIdentities: () => [...backend.identities.values()]
|
|
1421
|
+
})
|
|
1422
|
+
} : {},
|
|
1423
|
+
config: () => ({
|
|
1424
|
+
projectId,
|
|
1425
|
+
dataset,
|
|
1426
|
+
perspective,
|
|
1427
|
+
...token !== void 0 ? { token } : {},
|
|
1428
|
+
useProjectHostname
|
|
1429
|
+
}),
|
|
1355
1430
|
withConfig: (config) => {
|
|
1356
1431
|
rejectAmbiguousIdentity("withConfig()", config), config.token !== void 0 && requireIdentity(backend, config.token, "withConfig()");
|
|
1357
1432
|
const rebinds = config.token !== void 0 || config.accessControl !== void 0;
|
|
1358
|
-
return makeHandle(
|
|
1433
|
+
return makeHandle({
|
|
1359
1434
|
backend,
|
|
1360
|
-
config.projectId ?? projectId,
|
|
1361
|
-
config.dataset ?? dataset,
|
|
1362
|
-
rebinds ? config.accessControl : accessControl,
|
|
1363
|
-
config.perspective ?? perspective,
|
|
1435
|
+
projectId: config.projectId ?? projectId,
|
|
1436
|
+
dataset: config.dataset ?? dataset,
|
|
1437
|
+
accessControl: rebinds ? config.accessControl : accessControl,
|
|
1438
|
+
perspective: config.perspective ?? perspective,
|
|
1364
1439
|
mutationGuard,
|
|
1365
|
-
rebinds ? config.token : token
|
|
1366
|
-
|
|
1440
|
+
token: rebinds ? config.token : token,
|
|
1441
|
+
useProjectHostname: config.useProjectHostname ?? useProjectHostname
|
|
1442
|
+
});
|
|
1367
1443
|
},
|
|
1368
1444
|
seed: (documents) => store.seed(documents),
|
|
1369
1445
|
add: (documents) => store.add(documents),
|