@reventlessdev/reventless-local 3.0.0-alpha.281 → 3.0.0-alpha.283

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +9 -9
  3. package/src/Platform.res +5 -4
  4. package/src/Platform.res.mjs +12 -12
  5. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +6 -25
  6. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +4 -20
  7. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +34 -38
  8. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs +14 -6
  9. package/src/adapter/PlatformGraphQL_Server.res +1 -1
  10. package/tests/adapter/CommandAuthorizationTest.res +7 -8
  11. package/tests/adapter/CommandAuthorizationTest.res.mjs +5 -5
  12. package/tests/adapter/InboundTranslationMutationTest.res +95 -7
  13. package/tests/adapter/InboundTranslationMutationTest.res.mjs +97 -9
  14. package/tests/components/aggregate/AggregateFixtures.res.mjs +3 -3
  15. package/tests/components/aggregate/AggregateSelfMappingFixtures.res.mjs +3 -3
  16. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +4 -4
  17. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +8 -8
  18. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +2 -2
  19. package/tests/components/commandgenerator/CommandGeneratorFixtures.res.mjs +3 -3
  20. package/tests/components/commandtopic/CommandTopicFixtures.res.mjs +2 -2
  21. package/tests/components/commandtopic/CommandTopicStreamFixtures.res.mjs +2 -2
  22. package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +3 -3
  23. package/tests/components/dcb/DcbFixtures.res.mjs +3 -3
  24. package/tests/components/extensionpoint/ExtensionPointFixtures.res.mjs +5 -5
  25. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +116 -8
  26. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +160 -19
  27. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +58 -0
  28. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +145 -7
  29. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +3 -3
  30. package/tests/components/readmodel/DcbReadModelE2EFixtures.res.mjs +3 -3
  31. package/tests/plugin/PluginBehavior_GWT.res.mjs +2 -2
  32. package/tests/plugin/UiFragmentRegistry_GWT.res.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,24 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.283 (2026-10-10)
7
+
8
+ * feat!: a command's rule is looked up by its name, without a cast ([5f02c40](https://github.com/ReventlessDev/reventless-core/commit/5f02c4081d077bf4a6951d8c9b2b16553a8ca87b))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * a spec that writes `commandAuthorization` by hand writes
13
+ `authorizationOf = (name: string) => …` instead; the PPX refuses the old name.
14
+
15
+
16
+
17
+ # 3.0.0-alpha.282 (2026-10-10)
18
+
19
+ ### Bug Fixes
20
+
21
+ * an inbound translation is gated alike on every door, and encodes its commands by schema ([6ec3ce6](https://github.com/ReventlessDev/reventless-core/commit/6ec3ce672e53e2c162c64a90d103be691391be5e))
22
+
23
+
6
24
  # 3.0.0-alpha.281 (2026-10-08)
7
25
 
8
26
  **Note:** Version bump only for package @reventlessdev/reventless-local
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.281",
3
+ "version": "3.0.0-alpha.283",
4
4
  "description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -37,21 +37,21 @@
37
37
  "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
38
38
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
39
39
  "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
40
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
41
40
  "@reventlessdev/rescript-node": "2.0.0-alpha.22",
41
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
42
42
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
43
- "@reventlessdev/reventless-core": "3.0.0-alpha.291",
44
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.139",
45
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.238",
46
- "@reventlessdev/reventless-infra": "3.0.0-alpha.188",
47
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.155",
43
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.141",
44
+ "@reventlessdev/reventless-core": "3.0.0-alpha.293",
45
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.240",
46
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.190",
48
47
  "@reventlessdev/reventless-seed": "1.0.0-alpha.33",
49
- "@reventlessdev/reventless-spec": "3.0.0-alpha.160"
48
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.162",
49
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.157"
50
50
  },
51
51
  "devDependencies": {
52
52
  "rescript": "12.3.1",
53
53
  "sury-ppx": "11.0.0",
54
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.98"
54
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.99"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "rescript": "^12.3.1"
package/src/Platform.res CHANGED
@@ -290,14 +290,14 @@ module MakeWithConfig = (
290
290
  deployTarget: ref("Domain"),
291
291
  // Phase 1: register SDL + resolver stub synchronously.
292
292
  // Pass the resolved server so the correct target (domain or platform) receives the schema.
293
- mutationResolverHook: (~kind, ~fields, ~commandSchema, ~commandAuthorization) => {
293
+ mutationResolverHook: (~kind, ~fields, ~commandSchema, ~authorizationOf) => {
294
294
  let server = resolveTargetGraphQL()
295
295
  switch kind {
296
296
  | ReventlessCore.Plugin_Helpers.Aggregate =>
297
297
  CommandGeneratorResolvers_GraphQL.register(
298
298
  ~fields,
299
299
  ~commandSchema,
300
- ~commandAuthorization,
300
+ ~authorizationOf,
301
301
  ~server,
302
302
  )
303
303
  | Dcb =>
@@ -305,7 +305,7 @@ module MakeWithConfig = (
305
305
  CommandGeneratorResolvers_GraphQL.registerDcb(
306
306
  ~fieldName=field,
307
307
  ~commandSchema,
308
- ~commandAuthorization,
308
+ ~authorizationOf,
309
309
  ~server,
310
310
  )
311
311
  )
@@ -314,10 +314,11 @@ module MakeWithConfig = (
314
314
  // Phase 2: bind generateCommand when Output.apply resolves.
315
315
  mutationBindHook: CommandGeneratorResolvers_GraphQL.bindHandler,
316
316
  // InboundTranslationSlice hooks — pass resolved server for correct target routing.
317
- inboundMutationResolverHook: (~fieldName, ~externalInputSchema) =>
317
+ inboundMutationResolverHook: (~fieldName, ~externalInputSchema, ~permission=?) =>
318
318
  InboundTranslationResolvers_GraphQL.register(
319
319
  ~fieldName,
320
320
  ~externalInputSchema,
321
+ ~permission?,
321
322
  ~server=resolveTargetGraphQL(),
322
323
  ),
323
324
  inboundMutationBindReceiveHook: InboundTranslationResolvers_GraphQL.bindReceive,
@@ -225,15 +225,15 @@ function MakeWithConfig(Config) {
225
225
  }
226
226
  };
227
227
  let EventHistoryResolvers = EventHistoryResolvers_GraphQL$ReventlessLocal.Make(Bus);
228
- let hooks_mutationResolverHook = (kind, fields, commandSchema, commandAuthorization) => {
228
+ let hooks_mutationResolverHook = (kind, fields, commandSchema, authorizationOf) => {
229
229
  let server = resolveTargetGraphQL();
230
230
  if (kind === "Aggregate") {
231
- return CommandGeneratorResolvers_GraphQL$ReventlessLocal.register(fields, commandSchema, commandAuthorization, server);
231
+ return CommandGeneratorResolvers_GraphQL$ReventlessLocal.register(fields, commandSchema, authorizationOf, server);
232
232
  }
233
- fields.forEach(field => CommandGeneratorResolvers_GraphQL$ReventlessLocal.registerDcb(field, commandSchema, commandAuthorization, server));
233
+ fields.forEach(field => CommandGeneratorResolvers_GraphQL$ReventlessLocal.registerDcb(field, commandSchema, authorizationOf, server));
234
234
  };
235
235
  let hooks_mutationBindHook = CommandGeneratorResolvers_GraphQL$ReventlessLocal.bindHandler;
236
- let hooks_inboundMutationResolverHook = (fieldName, externalInputSchema) => InboundTranslationResolvers_GraphQL$ReventlessLocal.register(fieldName, externalInputSchema, resolveTargetGraphQL());
236
+ let hooks_inboundMutationResolverHook = (fieldName, externalInputSchema, permission) => InboundTranslationResolvers_GraphQL$ReventlessLocal.register(fieldName, externalInputSchema, permission, resolveTargetGraphQL());
237
237
  let hooks_inboundMutationBindReceiveHook = InboundTranslationResolvers_GraphQL$ReventlessLocal.bindReceive;
238
238
  let hooks_schemaTypeRegistrationHook = sdlTypes => resolveTargetGraphQL().registerTypes(sdlTypes);
239
239
  let hooks_mcpSchemaRegistrationHook = param => {
@@ -649,7 +649,7 @@ function MakeWithConfig(Config) {
649
649
  errorSchema: UiFragmentRegistry$ReventlessCore.errorSchema,
650
650
  eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema,
651
651
  commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
652
- commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization,
652
+ authorizationOf: UiFragmentRegistry$ReventlessCore.authorizationOf,
653
653
  commandTransition: UiFragmentRegistry$ReventlessCore.commandTransition,
654
654
  traits: UiFragmentRegistry$ReventlessCore.traits,
655
655
  readConsistency: UiFragmentRegistry$ReventlessCore.readConsistency
@@ -761,7 +761,7 @@ function MakeWithConfig(Config) {
761
761
  errorSchema: PluginSpec$ReventlessCore.errorSchema,
762
762
  commandSchema: PluginSpec$ReventlessCore.commandSchema,
763
763
  moduleUrl: PluginSpec$ReventlessCore.moduleUrl,
764
- commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization,
764
+ authorizationOf: PluginSpec$ReventlessCore.authorizationOf,
765
765
  commandTransition: PluginSpec$ReventlessCore.commandTransition,
766
766
  traits: PluginSpec$ReventlessCore.traits
767
767
  })({
@@ -1985,15 +1985,15 @@ function Make($star) {
1985
1985
  }
1986
1986
  };
1987
1987
  let EventHistoryResolvers = EventHistoryResolvers_GraphQL$ReventlessLocal.Make(Bus);
1988
- let hooks_mutationResolverHook = (kind, fields, commandSchema, commandAuthorization) => {
1988
+ let hooks_mutationResolverHook = (kind, fields, commandSchema, authorizationOf) => {
1989
1989
  let server = resolveTargetGraphQL();
1990
1990
  if (kind === "Aggregate") {
1991
- return CommandGeneratorResolvers_GraphQL$ReventlessLocal.register(fields, commandSchema, commandAuthorization, server);
1991
+ return CommandGeneratorResolvers_GraphQL$ReventlessLocal.register(fields, commandSchema, authorizationOf, server);
1992
1992
  }
1993
- fields.forEach(field => CommandGeneratorResolvers_GraphQL$ReventlessLocal.registerDcb(field, commandSchema, commandAuthorization, server));
1993
+ fields.forEach(field => CommandGeneratorResolvers_GraphQL$ReventlessLocal.registerDcb(field, commandSchema, authorizationOf, server));
1994
1994
  };
1995
1995
  let hooks_mutationBindHook = CommandGeneratorResolvers_GraphQL$ReventlessLocal.bindHandler;
1996
- let hooks_inboundMutationResolverHook = (fieldName, externalInputSchema) => InboundTranslationResolvers_GraphQL$ReventlessLocal.register(fieldName, externalInputSchema, resolveTargetGraphQL());
1996
+ let hooks_inboundMutationResolverHook = (fieldName, externalInputSchema, permission) => InboundTranslationResolvers_GraphQL$ReventlessLocal.register(fieldName, externalInputSchema, permission, resolveTargetGraphQL());
1997
1997
  let hooks_inboundMutationBindReceiveHook = InboundTranslationResolvers_GraphQL$ReventlessLocal.bindReceive;
1998
1998
  let hooks_schemaTypeRegistrationHook = sdlTypes => resolveTargetGraphQL().registerTypes(sdlTypes);
1999
1999
  let hooks_mcpSchemaRegistrationHook = param => {
@@ -2409,7 +2409,7 @@ function Make($star) {
2409
2409
  errorSchema: UiFragmentRegistry$ReventlessCore.errorSchema,
2410
2410
  eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema,
2411
2411
  commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
2412
- commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization,
2412
+ authorizationOf: UiFragmentRegistry$ReventlessCore.authorizationOf,
2413
2413
  commandTransition: UiFragmentRegistry$ReventlessCore.commandTransition,
2414
2414
  traits: UiFragmentRegistry$ReventlessCore.traits,
2415
2415
  readConsistency: UiFragmentRegistry$ReventlessCore.readConsistency
@@ -2521,7 +2521,7 @@ function Make($star) {
2521
2521
  errorSchema: PluginSpec$ReventlessCore.errorSchema,
2522
2522
  commandSchema: PluginSpec$ReventlessCore.commandSchema,
2523
2523
  moduleUrl: PluginSpec$ReventlessCore.moduleUrl,
2524
- commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization,
2524
+ authorizationOf: PluginSpec$ReventlessCore.authorizationOf,
2525
2525
  commandTransition: PluginSpec$ReventlessCore.commandTransition,
2526
2526
  traits: PluginSpec$ReventlessCore.traits
2527
2527
  })({
@@ -83,17 +83,6 @@ let checkPluginStatus = (~field: string): option<JSON.t> =>
83
83
  | None => None
84
84
  }
85
85
 
86
- // Build a synthetic command value sufficient for evaluating
87
- // `commandAuthorization`. ReScript variants with record payloads compile to
88
- // `{TAG: cname, ...payload}` and the PPX-generated switch matches `Ctor(_)`
89
- // which only checks `command.TAG === cname`. Payload-less constructors
90
- // compile to bare string literals and the switch matches them with
91
- // `command === "Cname"`, so the synthetic value must be a bare string in
92
- // that case — otherwise the wildcard branch (file-level default) wins
93
- // instead of the per-constructor rule.
94
- let syntheticCommand = (cname: string, ~hasPayload: bool): unknown =>
95
- hasPayload ? {"TAG": cname}->Obj.magic : cname->Obj.magic
96
-
97
86
  let capitalize = s => s->String.charAt(0)->String.toUpperCase ++ s->String.slice(~start=1)
98
87
 
99
88
  // -- CommandResult SDL types --------------------------------------------------
@@ -139,7 +128,7 @@ let extractVariantSchema = (commandSchema: S.t<unknown>, ~index=0) =>
139
128
  // stale `_0` payload arg into the SDL. Looking up by name avoids that.
140
129
  let variantIndexForField = (commandSchema: S.t<unknown>, ~field: string): int => {
141
130
  let cname = extractCommandName(field)
142
- let allNames = Reventless.DcbTag.extractAllVariantNames(commandSchema->Obj.magic)
131
+ let allNames = Reventless.DcbTag.extractAllVariantNames(commandSchema)
143
132
  allNames->Array.indexOf(cname)
144
133
  }
145
134
 
@@ -188,7 +177,7 @@ let runCommand = async (
188
177
  let register = (
189
178
  ~fields: array<string>,
190
179
  ~commandSchema: S.t<unknown>,
191
- ~commandAuthorization: unknown => Reventless.Authorization.permission,
180
+ ~authorizationOf: string => Reventless.Authorization.permission,
192
181
  ~server: ReventlessGraphqlServer.GraphQL_ServerInstance.t,
193
182
  ) => {
194
183
  ensureCommandResultTypes(server)
@@ -210,13 +199,6 @@ let register = (
210
199
  fields->Array.forEach(field => {
211
200
  let handlerRef = ref(None)
212
201
  handlerRefs->Dict.set(field, handlerRef)
213
- // hasPayload is fixed per field — capture once at registration time so
214
- // the resolver doesn't re-walk the schema per request.
215
- let variantIndex = variantIndexForField(commandSchema, ~field)
216
- let hasPayload = switch extractVariantSchema(commandSchema, ~index=variantIndex) {
217
- | Object(_) => true
218
- | _ => false
219
- }
220
202
  let commandName = extractCommandName(field)
221
203
  let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
222
204
  _root,
@@ -229,7 +211,7 @@ let register = (
229
211
  | Some(rejected) => rejected
230
212
  | None =>
231
213
  let identity = extractIdentity(ctx)
232
- let rule = commandAuthorization(syntheticCommand(commandName, ~hasPayload))
214
+ let rule = authorizationOf(commandName)
233
215
  if !Reventless.Authorization.isAllowed(rule, identity) {
234
216
  rejectForbidden(~field)
235
217
  } else {
@@ -262,7 +244,7 @@ let register = (
262
244
  let registerDcb = (
263
245
  ~fieldName: string,
264
246
  ~commandSchema: S.t<unknown>,
265
- ~commandAuthorization: unknown => Reventless.Authorization.permission,
247
+ ~authorizationOf: string => Reventless.Authorization.permission,
266
248
  ~server: ReventlessGraphqlServer.GraphQL_ServerInstance.t,
267
249
  ) => {
268
250
  ensureCommandResultTypes(server)
@@ -271,7 +253,7 @@ let registerDcb = (
271
253
  // constructor (slice name == constructor); a multi-command slice's `Plugin_Ctor`
272
254
  // resolves to `Ctor`. Position 0 was previously hardcoded, which dropped every
273
255
  // non-first constructor of a multi-command slice.
274
- let constructorNames = Reventless.DcbTag.extractAllVariantNames(commandSchema->Obj.magic)
256
+ let constructorNames = Reventless.DcbTag.extractAllVariantNames(commandSchema)
275
257
  let variantIndex = variantIndexForField(commandSchema, ~field=fieldName)
276
258
  let variantSchema = extractVariantSchema(
277
259
  commandSchema,
@@ -284,7 +266,6 @@ let registerDcb = (
284
266
  (
285
267
  variantIndex >= 0 ? constructorNames->Array.get(variantIndex) : constructorNames->Array.get(0)
286
268
  )->Option.getOr(fieldName)
287
- let hasPayload = Reventless.DcbTag.isVariantPayloadBearing(commandSchema->Obj.magic, tag)
288
269
 
289
270
  let handlerRef = ref(None)
290
271
  handlerRefs->Dict.set(fieldName, handlerRef)
@@ -300,7 +281,7 @@ let registerDcb = (
300
281
  | Some(rejected) => rejected
301
282
  | None =>
302
283
  let identity = extractIdentity(ctx)
303
- let rule = commandAuthorization(syntheticCommand(tag, ~hasPayload))
284
+ let rule = authorizationOf(tag)
304
285
  if !Reventless.Authorization.isAllowed(rule, identity) {
305
286
  rejectForbidden(~field=fieldName)
306
287
  } else {
@@ -93,16 +93,6 @@ function checkPluginStatus(field) {
93
93
  }
94
94
  }
95
95
 
96
- function syntheticCommand(cname, hasPayload) {
97
- if (hasPayload) {
98
- return {
99
- TAG: cname
100
- };
101
- } else {
102
- return cname;
103
- }
104
- }
105
-
106
96
  function capitalize(s) {
107
97
  return s.charAt(0).toUpperCase() + s.slice(1);
108
98
  }
@@ -156,7 +146,7 @@ async function runCommand(generateCommand, payload) {
156
146
  }
157
147
  }
158
148
 
159
- function register(fields, commandSchema, commandAuthorization, server) {
149
+ function register(fields, commandSchema, authorizationOf, server) {
160
150
  server.registerTypes([commandResultSdl]);
161
151
  let sdlFields = fields.map(field => {
162
152
  let variantIndex = variantIndexForField(commandSchema, field);
@@ -173,10 +163,6 @@ function register(fields, commandSchema, commandAuthorization, server) {
173
163
  contents: undefined
174
164
  };
175
165
  handlerRefs[field] = handlerRef;
176
- let variantIndex = variantIndexForField(commandSchema, field);
177
- let match = extractVariantSchema(commandSchema, variantIndex);
178
- let hasPayload;
179
- hasPayload = match.type === "object";
180
166
  let commandName = extractCommandName(field);
181
167
  let resolver = async (_root, args, ctx) => {
182
168
  let generateCommand = handlerRef.contents;
@@ -188,7 +174,7 @@ function register(fields, commandSchema, commandAuthorization, server) {
188
174
  return rejected;
189
175
  }
190
176
  let identity = extractIdentity(ctx);
191
- let rule = commandAuthorization(syntheticCommand(commandName, hasPayload));
177
+ let rule = authorizationOf(commandName);
192
178
  if (!Authorization$Reventless.isAllowed(rule, identity)) {
193
179
  return rejectForbidden(field);
194
180
  }
@@ -210,14 +196,13 @@ function register(fields, commandSchema, commandAuthorization, server) {
210
196
  server.registerMutations(sdlFields, resolvers);
211
197
  }
212
198
 
213
- function registerDcb(fieldName, commandSchema, commandAuthorization, server) {
199
+ function registerDcb(fieldName, commandSchema, authorizationOf, server) {
214
200
  server.registerTypes([commandResultSdl]);
215
201
  let constructorNames = DcbTag$Reventless.extractAllVariantNames(commandSchema);
216
202
  let variantIndex = variantIndexForField(commandSchema, fieldName);
217
203
  let variantSchema = extractVariantSchema(commandSchema, variantIndex >= 0 ? variantIndex : 0);
218
204
  let sdlFields = [deriveSdlField(fieldName, variantSchema)];
219
205
  let tag = Stdlib_Option.getOr(variantIndex >= 0 ? constructorNames[variantIndex] : constructorNames[0], fieldName);
220
- let hasPayload = DcbTag$Reventless.isVariantPayloadBearing(commandSchema, tag);
221
206
  let handlerRef = {
222
207
  contents: undefined
223
208
  };
@@ -232,7 +217,7 @@ function registerDcb(fieldName, commandSchema, commandAuthorization, server) {
232
217
  return rejected;
233
218
  }
234
219
  let identity = extractIdentity(ctx);
235
- let rule = commandAuthorization(syntheticCommand(tag, hasPayload));
220
+ let rule = authorizationOf(tag);
236
221
  if (!Authorization$Reventless.isAllowed(rule, identity)) {
237
222
  return rejectForbidden(fieldName);
238
223
  }
@@ -296,7 +281,6 @@ export {
296
281
  resetPluginStatusGate,
297
282
  rejectPluginStatus,
298
283
  checkPluginStatus,
299
- syntheticCommand,
300
284
  capitalize,
301
285
  commandResultSdl,
302
286
  ensureCommandResultTypes,
@@ -1,39 +1,35 @@
1
- // GraphQL mutation resolvers for in-memory InboundTranslationSlices.
2
- // Registers one GraphQL mutation field per InboundTranslationSlice.
3
- // Each resolver calls `receive(argsJson)` directly — the InboundTranslationSlice
4
- // handles parsing, validation, translation, and command publishing internally.
1
+ // GraphQL mutation resolvers for in-memory InboundTranslationSlices: one field per
2
+ // slice, gated by the door's rule and handing the caller on to `receive`.
5
3
 
6
4
  @@warning("-44")
7
5
  open ReventlessCore
8
6
 
9
- // Mutable registry: fieldName → receive function.
10
- // Pre-populated synchronously in Phase 1 via a queuing forwarder so callers
11
- // (e.g. PlatformInspector's onPlatformDeployedHook) can invoke receive before
12
- // Phase 2 bindReceive runs. Calls park in a Promise queue and drain once bound.
13
- let receiveRegistry: dict<
14
- JSON.t => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
15
- > = Dict.make()
7
+ type receive = (
8
+ JSON.t,
9
+ ~caller: Reventless.Identity.t=?,
10
+ ) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>
11
+
12
+ // fieldName → receive. Pre-populated with a queuing forwarder so a call made
13
+ // before `bindReceive` parks until it runs.
14
+ let receiveRegistry: dict<receive> = Dict.make()
16
15
 
17
16
  type pendingCall = {
18
17
  inputJson: JSON.t,
18
+ caller: option<Reventless.Identity.t>,
19
19
  resolve: ReventlessInfra.InboundTranslationSlice.receiveResult => unit,
20
20
  }
21
21
 
22
- // Per-field pending-call queues — drained when bindReceive fires.
23
22
  let pendingQueueRegistry: dict<ref<array<pendingCall>>> = Dict.make()
24
23
 
25
- // Phase 1: Register SDL + resolver stub synchronously (before server starts).
26
- // Also pre-registers a queuing forwarder in receiveRegistry so callers can
27
- // invoke receive immediately; calls are parked until bindReceive drains them.
24
+ // Phase 1: register SDL + resolver stub synchronously (before server starts).
25
+ // `permission` is the door's rule; without one the field admits nobody.
28
26
  let register = (
29
27
  ~fieldName: string,
30
28
  ~externalInputSchema: S.t<unknown>,
29
+ ~permission: option<Reventless.Authorization.permission>=?,
31
30
  ~server: ReventlessGraphqlServer.GraphQL_ServerInstance.t,
32
31
  ) => {
33
- // The mutation field returns CommandResult, so its union members must exist in
34
- // this scope's document. Registered here rather than relying on a plugin's
35
- // command handlers having registered them first — a plugin whose only mutation
36
- // is an inbound translation must not depend on registration order.
32
+ // Registered here, so a plugin whose only mutation is inbound has its result union.
37
33
  CommandGeneratorResolvers_GraphQL.ensureCommandResultTypes(server)
38
34
 
39
35
  let sdlFields = switch GraphQL_FragmentGenerator.deriveMutationFieldFromObject(
@@ -46,28 +42,32 @@ let register = (
46
42
  | None => [` ${fieldName}: CommandResult!`]
47
43
  }
48
44
 
49
- // Queue of pending calls when receive is not yet bound.
50
45
  let pendingQueue: ref<array<pendingCall>> = ref([])
51
46
  pendingQueueRegistry->Dict.set(fieldName, pendingQueue)
52
47
 
53
- // Queuing forwarder: parks calls until bindReceive populates receiveRegistry.
54
- let queuingReceive = (inputJson: JSON.t) =>
48
+ let queuingReceive: receive = (inputJson, ~caller=?) =>
55
49
  Promise.make((resolve, _reject) => {
56
- pendingQueue.contents->Array.push({inputJson, resolve})
50
+ pendingQueue.contents->Array.push({inputJson, caller, resolve})
57
51
  })
58
52
  receiveRegistry->Dict.set(fieldName, queuingReceive)
59
53
 
60
54
  let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
61
55
  _root,
62
56
  args,
63
- _ctx,
57
+ ctx,
64
58
  ) => {
65
- let inputJson: JSON.t = args->Obj.magic
66
- let receive = receiveRegistry->Dict.getUnsafe(fieldName)
67
- let result = await receive(inputJson)
68
- result
69
- ->InboundTranslationSlice_Callback.receiveResultToOutcome
70
- ->CommandTopic.commandOutcomeToJson
59
+ let caller = CommandGeneratorResolvers_GraphQL.extractIdentity(ctx)
60
+ let admitted =
61
+ permission->Option.mapOr(false, rule => Reventless.Authorization.isAllowed(rule, caller))
62
+ if !admitted {
63
+ CommandGeneratorResolvers_GraphQL.rejectForbidden(~field=fieldName)
64
+ } else {
65
+ let receive = receiveRegistry->Dict.getUnsafe(fieldName)
66
+ let result = await receive(args->Obj.magic, ~caller)
67
+ result
68
+ ->InboundTranslationSlice_Callback.receiveResultToOutcome
69
+ ->CommandTopic.commandOutcomeToJson
70
+ }
71
71
  }
72
72
 
73
73
  let resolvers = Dict.make()
@@ -75,19 +75,15 @@ let register = (
75
75
  server.registerMutations(~sdlFields, ~resolvers)
76
76
  }
77
77
 
78
- // Phase 2: Bind the real receive function once Output.apply resolves.
79
- // Replaces the queuing forwarder in receiveRegistry and drains pending calls.
80
- let bindReceive = (
81
- ~fieldName: string,
82
- ~receive: JSON.t => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
83
- ) => {
78
+ // Phase 2: bind the real receive, replacing the forwarder and draining its queue.
79
+ let bindReceive = (~fieldName: string, ~receive: receive) => {
84
80
  receiveRegistry->Dict.set(fieldName, receive)
85
81
  switch pendingQueueRegistry->Dict.get(fieldName) {
86
82
  | Some(pendingQueue) =>
87
83
  let pending = pendingQueue.contents
88
84
  pendingQueue.contents = []
89
- pending->Array.forEach(({inputJson, resolve}) => {
90
- let _ = receive(inputJson)->Promise.thenResolve(result => resolve(result))
85
+ pending->Array.forEach(({inputJson, caller, resolve}) => {
86
+ let _ = receive(inputJson, ~caller?)->Promise.thenResolve(result => resolve(result))
91
87
  })
92
88
  | None => ()
93
89
  }
@@ -1,5 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
3
5
  import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
4
6
  import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
5
7
  import * as InboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
@@ -9,7 +11,7 @@ let receiveRegistry = {};
9
11
 
10
12
  let pendingQueueRegistry = {};
11
13
 
12
- function register(fieldName, externalInputSchema, server) {
14
+ function register(fieldName, externalInputSchema, permission, server) {
13
15
  CommandGeneratorResolvers_GraphQL$ReventlessLocal.ensureCommandResultTypes(server);
14
16
  let field = GraphQL_FragmentGenerator$ReventlessCore.deriveMutationFieldFromObject(fieldName, [], new Set(), externalInputSchema);
15
17
  let sdlFields = field !== undefined ? [field] : [` ` + fieldName + `: CommandResult!`];
@@ -17,16 +19,22 @@ function register(fieldName, externalInputSchema, server) {
17
19
  contents: []
18
20
  };
19
21
  pendingQueueRegistry[fieldName] = pendingQueue;
20
- let queuingReceive = inputJson => new Promise((resolve, _reject) => {
22
+ let queuingReceive = (inputJson, caller) => new Promise((resolve, _reject) => {
21
23
  pendingQueue.contents.push({
22
24
  inputJson: inputJson,
25
+ caller: caller,
23
26
  resolve: resolve
24
27
  });
25
28
  });
26
29
  receiveRegistry[fieldName] = queuingReceive;
27
- let resolver = async (_root, args, _ctx) => {
30
+ let resolver = async (_root, args, ctx) => {
31
+ let caller = CommandGeneratorResolvers_GraphQL$ReventlessLocal.extractIdentity(ctx);
32
+ let admitted = Stdlib_Option.mapOr(permission, false, rule => Authorization$Reventless.isAllowed(rule, caller));
33
+ if (!admitted) {
34
+ return CommandGeneratorResolvers_GraphQL$ReventlessLocal.rejectForbidden(fieldName);
35
+ }
28
36
  let receive = receiveRegistry[fieldName];
29
- let result = await receive(args);
37
+ let result = await receive(args, caller);
30
38
  return CommandTopic$ReventlessCore.commandOutcomeToJson(InboundTranslationSlice_Callback$ReventlessCore.receiveResultToOutcome(result));
31
39
  };
32
40
  let resolvers = {};
@@ -44,7 +52,7 @@ function bindReceive(fieldName, receive) {
44
52
  pendingQueue.contents = [];
45
53
  pending.forEach(param => {
46
54
  let resolve = param.resolve;
47
- receive(param.inputJson).then(result => resolve(result));
55
+ receive(param.inputJson, param.caller).then(result => resolve(result));
48
56
  });
49
57
  }
50
58
 
@@ -54,4 +62,4 @@ export {
54
62
  register,
55
63
  bindReceive,
56
64
  }
57
- /* CommandTopic-ReventlessCore Not a pure module */
65
+ /* Authorization-Reventless Not a pure module */
@@ -21,7 +21,7 @@
21
21
  // failure, and cannot afford to read it as a session that has ended.
22
22
  //
23
23
  // Mutations are wrapped (in addition to their per-command
24
- // `commandAuthorization` rule inside `CommandGeneratorResolvers_GraphQL.register`)
24
+ // `authorizationOf` rule inside `CommandGeneratorResolvers_GraphQL.register`)
25
25
  // because the per-command rule defaults to `AllowAuthenticated` and the
26
26
  // in-memory adapter falls back to `defaultUser` on missing-bearer requests
27
27
  // — without the wrapper, anonymous callers could trigger admin mutations
@@ -3,7 +3,7 @@
3
3
  // constructor `Add({name})` defaulting to AllowAuthenticated, plus a
4
4
  // payload-less `Archive` constrained to AllowRoles([Admin]).
5
5
  //
6
- // The PPX emits a `commandAuthorization` switch at compile time (see
6
+ // The PPX emits an `authorizationOf` switch at compile time (see
7
7
  // examples/online-shop-hybrid/catalog/.../Category.res.mjs). This test hand-
8
8
  // writes an equivalent switch and feeds it through the live resolver path
9
9
  // (CommandGeneratorResolvers_GraphQL.register), so the test covers the same
@@ -25,11 +25,10 @@ type command =
25
25
 
26
26
  // Matches the shape the PPX emits for `@authorize(AllowRoles([Admin]))` on
27
27
  // the payload-less `Archive` constructor with a file-level default.
28
- let commandAuthorization = (cmd: unknown): Reventless.Authorization.permission =>
29
- if (cmd->Obj.magic: 'a) === "Archive" {
30
- AllowRoles([Reventless.Role.make("Admin")])
31
- } else {
32
- AllowAuthenticated
28
+ let authorizationOf = (name: string): Reventless.Authorization.permission =>
29
+ switch name {
30
+ | "Archive" => AllowRoles([Reventless.Role.make("Admin")])
31
+ | _ => AllowAuthenticated
33
32
  }
34
33
 
35
34
  // ── Identity fixtures ────────────────────────────────────────────────────────
@@ -84,7 +83,7 @@ let buildFixture = (~namespace: string) => {
84
83
  CommandGeneratorResolvers_GraphQL.register(
85
84
  ~fields=[addField, renameField, archiveField],
86
85
  ~commandSchema=commandSchema->S.castToUnknown,
87
- ~commandAuthorization,
86
+ ~authorizationOf,
88
87
  ~server,
89
88
  )
90
89
 
@@ -242,7 +241,7 @@ let failingFixture = (~namespace: string, ~failWith: unit => unit) => {
242
241
  CommandGeneratorResolvers_GraphQL.register(
243
242
  ~fields=[field],
244
243
  ~commandSchema=commandSchema->S.castToUnknown,
245
- ~commandAuthorization,
244
+ ~authorizationOf,
246
245
  ~server,
247
246
  )
248
247
  let generateCommand: ReventlessCore.CommandGenerator.commandGenerator = _payload =>
@@ -28,8 +28,8 @@ let commandSchema = Sury.union([
28
28
  Sury.literal("Archive")
29
29
  ]);
30
30
 
31
- function commandAuthorization(cmd) {
32
- if (cmd === "Archive") {
31
+ function authorizationOf(name) {
32
+ if (name === "Archive") {
33
33
  return {
34
34
  TAG: "AllowRoles",
35
35
  _0: ["Admin"]
@@ -91,7 +91,7 @@ function buildFixture(namespace) {
91
91
  addField,
92
92
  renameField,
93
93
  archiveField
94
- ], commandSchema, commandAuthorization, DomainGraphQL_Server$ReventlessLocal.asInterface);
94
+ ], commandSchema, authorizationOf, DomainGraphQL_Server$ReventlessLocal.asInterface);
95
95
  let stubGenerateCommand = payload => Effect.promise(() => {
96
96
  calls.contents.push(payload.command);
97
97
  return Promise.resolve({
@@ -203,7 +203,7 @@ globalThis.describe("CommandGeneratorResolvers_GraphQL — per-constructor autho
203
203
 
204
204
  function failingFixture(namespace, failWith) {
205
205
  let field = namespace + `_Add`;
206
- CommandGeneratorResolvers_GraphQL$ReventlessLocal.register([field], commandSchema, commandAuthorization, DomainGraphQL_Server$ReventlessLocal.asInterface);
206
+ CommandGeneratorResolvers_GraphQL$ReventlessLocal.register([field], commandSchema, authorizationOf, DomainGraphQL_Server$ReventlessLocal.asInterface);
207
207
  let generateCommand = _payload => Effect.promise(() => {
208
208
  failWith();
209
209
  return Promise.resolve({
@@ -260,7 +260,7 @@ globalThis.describe("CommandGeneratorResolvers_GraphQL — a failure the caller
260
260
 
261
261
  export {
262
262
  commandSchema,
263
- commandAuthorization,
263
+ authorizationOf,
264
264
  adminIdentity,
265
265
  userIdentity,
266
266
  ctxFor,