@sanity-labs/client-fake-for-test 0.10.0 → 0.12.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 +14 -5
- package/dist/index.cjs +117 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -60
- package/dist/index.d.ts +70 -60
- package/dist/index.js +117 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ More patterns — passing the fake to code typed against `SanityClient`, asserti
|
|
|
80
80
|
| Config | `config()` | Returns `{ projectId, dataset, perspective, token?, useProjectHostname }` |
|
|
81
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` (host-aware: project-scoped record on the project host, `globalUser` on a global-hosted handle), the configured ACL path, `/projects/<projectId>` (the project's members: `{ id, isRobot? }` rows, one per distinct resolved project user), and `/projects/<projectId>/users/<ids>` (project-user records with `id` + `sanityUserId`). Calling it without one throws a guidance error. |
|
|
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, `/projects/<projectId>` (the project's members: `{ id, isRobot?, roles? }` rows, one per distinct resolved project user), 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
|
|
|
@@ -98,9 +98,9 @@ More patterns — passing the fake to code typed against `SanityClient`, asserti
|
|
|
98
98
|
|
|
99
99
|
### Gating writes — `mutationGuard`
|
|
100
100
|
|
|
101
|
-
`createTestClient({ mutationGuard })` installs a pluggable write interceptor. It runs
|
|
101
|
+
`createTestClient({ mutationGuard })` installs a pluggable write interceptor. It runs once for each document's net change in a commit through the document mutation API — `create` / `createOrReplace` / `createIfNotExists` / `patch` / `delete` and the equivalent `transaction()` / `mutate()` operations. Document publish and unpublish actions are expanded into their published and draft mutations. Release and version actions (`releases.*`, `createVersion`, and `unpublishVersion`) are not intercepted.
|
|
102
102
|
|
|
103
|
-
The client owns the _seam_; the caller owns the _policy_. This is the hook a workflow test bench uses to enforce Content Lake **mutation guards** at the write path without re-implementing guard evaluation. Each call receives the
|
|
103
|
+
The client owns the _seam_; the caller owns the _policy_. This is the hook a workflow test bench uses to enforce Content Lake **mutation guards** at the write path without re-implementing guard evaluation. Each call receives the original/final image (`before` is `null` on create, `after` is `null` on delete), the `action`, and the acting `identity`. Return every denying guard as `{ guardID, name? }`; the fake evaluates every net document mutation, aggregates the returned denials, deduplicates them by `guardID` in first-observed order, rolls back the commit, and throws a 403-shaped `GuardDeniedError` with `type: "guardDeniedError"` and `denials`. Other thrown callback errors propagate unchanged.
|
|
104
104
|
|
|
105
105
|
```ts
|
|
106
106
|
const client = createTestClient({
|
|
@@ -108,13 +108,16 @@ const client = createTestClient({
|
|
|
108
108
|
mutationGuard: ({ action, before, after }) => {
|
|
109
109
|
// Freeze `body` once an article is locked.
|
|
110
110
|
if (action === "update" && before?.locked && before.body !== after?.body) {
|
|
111
|
-
|
|
111
|
+
return [{ guardID: "_.guards.article-body", name: "Locked article body" }];
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
await client.patch("article-1").set({ title: "ok" }).commit(); // allowed
|
|
117
|
-
await expect(client.patch("article-1").set({ body: "no" }).commit()).rejects.
|
|
117
|
+
await expect(client.patch("article-1").set({ body: "no" }).commit()).rejects.toMatchObject({
|
|
118
|
+
statusCode: 403,
|
|
119
|
+
type: "guardDeniedError",
|
|
120
|
+
});
|
|
118
121
|
```
|
|
119
122
|
|
|
120
123
|
> [!NOTE]
|
|
@@ -140,6 +143,10 @@ const bob = alice.withConfig({ token: "t-bob" });
|
|
|
140
143
|
|
|
141
144
|
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).
|
|
142
145
|
|
|
146
|
+
#### System-document grants
|
|
147
|
+
|
|
148
|
+
User-issued writes to system document IDs beginning with `_.` require the `manage` permission. Ordinary `create` and `update` grants do not authorize these writes; add `manage` to identities whose tests intentionally create, replace, patch, delete, transact, mutate, publish, or unpublish system documents. The fake's internal release/version bookkeeping remains outside access-control interception.
|
|
149
|
+
|
|
143
150
|
#### Dual-namespace identities: global vs project user ids
|
|
144
151
|
|
|
145
152
|
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`):
|
|
@@ -162,6 +169,8 @@ await client.withConfig({ useProjectHostname: false }).request({ uri: "/users/me
|
|
|
162
169
|
await client.request({ uri: "/projects/alpha/users/pAlpha" }); // { id: "pAlpha", sanityUserId: "gSnorre", ... } (ProjectUserRecord)
|
|
163
170
|
```
|
|
164
171
|
|
|
172
|
+
The project directory endpoint (`/projects/<projectId>`) returns each resolved project member's `id`, project-specific `roles`, and robot marker. Roles are served in canonical order, and duplicate-token agreement ignores role and property order. Registering two tokens with the same project user id is supported when their served member records agree; conflicting roles fail the request instead of making the result depend on registration order.
|
|
173
|
+
|
|
165
174
|
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.
|
|
166
175
|
|
|
167
176
|
### Test-only helpers (not on real `SanityClient`)
|
package/dist/index.cjs
CHANGED
|
@@ -64,6 +64,28 @@ function defaultAclPath(projectId, dataset) {
|
|
|
64
64
|
function projectUserOf(identity, projectId) {
|
|
65
65
|
return identity.usersByProject?.[projectId] ?? identity.user;
|
|
66
66
|
}
|
|
67
|
+
function consistentProjectRecords(args) {
|
|
68
|
+
const byId = /* @__PURE__ */ new Map();
|
|
69
|
+
for (const record of args.records) {
|
|
70
|
+
const existing = byId.get(record.id);
|
|
71
|
+
if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(record))
|
|
72
|
+
throw fakeError(
|
|
73
|
+
`request(): two registered identities resolve to project user "${record.id}" in project "${args.projectId}" with different records. The project records cannot be served unambiguously \u2014 fix the identity registrations so duplicate ids carry identical records.`
|
|
74
|
+
);
|
|
75
|
+
byId.set(record.id, record);
|
|
76
|
+
}
|
|
77
|
+
return byId;
|
|
78
|
+
}
|
|
79
|
+
function canonicalProjectRoles(roles) {
|
|
80
|
+
return roles.map((role) => ({
|
|
81
|
+
name: role.name,
|
|
82
|
+
...role.title !== void 0 ? { title: role.title } : {},
|
|
83
|
+
...role.description !== void 0 ? { description: role.description } : {}
|
|
84
|
+
})).toSorted((left, right) => {
|
|
85
|
+
const leftKey = JSON.stringify(left), rightKey = JSON.stringify(right);
|
|
86
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
67
89
|
function serveProjectUsers(args) {
|
|
68
90
|
const { requestedIds, pathProjectId, identities } = args, toRecord = (identity) => {
|
|
69
91
|
const projectUser = projectUserOf(identity, pathProjectId);
|
|
@@ -73,43 +95,37 @@ function serveProjectUsers(args) {
|
|
|
73
95
|
displayName: projectUser.name ?? projectUser.id,
|
|
74
96
|
...projectUser.email !== void 0 ? { email: projectUser.email } : {}
|
|
75
97
|
};
|
|
76
|
-
}, byId =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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;
|
|
98
|
+
}, byId = consistentProjectRecords({
|
|
99
|
+
projectId: pathProjectId,
|
|
100
|
+
records: identities.map(toRecord)
|
|
101
|
+
}), ids = requestedIds.split(","), [firstId] = ids;
|
|
86
102
|
if (ids.length === 1 && firstId !== void 0) {
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
103
|
+
const record = byId.get(firstId);
|
|
104
|
+
if (record === void 0)
|
|
89
105
|
throw Object.assign(
|
|
90
106
|
fakeError(
|
|
91
107
|
`request(): no registered identity resolves to project user "${firstId}" in project "${pathProjectId}". Register it via createTestClient({ identities }) or addIdentity().`
|
|
92
108
|
),
|
|
93
109
|
{ statusCode: 404 }
|
|
94
110
|
);
|
|
95
|
-
return
|
|
111
|
+
return record;
|
|
96
112
|
}
|
|
97
113
|
return ids.flatMap((id) => {
|
|
98
|
-
const
|
|
99
|
-
return
|
|
114
|
+
const record = byId.get(id);
|
|
115
|
+
return record === void 0 ? [] : [record];
|
|
100
116
|
});
|
|
101
117
|
}
|
|
102
118
|
const PROJECT_USERS_PATH = /^\/projects\/([^/]+)\/users\/([^/]+)$/, PROJECT_PATH = /^\/projects\/([^/]+)$/;
|
|
103
119
|
function serveProject(args) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
};
|
|
120
|
+
const records = args.identities.map((identity) => {
|
|
121
|
+
const projectUser = projectUserOf(identity, args.pathProjectId);
|
|
122
|
+
return {
|
|
123
|
+
id: projectUser.id,
|
|
124
|
+
...projectUser.id.startsWith("p-") ? { isRobot: !0 } : {},
|
|
125
|
+
...projectUser.roles !== void 0 ? { roles: canonicalProjectRoles(projectUser.roles) } : {}
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
return { members: [...consistentProjectRecords({ projectId: args.pathProjectId, records }).values()] };
|
|
113
129
|
}
|
|
114
130
|
function makeRequestImpl(ctx) {
|
|
115
131
|
const { getAccess, projectId, dataset, useProjectHostname, listIdentities } = ctx;
|
|
@@ -137,6 +153,48 @@ function makeRequestImpl(ctx) {
|
|
|
137
153
|
);
|
|
138
154
|
};
|
|
139
155
|
}
|
|
156
|
+
class GuardDeniedError extends Error {
|
|
157
|
+
statusCode = 403;
|
|
158
|
+
type = "guardDeniedError";
|
|
159
|
+
denials;
|
|
160
|
+
details;
|
|
161
|
+
response;
|
|
162
|
+
constructor(denials) {
|
|
163
|
+
const unique = uniqueDenials(denials);
|
|
164
|
+
super(`Mutation denied by guard(s) [${unique.map((denial) => denial.guardID).join(", ")}]`), this.name = "GuardDeniedError", this.denials = unique, this.details = { type: this.type, denials: unique }, this.response = {
|
|
165
|
+
body: { error: this.details },
|
|
166
|
+
url: "memory://client-fake-for-test/data/mutate",
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: {},
|
|
169
|
+
statusCode: this.statusCode,
|
|
170
|
+
statusMessage: "Forbidden"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function uniqueDenials(denials) {
|
|
175
|
+
const seen = /* @__PURE__ */ new Set();
|
|
176
|
+
return denials.filter((denial) => seen.has(denial.guardID) ? !1 : (seen.add(denial.guardID), !0));
|
|
177
|
+
}
|
|
178
|
+
function mutationContext(images) {
|
|
179
|
+
const { before, after } = images;
|
|
180
|
+
if (before !== after)
|
|
181
|
+
return before === void 0 ? { action: "create", before: null, after: after ?? null } : after === void 0 ? { action: "delete", before, after: null } : { action: "update", before, after };
|
|
182
|
+
}
|
|
183
|
+
function mutationContexts(images) {
|
|
184
|
+
return [...images].flatMap((image) => {
|
|
185
|
+
const context = mutationContext(image);
|
|
186
|
+
return context === void 0 ? [] : [context];
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async function enforceMutationGuards(guard, contexts) {
|
|
190
|
+
if (guard === void 0) return;
|
|
191
|
+
const denials = [];
|
|
192
|
+
for (const context of contexts) {
|
|
193
|
+
const denied = await guard(context);
|
|
194
|
+
denied !== void 0 && denials.push(...denied);
|
|
195
|
+
}
|
|
196
|
+
if (denials.length > 0) throw new GuardDeniedError(denials);
|
|
197
|
+
}
|
|
140
198
|
function describeType(value) {
|
|
141
199
|
return value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`;
|
|
142
200
|
}
|
|
@@ -670,7 +728,7 @@ async function applyAction(store, action, hooks) {
|
|
|
670
728
|
const published = store.get(action.publishedId);
|
|
671
729
|
requireRevisionMatch(action.draftId, draft._rev, action.ifDraftRevisionId), requireRevisionMatch(action.publishedId, published?._rev, action.ifPublishedRevisionId);
|
|
672
730
|
const promoted = promote({ ...draft, _id: action.publishedId });
|
|
673
|
-
return published !== void 0 && (promoted._createdAt = published._createdAt), await hooks?.gate?.(promoted, published === void 0 ? "create" : "update"),
|
|
731
|
+
return published !== void 0 && (promoted._createdAt = published._createdAt), await hooks?.gate?.(promoted, published === void 0 ? "create" : "update"), store.put(promoted), store.remove(action.draftId), { transactionId: cryptoRandomKey() };
|
|
674
732
|
}
|
|
675
733
|
case "sanity.action.document.unpublish": {
|
|
676
734
|
requireDraftOf(action);
|
|
@@ -679,7 +737,7 @@ async function applyAction(store, action, hooks) {
|
|
|
679
737
|
throw fakeError(
|
|
680
738
|
`Cannot unpublish "${action.publishedId}": no published document with that _id exists. document.unpublish retracts a live published document into "${action.draftId}".`
|
|
681
739
|
);
|
|
682
|
-
if (await hooks?.gate?.(published, "update"),
|
|
740
|
+
if (await hooks?.gate?.(published, "update"), store.get(action.draftId) === void 0) {
|
|
683
741
|
const draft = promote({ ...published, _id: action.draftId });
|
|
684
742
|
draft._createdAt = published._createdAt, store.put(draft);
|
|
685
743
|
}
|
|
@@ -700,7 +758,7 @@ function requireRevisionMatch(documentId, actualRev, expectedRev) {
|
|
|
700
758
|
);
|
|
701
759
|
}
|
|
702
760
|
async function applyActions(store, actions, hooks) {
|
|
703
|
-
const list = Array.isArray(actions) ? actions : [actions], snapshot = /* @__PURE__ */ new Map(), remember = (id) => {
|
|
761
|
+
const list = Array.isArray(actions) ? actions : [actions], snapshot = /* @__PURE__ */ new Map(), guardedIds = /* @__PURE__ */ new Set(), remember = (id) => {
|
|
704
762
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
705
763
|
};
|
|
706
764
|
try {
|
|
@@ -736,13 +794,18 @@ async function applyActions(store, actions, hooks) {
|
|
|
736
794
|
remember(a.versionId);
|
|
737
795
|
break;
|
|
738
796
|
case "sanity.action.document.publish":
|
|
797
|
+
remember(a.draftId), remember(a.publishedId), guardedIds.add(a.publishedId), guardedIds.add(a.draftId);
|
|
798
|
+
break;
|
|
739
799
|
case "sanity.action.document.unpublish":
|
|
740
|
-
remember(a.draftId), remember(a.publishedId);
|
|
800
|
+
remember(a.draftId), remember(a.publishedId), guardedIds.add(a.draftId), guardedIds.add(a.publishedId);
|
|
741
801
|
break;
|
|
742
802
|
}
|
|
743
803
|
last = await applyAction(store, a, hooks);
|
|
744
804
|
}
|
|
745
|
-
|
|
805
|
+
const contexts = mutationContexts(
|
|
806
|
+
[...guardedIds].map((id) => ({ before: snapshot.get(id), after: store.get(id) }))
|
|
807
|
+
);
|
|
808
|
+
return await hooks?.guard?.(contexts), last;
|
|
746
809
|
} catch (err) {
|
|
747
810
|
for (const [id, doc] of snapshot)
|
|
748
811
|
doc === void 0 ? store.remove(id) : store.put(doc);
|
|
@@ -1173,11 +1236,17 @@ function makeHandle(handleArgs) {
|
|
|
1173
1236
|
access !== void 0 && await enforceWriteAccess({
|
|
1174
1237
|
document,
|
|
1175
1238
|
grants: access.grants,
|
|
1176
|
-
permission,
|
|
1239
|
+
permission: document._id !== void 0 && isSystemId(document._id) ? "manage" : permission,
|
|
1177
1240
|
userId: access.currentUser.id
|
|
1178
1241
|
});
|
|
1179
|
-
}, guardGate = async (
|
|
1180
|
-
|
|
1242
|
+
}, guardGate = async (contexts) => {
|
|
1243
|
+
const identity = activeAccess()?.currentUser.id;
|
|
1244
|
+
await enforceMutationGuards(
|
|
1245
|
+
mutationGuard,
|
|
1246
|
+
contexts.map((context) => ({ ...context, identity }))
|
|
1247
|
+
);
|
|
1248
|
+
}, guardOne = async (action, before, after) => {
|
|
1249
|
+
await guardGate([{ action, before, after }]);
|
|
1181
1250
|
}, canRead = async (document) => {
|
|
1182
1251
|
const access = activeAccess();
|
|
1183
1252
|
return access === void 0 ? !0 : canAccess({
|
|
@@ -1186,7 +1255,7 @@ function makeHandle(handleArgs) {
|
|
|
1186
1255
|
permission: "read",
|
|
1187
1256
|
userId: access.currentUser.id
|
|
1188
1257
|
});
|
|
1189
|
-
}, applyPatch = async (documentId, ops, ifRev) => {
|
|
1258
|
+
}, applyPatch = async (documentId, ops, ifRev, options = {}) => {
|
|
1190
1259
|
const existing = store.get(documentId);
|
|
1191
1260
|
if (existing === void 0)
|
|
1192
1261
|
throw fakeError(
|
|
@@ -1198,7 +1267,7 @@ function makeHandle(handleArgs) {
|
|
|
1198
1267
|
_rev: cryptoRandomKey(),
|
|
1199
1268
|
_updatedAt: now
|
|
1200
1269
|
};
|
|
1201
|
-
return await
|
|
1270
|
+
return options.evaluateGuard !== !1 && await guardOne("update", existing, next), store.put(next), next;
|
|
1202
1271
|
}, applyTransaction = async (mutations, txnOptions) => {
|
|
1203
1272
|
const transactionId = txnOptions?.transactionId ?? cryptoRandomKey(), snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
|
|
1204
1273
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
@@ -1213,17 +1282,13 @@ function makeHandle(handleArgs) {
|
|
|
1213
1282
|
throw fakeError(
|
|
1214
1283
|
`create() failed: a document with _id "${promoted._id}" already exists in dataset "${projectId}:${dataset}". Use createOrReplace() to overwrite it, or createIfNotExists() to leave the existing one untouched.`
|
|
1215
1284
|
);
|
|
1216
|
-
await gate(promoted, "create"),
|
|
1285
|
+
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1217
1286
|
break;
|
|
1218
1287
|
}
|
|
1219
1288
|
case "createOrReplace": {
|
|
1220
1289
|
remember(mut.doc._id);
|
|
1221
1290
|
const promoted = promote2(mut.doc), existing = store.get(promoted._id);
|
|
1222
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create")
|
|
1223
|
-
existing !== void 0 ? "update" : "create",
|
|
1224
|
-
existing ?? null,
|
|
1225
|
-
promoted
|
|
1226
|
-
);
|
|
1291
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create");
|
|
1227
1292
|
const wasPresent = existing !== void 0;
|
|
1228
1293
|
store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: wasPresent ? "update" : "create" });
|
|
1229
1294
|
break;
|
|
@@ -1231,21 +1296,25 @@ function makeHandle(handleArgs) {
|
|
|
1231
1296
|
case "createIfNotExists": {
|
|
1232
1297
|
if (remember(mut.doc._id), store.get(mut.doc._id) === void 0) {
|
|
1233
1298
|
const promoted = promote2(mut.doc);
|
|
1234
|
-
await gate(promoted, "create"),
|
|
1299
|
+
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1235
1300
|
}
|
|
1236
1301
|
break;
|
|
1237
1302
|
}
|
|
1238
1303
|
case "delete": {
|
|
1239
1304
|
remember(mut.id);
|
|
1240
1305
|
const existing = store.get(mut.id);
|
|
1241
|
-
existing !== void 0 && await gate(existing, "update"),
|
|
1306
|
+
existing !== void 0 && await gate(existing, "update"), store.remove(mut.id) && (touchedIds.push(mut.id), operations.push({ id: mut.id, operation: "delete" }));
|
|
1242
1307
|
break;
|
|
1243
1308
|
}
|
|
1244
1309
|
case "patch": {
|
|
1245
|
-
remember(mut.documentId), await applyPatch(mut.documentId, mut.ops, mut.ifRev), touchedIds.push(mut.documentId), operations.push({ id: mut.documentId, operation: "update" });
|
|
1310
|
+
remember(mut.documentId), await applyPatch(mut.documentId, mut.ops, mut.ifRev, { evaluateGuard: !1 }), touchedIds.push(mut.documentId), operations.push({ id: mut.documentId, operation: "update" });
|
|
1246
1311
|
break;
|
|
1247
1312
|
}
|
|
1248
1313
|
}
|
|
1314
|
+
const contexts = mutationContexts(
|
|
1315
|
+
[...snapshot].map(([id, before]) => ({ before, after: store.get(id) }))
|
|
1316
|
+
);
|
|
1317
|
+
await guardGate(contexts);
|
|
1249
1318
|
} catch (error) {
|
|
1250
1319
|
for (const [id, doc] of snapshot)
|
|
1251
1320
|
doc === void 0 ? store.remove(id) : store.put(doc);
|
|
@@ -1310,7 +1379,7 @@ function makeHandle(handleArgs) {
|
|
|
1310
1379
|
`create() failed: a document with _id "${doc._id}" already exists in dataset "${projectId}:${dataset}". Use createOrReplace() to overwrite it, or createIfNotExists() to leave the existing one untouched.`
|
|
1311
1380
|
);
|
|
1312
1381
|
const promoted = promote2(doc);
|
|
1313
|
-
await gate(promoted, "create"), await
|
|
1382
|
+
await gate(promoted, "create"), await guardOne("create", null, promoted);
|
|
1314
1383
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1315
1384
|
return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }], opts?.tag)), writeResult(
|
|
1316
1385
|
[promoted],
|
|
@@ -1322,7 +1391,7 @@ function makeHandle(handleArgs) {
|
|
|
1322
1391
|
createOrReplace: (doc, opts) => serializeWrite(backend, async () => {
|
|
1323
1392
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
1324
1393
|
const promoted = promote2(doc), existing = store.get(doc._id);
|
|
1325
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await
|
|
1394
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardOne(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
|
|
1326
1395
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1327
1396
|
opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }], opts?.tag));
|
|
1328
1397
|
const operation = existing !== void 0 ? "update" : "create";
|
|
@@ -1344,7 +1413,7 @@ function makeHandle(handleArgs) {
|
|
|
1344
1413
|
opts
|
|
1345
1414
|
);
|
|
1346
1415
|
const promoted = promote2(doc);
|
|
1347
|
-
return await gate(promoted, "create"), await
|
|
1416
|
+
return await gate(promoted, "create"), await guardOne("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }], opts?.tag)), writeResult(
|
|
1348
1417
|
[promoted],
|
|
1349
1418
|
[{ id: promoted._id, operation: "create" }],
|
|
1350
1419
|
transactionId,
|
|
@@ -1354,7 +1423,7 @@ function makeHandle(handleArgs) {
|
|
|
1354
1423
|
delete: (id, opts) => serializeWrite(backend, async () => {
|
|
1355
1424
|
checkRevision(id, opts?.ifRevisionId);
|
|
1356
1425
|
const existing = store.get(id);
|
|
1357
|
-
existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await
|
|
1426
|
+
existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardOne("delete", existing, null);
|
|
1358
1427
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1359
1428
|
let removed = existing;
|
|
1360
1429
|
return opts?.dryRun !== !0 && (removed = store.remove(id), removed !== void 0 && record(transactionId, [{ kind: "delete", id }], opts?.tag)), opts?.returnDocuments === void 0 && opts?.returnFirst === void 0 ? removed !== void 0 ? { _id: id } : null : formatMutationResult(
|
|
@@ -1504,6 +1573,7 @@ function makeHandle(handleArgs) {
|
|
|
1504
1573
|
});
|
|
1505
1574
|
}
|
|
1506
1575
|
exports.ERR_PREFIX = ERR_PREFIX;
|
|
1576
|
+
exports.GuardDeniedError = GuardDeniedError;
|
|
1507
1577
|
exports.SUPPORTED_SURFACE = SUPPORTED_SURFACE;
|
|
1508
1578
|
exports.UNSUPPORTED_MEMBERS = UNSUPPORTED_MEMBERS;
|
|
1509
1579
|
exports.WriteAccessDeniedError = WriteAccessDeniedError;
|