@sanity/workflow-engine-test 0.7.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 CHANGED
@@ -17,19 +17,24 @@ npm install -D @sanity/workflow-engine-test
17
17
 
18
18
  A workflow stage can deploy **mutation guards** — `temp.system.guard` documents
19
19
  that lock the documents it governs while the stage is active, and lift when it
20
- exits. There are two distinct things you test, and the bench gives you a helper
21
- for each:
22
-
23
- - **Write-time enforcement** `bench.editDocument(id, patch)` writes through the
24
- bench client, which **simulates the lake's server-side guard rejection**: a
25
- write a deployed guard denies throws `MutationGuardDeniedError`. In production
26
- the lake enforces this for _any_ client; the bench reproduces it on the
20
+ exits. The lake does not enforce this doc type yet: in production, guard
21
+ enforcement is the engine's own **optimistic** evaluation (advisory verdicts
22
+ and engine-side write denials), and the lake ACL is the only hard boundary.
23
+ The bench installs the _intended_ lake-side contract at its client's write
24
+ seam, so tests exercise the rejection semantics guards are designed to have
25
+ once the lake enforces them. There are two distinct things you test, and the
26
+ bench gives you a helper for each:
27
+
28
+ - **Write-time rejection (simulated)** — `bench.editDocument({documentId, patch})`
29
+ writes through the bench client, and a write a deployed guard denies throws
30
+ `MutationGuardDeniedError` — the rejection the lake is designed to apply to
31
+ _any_ client once guard enforcement ships; the bench reproduces it on the
27
32
  client it mints.
28
33
  - **Read-time preflight** — `bench.activeGuardsForDocument(id)` answers "_would_
29
34
  a write be denied right now?" _without_ writing. This is the check a UI uses
30
35
  to disable a button or show a lock ahead of time. It's a pure read over the
31
- same evaluation path as enforcement, so its verdict matches what a write would
32
- actually do — and it works against any client.
36
+ same evaluation path as the bench's write rejection, so its verdict matches
37
+ what a bench write would actually do — and it works against any client.
33
38
 
34
39
  `bench.listGuards()` returns the deployed guard docs for inspection.
35
40
 
@@ -40,15 +45,15 @@ const bench = createBench({documents: [{_id: 'doc-1', _type: 'article', body: 'b
40
45
  // Preflight: is the doc locked for this user right now?
41
46
  const active = await bench.activeGuardsForDocument('doc-1')
42
47
 
43
- // Enforcement: a guard-violating write is rejected.
44
- await expect(bench.editDocument('doc-1', {body: 'edited'})).rejects.toThrow(
48
+ // Rejection: a guard-violating write throws (bench-simulated lake contract).
49
+ await expect(bench.editDocument({documentId: 'doc-1', patch: {body: 'edited'}})).rejects.toThrow(
45
50
  MutationGuardDeniedError,
46
51
  )
47
52
  ```
48
53
 
49
- > To share one store across benches with enforcement intact, pass another
50
- > bench's already-wired client: `createBench({client: other.client, tags})`. The
51
- > `mutationGuard` rides along, so the second bench enforces the same locks. Only
54
+ > To share one store across benches with the write seam intact, pass another
55
+ > bench's already-wired client: `createBench({client: other.client, tag})`. The
56
+ > `mutationGuard` rides along, so the second bench denies the same locks. Only
52
57
  > a bare hand-built `createTestClient()` opts out of write rejection — the
53
58
  > read-time preflight needs no wiring either way.
54
59
 
package/dist/index.cjs CHANGED
@@ -5014,7 +5014,7 @@ const DEFAULT_WORKFLOW_RESOURCE = {
5014
5014
  type: "dataset",
5015
5015
  id: "test.test"
5016
5016
  }, ALL_ACCESS_USER = {
5017
- kind: "user",
5017
+ kind: "person",
5018
5018
  id: "bench-user",
5019
5019
  roles: ["*"]
5020
5020
  }, WILDCARD_GRANTS = [
@@ -5073,7 +5073,11 @@ function resolveBenchAccess(options) {
5073
5073
  grants: options.grants ?? WILDCARD_GRANTS
5074
5074
  };
5075
5075
  }
5076
- function accessFor(access, args, canTokenResolve) {
5076
+ function accessFor({
5077
+ access,
5078
+ args,
5079
+ canTokenResolve
5080
+ }) {
5077
5081
  if (args.access !== void 0) return args.access;
5078
5082
  if (args.actor === void 0 && args.grants === void 0) return access;
5079
5083
  const inheritedGrants = canTokenResolve ? void 0 : access.grants, grants = args.grants ?? inheritedGrants;
@@ -5086,10 +5090,14 @@ function stripAccessShortcuts(args) {
5086
5090
  const rest = { ...args };
5087
5091
  return delete rest.access, delete rest.actor, delete rest.grants, rest;
5088
5092
  }
5089
- function createEngineWrappers(scope, access, clock) {
5093
+ function createEngineWrappers({
5094
+ scope,
5095
+ access,
5096
+ clock
5097
+ }) {
5090
5098
  const canTokenResolve = "request" in scope.client, withAccess = (args) => ({
5091
5099
  ...scope,
5092
- access: accessFor(access, args, canTokenResolve),
5100
+ access: accessFor({ access, args, canTokenResolve }),
5093
5101
  clock,
5094
5102
  ...stripAccessShortcuts(args)
5095
5103
  });
@@ -5121,8 +5129,8 @@ function createGuardHelpers(client) {
5121
5129
  ...as ? { identity: as.id } : {}
5122
5130
  });
5123
5131
  },
5124
- editDocument: async (documentId, patch, editOptions) => {
5125
- const action = editOptions?.action ?? "update", as = editOptions?.as, writer = as ? client.withConfig({
5132
+ editDocument: async ({ documentId, patch, action: editAction, as }) => {
5133
+ const action = editAction ?? "update", writer = as ? client.withConfig({
5126
5134
  accessControl: { currentUser: { id: as.id }, grants: WILDCARD_GRANTS }
5127
5135
  }) : client;
5128
5136
  return action === "delete" ? await writer.delete(documentId) : await writer.patch(documentId).set(patch).commit(), await client.getDocument(documentId);
@@ -5133,7 +5141,11 @@ function createQueryHelpers(scope) {
5133
5141
  const withParams = (params) => params !== void 0 ? { params } : {};
5134
5142
  return {
5135
5143
  query: (groq, params) => workflowEngine.workflow.query({ ...scope, groq, ...withParams(params) }),
5136
- queryInScope: (instanceId, groq, params) => workflowEngine.workflow.queryInScope({ ...scope, instanceId, groq, ...withParams(params) })
5144
+ queryInScope: ({
5145
+ instanceId,
5146
+ groq,
5147
+ params
5148
+ }) => workflowEngine.workflow.queryInScope({ ...scope, instanceId, groq, ...withParams(params) })
5137
5149
  };
5138
5150
  }
5139
5151
  function createReadHelpers(scope) {
@@ -5172,24 +5184,49 @@ function createReadHelpers(scope) {
5172
5184
  { ref: subjectRef, entry: entryName, tag }
5173
5185
  )
5174
5186
  ),
5175
- instancesByStage: async (workflowName, stage, queryOptions) => {
5176
- const filter = queryOptions?.openOnly ? " && completedAt == null" : "";
5177
- return client.fetch(
5178
- `*[_type == "${workflowEngine.WORKFLOW_INSTANCE_TYPE}" && definition == $wf && currentStage == $stage && ${workflowEngine.tagScopeFilter()}${filter}]
5179
- | order(startedAt asc)`,
5180
- { wf: workflowName, stage, tag }
5181
- );
5187
+ instancesByStage: async ({ workflowName, stage, openOnly }) => {
5188
+ const { query, params } = workflowEngine.instancesQuery({
5189
+ tag,
5190
+ filter: { definition: workflowName, stage, includeCompleted: !openOnly }
5191
+ });
5192
+ return client.fetch(query, params);
5182
5193
  },
5183
5194
  instancesForDocument: async (document2) => workflowEngine.workflow.instancesForDocument({ ...scope, document: document2 }),
5184
5195
  snapshot: () => client.snapshot()
5185
5196
  };
5186
5197
  }
5198
+ function subjectField(documentId, opts) {
5199
+ return {
5200
+ type: "doc.ref",
5201
+ name: opts?.name ?? "subject",
5202
+ value: workflowEngine.gdrRef({
5203
+ res: DEFAULT_WORKFLOW_RESOURCE,
5204
+ documentId,
5205
+ type: opts?.type ?? "document"
5206
+ })
5207
+ };
5208
+ }
5209
+ function releaseField(releaseName, opts) {
5210
+ return {
5211
+ type: "release.ref",
5212
+ name: opts?.name ?? "release",
5213
+ value: {
5214
+ id: workflowEngine.gdrFromResource(DEFAULT_WORKFLOW_RESOURCE, `_.releases.${releaseName}`),
5215
+ type: "system.release",
5216
+ releaseName
5217
+ }
5218
+ };
5219
+ }
5187
5220
  function createBench(options = {}) {
5188
5221
  const tag = options.tag ?? "bench";
5189
5222
  workflowEngine.validateTag(tag);
5190
- const workflowResource = options.workflowResource ?? DEFAULT_WORKFLOW_RESOURCE, client = resolveBenchClient(options), scope = { client, tag, workflowResource }, access = resolveBenchAccess(options);
5223
+ const workflowResource = options.workflowResource ?? DEFAULT_WORKFLOW_RESOURCE, client = resolveBenchClient(options), scope = {
5224
+ client,
5225
+ tag,
5226
+ workflowResource
5227
+ }, access = resolveBenchAccess(options);
5191
5228
  let frozenNow = options.now !== void 0 ? assertIsoInstant(options.now) : void 0;
5192
- const clock = () => frozenNow ?? (/* @__PURE__ */ new Date()).toISOString();
5229
+ const clock = () => frozenNow ?? (/* @__PURE__ */ new Date()).toISOString(), engine = createEngineWrappers({ scope, access, clock }), reads = createReadHelpers(scope);
5193
5230
  return {
5194
5231
  client,
5195
5232
  access,
@@ -5205,10 +5242,16 @@ function createBench(options = {}) {
5205
5242
  throw new Error(`bench.advance: ms must be a finite number, got ${ms2}`);
5206
5243
  frozenNow = new Date(Date.parse(frozenNow ?? (/* @__PURE__ */ new Date()).toISOString()) + ms2).toISOString();
5207
5244
  },
5208
- ...createEngineWrappers(scope, access, clock),
5209
- ...createReadHelpers(scope),
5245
+ ...engine,
5246
+ ...reads,
5210
5247
  ...createGuardHelpers(client),
5211
- ...createQueryHelpers(scope)
5248
+ ...createQueryHelpers(scope),
5249
+ completePendingEffect: async ({ effect, ...rest }) => {
5250
+ const match = (await reads.pendingEffects(rest.instanceId)).find((e) => e.name === effect);
5251
+ if (match === void 0)
5252
+ throw new Error(`completePendingEffect: no pending effect named "${effect}"`);
5253
+ return engine.completeEffect({ ...rest, effectKey: match._key });
5254
+ }
5212
5255
  };
5213
5256
  }
5214
5257
  function assertIsoInstant(iso) {
@@ -5222,4 +5265,6 @@ exports.ALL_ACCESS_USER = ALL_ACCESS_USER;
5222
5265
  exports.DEFAULT_WORKFLOW_RESOURCE = DEFAULT_WORKFLOW_RESOURCE;
5223
5266
  exports.WILDCARD_GRANTS = WILDCARD_GRANTS;
5224
5267
  exports.createBench = createBench;
5268
+ exports.releaseField = releaseField;
5269
+ exports.subjectField = subjectField;
5225
5270
  //# sourceMappingURL=index.cjs.map