@sanity-labs/client-fake-for-test 0.5.0 → 0.7.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/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(accessControl, projectId, dataset) {
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 accessControl 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.`
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
  }
@@ -137,7 +136,7 @@ function createPatch(documentId, executor) {
137
136
  dec: (props) => (ops.push({ kind: "dec", props }), handle),
138
137
  insert: (position, anchor, items) => (ops.push({ kind: "insert", position, anchor, items }), handle),
139
138
  ifRevisionId: (rev) => (ifRev = rev, handle),
140
- commit: () => executor.apply(documentId, ops, ifRev),
139
+ commit: (options) => executor.apply(documentId, ops, ifRev, options),
141
140
  toJSON: () => ({
142
141
  id: documentId,
143
142
  ops: [...ops],
@@ -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 accessControl is configured",
741
- workaround: "Create the client with `accessControl` to enable request() (it serves /users/me + the ACL path), or register a stub."
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?",
@@ -838,6 +837,25 @@ function wrapWithGuards(client, config) {
838
837
  }
839
838
  });
840
839
  }
840
+ const OPTIONS_ARG_POSITION = {
841
+ fetch: 2,
842
+ getDocument: 1,
843
+ getDocuments: 1,
844
+ create: 1,
845
+ createOrReplace: 1,
846
+ createIfNotExists: 1,
847
+ delete: 1,
848
+ mutate: 1,
849
+ action: 1,
850
+ request: 0
851
+ };
852
+ function tagFromArgs(method, args) {
853
+ const position = OPTIONS_ARG_POSITION[method];
854
+ if (position === void 0) return;
855
+ const options = args[position];
856
+ if (!(typeof options != "object" || options === null || !("tag" in options)))
857
+ return options.tag;
858
+ }
841
859
  const RECORDED_METHODS = /* @__PURE__ */ new Set([
842
860
  "fetch",
843
861
  "getDocument",
@@ -854,7 +872,21 @@ const RECORDED_METHODS = /* @__PURE__ */ new Set([
854
872
  "createVersion",
855
873
  "unpublishVersion",
856
874
  "request"
857
- ]), DEFAULT_PROJECT_ID = "test", DEFAULT_DATASET = "test", registryKey = (projectId, dataset) => `${projectId}:${dataset}`, parseRegistryKey = (key) => {
875
+ ]), DEFAULT_PROJECT_ID = "test", DEFAULT_DATASET = "test";
876
+ function requireIdentity(backend, token, site) {
877
+ if (backend.identities.has(token)) return;
878
+ const known = [...backend.identities.keys()];
879
+ throw fakeError(
880
+ `${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)."
881
+ );
882
+ }
883
+ function rejectAmbiguousIdentity(site, config) {
884
+ if (config.token !== void 0 && config.accessControl !== void 0)
885
+ throw fakeError(
886
+ `${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.`
887
+ );
888
+ }
889
+ const registryKey = (projectId, dataset) => `${projectId}:${dataset}`, parseRegistryKey = (key) => {
858
890
  const [projectId, dataset] = key.split(":");
859
891
  return { projectId, dataset };
860
892
  };
@@ -864,8 +896,11 @@ function createTestClient(options = {}) {
864
896
  log: [],
865
897
  stubs: /* @__PURE__ */ new Map(),
866
898
  calls: [],
867
- concurrentEdits: /* @__PURE__ */ new Map()
868
- }, registry = backend.registry, ensureStore = (projectId, dataset) => {
899
+ concurrentEdits: /* @__PURE__ */ new Map(),
900
+ identities: new Map(Object.entries(options.identities ?? {}))
901
+ }, registry = backend.registry;
902
+ rejectAmbiguousIdentity("createTestClient()", options), options.token !== void 0 && requireIdentity(backend, options.token, "createTestClient()");
903
+ const ensureStore = (projectId, dataset) => {
869
904
  const key = registryKey(projectId, dataset);
870
905
  let store = registry.get(key);
871
906
  return store === void 0 && (store = createTestStore(), registry.set(key, store)), store;
@@ -881,7 +916,8 @@ function createTestClient(options = {}) {
881
916
  defaultDataset,
882
917
  options.accessControl,
883
918
  options.perspective ?? "raw",
884
- options.mutationGuard
919
+ options.mutationGuard,
920
+ options.token
885
921
  );
886
922
  }
887
923
  function dedupeIds(results) {
@@ -934,13 +970,30 @@ function toTxnMutations(operations) {
934
970
  }
935
971
  ];
936
972
  }
937
- function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard) {
938
- const { registry } = backend, store = (() => {
973
+ function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard, token) {
974
+ const { registry } = backend, activeAccess = () => {
975
+ if (token === void 0) return accessControl;
976
+ const identity = backend.identities.get(token);
977
+ if (identity === void 0)
978
+ throw fakeError(`no identity is registered for token "${token}" (was it removed?).`);
979
+ return { currentUser: identity.user, grants: identity.grants };
980
+ }, requireActiveAccess = () => {
981
+ const access = activeAccess();
982
+ if (access === void 0)
983
+ throw fakeError("this handle has no acting identity (no token or accessControl bound).");
984
+ return access;
985
+ }, store = (() => {
939
986
  const key = registryKey(projectId, dataset);
940
987
  let s = registry.get(key);
941
988
  return s === void 0 && (s = createTestStore(), registry.set(key, s)), s;
942
- })(), concurrentEditKey = (id) => registryKey(projectId, dataset) + " " + id, record = (transactionId, mutations) => {
943
- backend.log.push({ transactionId, projectId, dataset, mutations });
989
+ })(), concurrentEditKey = (id) => registryKey(projectId, dataset) + " " + id, record = (transactionId, mutations, tag) => {
990
+ backend.log.push({
991
+ transactionId,
992
+ projectId,
993
+ dataset,
994
+ mutations,
995
+ ...tag !== void 0 ? { tag } : {}
996
+ });
944
997
  }, writeResult = (documents, results, transactionId, opts) => formatMutationResult(
945
998
  documents,
946
999
  results,
@@ -982,20 +1035,24 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
982
1035
  `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
1036
  );
984
1037
  }, gate = async (document, permission) => {
985
- accessControl !== void 0 && await enforceWriteAccess({
1038
+ const access = activeAccess();
1039
+ access !== void 0 && await enforceWriteAccess({
986
1040
  document,
987
- grants: accessControl.grants,
1041
+ grants: access.grants,
988
1042
  permission,
989
- userId: accessControl.currentUser.id
1043
+ userId: access.currentUser.id
990
1044
  });
991
1045
  }, guardGate = async (action, before, after) => {
992
- mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: accessControl?.currentUser.id });
993
- }, canRead = async (document) => accessControl === void 0 ? !0 : canAccess({
994
- document,
995
- grants: accessControl.grants,
996
- permission: "read",
997
- userId: accessControl.currentUser.id
998
- }), applyPatch = async (documentId, ops, ifRev) => {
1046
+ mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: activeAccess()?.currentUser.id });
1047
+ }, canRead = async (document) => {
1048
+ const access = activeAccess();
1049
+ return access === void 0 ? !0 : canAccess({
1050
+ document,
1051
+ grants: access.grants,
1052
+ permission: "read",
1053
+ userId: access.currentUser.id
1054
+ });
1055
+ }, applyPatch = async (documentId, ops, ifRev) => {
999
1056
  const existing = store.get(documentId);
1000
1057
  if (existing === void 0)
1001
1058
  throw fakeError(
@@ -1060,7 +1117,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1060
1117
  doc === void 0 ? store.remove(id) : store.put(doc);
1061
1118
  throw error;
1062
1119
  }
1063
- record(transactionId, mutations);
1120
+ record(transactionId, mutations, txnOptions?.tag);
1064
1121
  const documents = touchedIds.map((id) => store.get(id)).filter((d) => d !== void 0);
1065
1122
  return formatMutationResult(
1066
1123
  documents,
@@ -1071,7 +1128,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1071
1128
  );
1072
1129
  }, accessVisibleDocs = async () => {
1073
1130
  const all = store.all();
1074
- if (accessControl === void 0) return all;
1131
+ if (activeAccess() === void 0) return all;
1075
1132
  const out = [];
1076
1133
  for (const doc of all)
1077
1134
  await canRead(doc) && out.push(doc);
@@ -1121,7 +1178,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1121
1178
  const promoted = promote2(doc);
1122
1179
  await gate(promoted, "create"), await guardGate("create", null, promoted);
1123
1180
  const transactionId = opts?.transactionId ?? cryptoRandomKey();
1124
- return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }])), writeResult(
1181
+ return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }], opts?.tag)), writeResult(
1125
1182
  [promoted],
1126
1183
  [{ id: promoted._id, operation: "create" }],
1127
1184
  transactionId,
@@ -1133,7 +1190,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1133
1190
  const promoted = promote2(doc), existing = store.get(doc._id);
1134
1191
  await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
1135
1192
  const transactionId = opts?.transactionId ?? cryptoRandomKey();
1136
- opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }]));
1193
+ opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }], opts?.tag));
1137
1194
  const operation = existing !== void 0 ? "update" : "create";
1138
1195
  return writeResult(
1139
1196
  [promoted],
@@ -1153,7 +1210,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1153
1210
  opts
1154
1211
  );
1155
1212
  const promoted = promote2(doc);
1156
- return await gate(promoted, "create"), await guardGate("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }])), writeResult(
1213
+ return await gate(promoted, "create"), await guardGate("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }], opts?.tag)), writeResult(
1157
1214
  [promoted],
1158
1215
  [{ id: promoted._id, operation: "create" }],
1159
1216
  transactionId,
@@ -1166,7 +1223,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1166
1223
  existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardGate("delete", existing, null);
1167
1224
  const transactionId = opts?.transactionId ?? cryptoRandomKey();
1168
1225
  let removed = existing;
1169
- return opts?.dryRun !== !0 && (removed = store.remove(id), removed !== void 0 && record(transactionId, [{ kind: "delete", id }])), opts?.returnDocuments === void 0 && opts?.returnFirst === void 0 ? removed !== void 0 ? { _id: id } : null : formatMutationResult(
1226
+ 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(
1170
1227
  removed !== void 0 ? [removed] : [],
1171
1228
  removed !== void 0 ? [{ id, operation: "delete" }] : [],
1172
1229
  transactionId,
@@ -1175,11 +1232,13 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1175
1232
  );
1176
1233
  },
1177
1234
  patch: (documentId) => createPatch(documentId, {
1178
- apply: async (docId, ops, ifRev) => {
1235
+ apply: async (docId, ops, ifRev, options) => {
1179
1236
  const doc = await applyPatch(docId, ops, ifRev);
1180
- return record(cryptoRandomKey(), [
1181
- { kind: "patch", documentId: docId, ops, ...ifRev !== void 0 ? { ifRev } : {} }
1182
- ]), doc;
1237
+ return record(
1238
+ cryptoRandomKey(),
1239
+ [{ kind: "patch", documentId: docId, ops, ...ifRev !== void 0 ? { ifRev } : {} }],
1240
+ options?.tag
1241
+ ), doc;
1183
1242
  }
1184
1243
  }),
1185
1244
  transaction: () => createTransaction({ apply: applyTransaction }),
@@ -1226,16 +1285,21 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1226
1285
  versionId: `versions.${releaseId}.${publishedId}`,
1227
1286
  publishedId
1228
1287
  }),
1229
- ...accessControl !== void 0 ? { request: makeRequestImpl(accessControl, projectId, dataset) } : {},
1230
- config: () => ({ projectId, dataset, perspective }),
1231
- withConfig: (config) => makeHandle(
1232
- backend,
1233
- config.projectId ?? projectId,
1234
- config.dataset ?? dataset,
1235
- config.accessControl ?? accessControl,
1236
- config.perspective ?? perspective,
1237
- mutationGuard
1238
- ),
1288
+ ...token !== void 0 || accessControl !== void 0 ? { request: makeRequestImpl(requireActiveAccess, projectId, dataset) } : {},
1289
+ config: () => ({ projectId, dataset, perspective, ...token !== void 0 ? { token } : {} }),
1290
+ withConfig: (config) => {
1291
+ rejectAmbiguousIdentity("withConfig()", config), config.token !== void 0 && requireIdentity(backend, config.token, "withConfig()");
1292
+ const rebinds = config.token !== void 0 || config.accessControl !== void 0;
1293
+ return makeHandle(
1294
+ backend,
1295
+ config.projectId ?? projectId,
1296
+ config.dataset ?? dataset,
1297
+ rebinds ? config.accessControl : accessControl,
1298
+ config.perspective ?? perspective,
1299
+ mutationGuard,
1300
+ rebinds ? config.token : token
1301
+ );
1302
+ },
1239
1303
  seed: (documents) => store.seed(documents),
1240
1304
  add: (documents) => store.add(documents),
1241
1305
  reset: () => store.reset(),
@@ -1250,6 +1314,10 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1250
1314
  backend.stubs.set(name, handler);
1251
1315
  },
1252
1316
  stubs: () => Object.fromEntries(backend.stubs),
1317
+ addIdentity: (identityToken, identity) => {
1318
+ backend.identities.set(identityToken, identity);
1319
+ },
1320
+ identities: () => Object.fromEntries(backend.identities),
1253
1321
  mutations: () => backend.log.flatMap((entry) => entry.mutations),
1254
1322
  transactions: () => [...backend.log],
1255
1323
  clearLog: () => {
@@ -1264,8 +1332,8 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1264
1332
  return wrapWithGuards(client, {
1265
1333
  getStub: (name) => backend.stubs.get(name),
1266
1334
  onCall: (method, args) => {
1267
- const rec = { method, args, projectId, dataset };
1268
- return backend.calls.push(rec), (outcome) => {
1335
+ const rec = { method, args, projectId, dataset }, tag = tagFromArgs(method, args);
1336
+ return tag !== void 0 && (rec.tag = tag), backend.calls.push(rec), (outcome) => {
1269
1337
  "error" in outcome ? rec.error = outcome.error : rec.result = outcome.result;
1270
1338
  };
1271
1339
  },