@reventlessdev/reventless-aws 3.0.0-alpha.196 → 3.0.0-alpha.198

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 (29) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +81 -54
  3. package/package.json +12 -12
  4. package/src/Platform.res +290 -44
  5. package/src/Platform.res.mjs +416 -90
  6. package/src/adapter/Api/ApiFragmentDeregistration.res +137 -0
  7. package/src/adapter/Api/ApiFragmentDeregistration.res.mjs +108 -0
  8. package/src/adapter/Api/CommandSubscriptionResolvers_AppSync.res +4 -3
  9. package/src/adapter/Api/Platform_ApiFragments_Lambda.res +212 -0
  10. package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +202 -0
  11. package/src/adapter/Api/Platform_UIFragments_Lambda.res +8 -8
  12. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +11 -11
  13. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +2 -1
  14. package/src/adapter/QueryDb/QueryDbBackend.res +1 -1
  15. package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +235 -9
  16. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +4 -0
  17. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +5 -0
  18. package/src/components/Api/AppSync_Adapter.res +36 -59
  19. package/src/components/Api/AppSync_Adapter.res.mjs +18 -74
  20. package/src/components/Api/AppSync_SdlDecorate.res +189 -0
  21. package/src/components/Api/AppSync_SdlDecorate.res.mjs +125 -0
  22. package/src/plugin/runtime/PluginRuntime_Builder.res +118 -2
  23. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +70 -7
  24. package/src/util/Util_AppSync_Caller.res +12 -4
  25. package/src/util/Util_AppSync_Caller.res.mjs +9 -3
  26. package/tests/AppSync_AdapterTest.res +6 -0
  27. package/tests/AppSync_AdapterTest.res.mjs +12 -6
  28. package/tests/AppSync_SdlDecorateTest.res +157 -0
  29. package/tests/AppSync_SdlDecorateTest.res.mjs +147 -0
@@ -30,7 +30,11 @@ let configRef = {
30
30
  schedulerQueueArn: undefined,
31
31
  schedulerQueueName: undefined,
32
32
  appSyncApiId: undefined,
33
- clonerEnabled: false
33
+ clonerEnabled: false,
34
+ platformApiId: undefined,
35
+ apiFragmentRegistryTableName: undefined,
36
+ adminDcbCmdTopicUrl: undefined,
37
+ splitApi: false
34
38
  }
35
39
  };
36
40
 
@@ -104,8 +108,9 @@ function setStateChangesConfig(sync, async, param) {
104
108
  });
105
109
  }
106
110
 
107
- function registerConfig(eventTopicArn, pluginReadModelTableName, pluginSchemaPersistenceTableName, schedulerRoleArn, schedulerQueueArn, schedulerQueueName, appSyncApiId, clonerEnabledOpt, param) {
111
+ function registerConfig(eventTopicArn, pluginReadModelTableName, pluginSchemaPersistenceTableName, schedulerRoleArn, schedulerQueueArn, schedulerQueueName, appSyncApiId, clonerEnabledOpt, platformApiId, apiFragmentRegistryTableName, adminDcbCmdTopicUrl, splitApiOpt, param) {
108
112
  let clonerEnabled = clonerEnabledOpt !== undefined ? clonerEnabledOpt : false;
113
+ let splitApi = splitApiOpt !== undefined ? splitApiOpt : false;
109
114
  configRef.contents = {
110
115
  eventTopicArn: eventTopicArn,
111
116
  pluginReadModelTableName: pluginReadModelTableName,
@@ -114,7 +119,11 @@ function registerConfig(eventTopicArn, pluginReadModelTableName, pluginSchemaPer
114
119
  schedulerQueueArn: schedulerQueueArn,
115
120
  schedulerQueueName: schedulerQueueName,
116
121
  appSyncApiId: appSyncApiId,
117
- clonerEnabled: clonerEnabled
122
+ clonerEnabled: clonerEnabled,
123
+ platformApiId: platformApiId,
124
+ apiFragmentRegistryTableName: apiFragmentRegistryTableName,
125
+ adminDcbCmdTopicUrl: adminDcbCmdTopicUrl,
126
+ splitApi: splitApi
118
127
  };
119
128
  }
120
129
 
@@ -136,11 +145,12 @@ function Make(EventCollectorChannel) {
136
145
  };
137
146
  let synthesizeAdminContext = () => {
138
147
  let config = configRef.contents;
139
- let fakePluginDefinitionJson = Pulumi.output(`{"id":"Admin@INTERNAL","name":"Admin","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"uiFragments":null,"structure":null}`);
148
+ let fakePluginDefinitionJson = Pulumi.output(`{"id":"Admin@INTERNAL","name":"Admin","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"structure":null}`);
140
149
  let arn = config.eventTopicArn;
141
150
  let adminEpEventTopicArn = arn !== undefined ? arn : Pulumi.output("NOT_AVAILABLE");
142
151
  return {
143
152
  pluginDefinitionJson: fakePluginDefinitionJson,
153
+ uiFragmentsJson: Pulumi.output("null"),
144
154
  extensionPoints: [{
145
155
  specModule: Plugin_Helpers$ReventlessCore.adminPluginExtensionPointSpecModule,
146
156
  mappingsModule: Plugin_Helpers$ReventlessCore.adminPluginExtensionPointMappingsModule,
@@ -200,7 +210,10 @@ function Make(EventCollectorChannel) {
200
210
  outputOrPlaceholder(config.schedulerQueueName),
201
211
  outputOrPlaceholder(config.appSyncApiId),
202
212
  epEventTopicArnsOutput,
203
- outputOrPlaceholder(config.pluginSchemaPersistenceTableName)
213
+ outputOrPlaceholder(config.pluginSchemaPersistenceTableName),
214
+ outputOrPlaceholder(config.platformApiId),
215
+ outputOrPlaceholder(config.apiFragmentRegistryTableName),
216
+ outputOrPlaceholder(config.adminDcbCmdTopicUrl)
204
217
  ]).apply(values => {
205
218
  let queueUrl = values[0];
206
219
  let pluginEpCmdTopicUrl = values[1];
@@ -212,6 +225,9 @@ function Make(EventCollectorChannel) {
212
225
  let appSyncApiId = values[7];
213
226
  let epEventTopicArns = values[8];
214
227
  let schemaPersistenceTable = values[9];
228
+ let platformApiId = values[10];
229
+ let apiFragmentRegistryTable = values[11];
230
+ let adminDcbCmdTopicUrl = values[12];
215
231
  let dict = {};
216
232
  dict["queueUrl"] = queueUrl;
217
233
  dict["pluginExtensionPointCmdTopicUrl"] = pluginEpCmdTopicUrl;
@@ -219,6 +235,10 @@ function Make(EventCollectorChannel) {
219
235
  dict["pluginReadModelTableName"] = rmTable;
220
236
  dict["pluginSchemaPersistenceTableName"] = schemaPersistenceTable;
221
237
  dict["appSyncApiId"] = appSyncApiId;
238
+ dict["platformApiId"] = platformApiId;
239
+ dict["apiFragmentRegistryTableName"] = apiFragmentRegistryTable;
240
+ dict["adminDcbCmdTopicUrl"] = adminDcbCmdTopicUrl;
241
+ dict["splitApi"] = config.splitApi;
222
242
  dict["clonerEnabled"] = config.clonerEnabled;
223
243
  dict["schedulerRoleArn"] = schedRoleArn;
224
244
  dict["schedulerQueueArn"] = schedQueueArn;
@@ -305,9 +325,13 @@ function Make(EventCollectorChannel) {
305
325
  }
306
326
  packageDirs["@reventlessdev/reventless-aws"] = Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-aws");
307
327
  packageDirs["@reventlessdev/reventless-core"] = Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-core");
308
- let bundleOutput = context.pluginDefinitionJson.apply(pluginDefinitionJson => {
328
+ let bundleOutput = Pulumi.all([
329
+ context.pluginDefinitionJson,
330
+ context.uiFragmentsJson
331
+ ]).apply(param => {
309
332
  let extraStringAssets = {};
310
- extraStringAssets["pluginDefinition.json"] = pluginDefinitionJson;
333
+ extraStringAssets["pluginDefinition.json"] = param[0];
334
+ extraStringAssets["uiFragments.json"] = param[1];
311
335
  return Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs", packageDirs, extraStringAssets);
312
336
  });
313
337
  let codeOutput = bundleOutput.apply(b => b.code);
@@ -372,6 +396,45 @@ function Make(EventCollectorChannel) {
372
396
  role: runtime.parts.lambdaRole.id
373
397
  });
374
398
  }
399
+ let tableOutput = config.apiFragmentRegistryTableName;
400
+ if (tableOutput !== undefined) {
401
+ let policyJson$2 = tableOutput.apply(tableName => PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `ApiFragmentsScanPolicy`, [{
402
+ Sid: "AllowAdminScanApiFragments",
403
+ Effect: "Allow",
404
+ Action: ["dynamodb:Scan"],
405
+ Resource: "arn:aws:dynamodb:*:*:table/" + tableName
406
+ }])));
407
+ new (Aws.iam.RolePolicy)(name + `-apiFragmentsScan`, {
408
+ policy: policyJson$2,
409
+ role: runtime.parts.lambdaRole.id
410
+ });
411
+ }
412
+ let urlOutput = config.adminDcbCmdTopicUrl;
413
+ if (urlOutput !== undefined) {
414
+ let policyJson$3 = urlOutput.apply(url => {
415
+ let match = url.split("/");
416
+ let arn;
417
+ if (match.length !== 5) {
418
+ arn = url;
419
+ } else {
420
+ let host = match[2];
421
+ let acct = match[3];
422
+ let qn = match[4];
423
+ let region = Stdlib_Option.getOr(host.split(".")[1], "eu-west-1");
424
+ arn = `arn:aws:sqs:` + region + `:` + acct + `:` + qn;
425
+ }
426
+ return PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `AdminDcbSendPolicy`, [{
427
+ Sid: "AllowAdminDispatchRecordApiFragmentPush",
428
+ Effect: "Allow",
429
+ Action: ["sqs:SendMessage"],
430
+ Resource: arn
431
+ }]));
432
+ });
433
+ new (Aws.iam.RolePolicy)(name + `-adminDcbSend`, {
434
+ policy: policyJson$3,
435
+ role: runtime.parts.lambdaRole.id
436
+ });
437
+ }
375
438
  }
376
439
  let aggregateNameSet = {};
377
440
  context.extensions.forEach(ext => {
@@ -225,9 +225,17 @@ let sendMutation = async (
225
225
  let response = await fetch(endpoint, {method: "POST", headers: signed.headers, body: signed.body})
226
226
 
227
227
  let json = await response->responseJson
228
- switch json->JSON.Decode.object->Option.flatMap(d => d->Dict.get("errors")) {
229
- | Some(errors) =>
230
- log.error(~comp="Util_AppSync_Caller", ~data=errors, `${mutation} errors`)
231
- | None => ()
228
+ // Returns the `data` object (or None on GraphQL errors) so a caller can inspect
229
+ // the result — e.g. the deploy-time RegisterApiFragment caller reads the
230
+ // CommandResult __typename / eventCount to decide whether to wait for a push.
231
+ switch json->JSON.Decode.object {
232
+ | Some(d) =>
233
+ switch d->Dict.get("errors") {
234
+ | Some(errors) =>
235
+ log.error(~comp="Util_AppSync_Caller", ~data=errors, `${mutation} errors`)
236
+ None
237
+ | None => d->Dict.get("data")
238
+ }
239
+ | None => None
232
240
  }
233
241
  }
@@ -1,7 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
- import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
4
  import * as HashNode from "@smithy/hash-node";
6
5
  import * as SignatureV4 from "@smithy/signature-v4";
7
6
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
@@ -155,9 +154,16 @@ async function sendMutation(endpoint, region, mutation, selectionOpt, variables)
155
154
  body: signed.body
156
155
  });
157
156
  let json = await response.json();
158
- let errors = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["errors"]);
157
+ let d$1 = Stdlib_JSON.Decode.object(json);
158
+ if (d$1 === undefined) {
159
+ return;
160
+ }
161
+ let errors = d$1["errors"];
159
162
  if (errors !== undefined) {
160
- return log.error("Util_AppSync_Caller", errors, mutation + ` errors`);
163
+ log.error("Util_AppSync_Caller", errors, mutation + ` errors`);
164
+ return;
165
+ } else {
166
+ return d$1["data"];
161
167
  }
162
168
  }
163
169
 
@@ -140,6 +140,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
140
140
  mutations: mutationFields,
141
141
  queries: queryFields,
142
142
  subscriptions: [],
143
+ subscriptionSources: [],
143
144
  })
144
145
 
145
146
  let mutationEntry = (
@@ -361,6 +362,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
361
362
  mutations: mutationFields,
362
363
  queries: queryFields,
363
364
  subscriptions: [],
365
+ subscriptionSources: [],
364
366
  })
365
367
 
366
368
  testSync("systemCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
@@ -500,6 +502,7 @@ describe("AppSync_Adapter — type-level dual-auth", () => {
500
502
  mutations: [],
501
503
  queries: queryFields,
502
504
  subscriptions: [],
505
+ subscriptionSources: [],
503
506
  })
504
507
 
505
508
  let callableEntry: ReventlessInfra.Api.querySchemaEntry = {
@@ -606,6 +609,7 @@ describe("Split mode — empty base fragment", () => {
606
609
  mutations: [],
607
610
  queries: [],
608
611
  subscriptions: [],
612
+ subscriptionSources: [],
609
613
  })
610
614
  let parts = decodeFragment(emptyFragment)
611
615
  expect(parts.types)->toHaveLength(0)
@@ -619,12 +623,14 @@ describe("Split mode — empty base fragment", () => {
619
623
  mutations: [],
620
624
  queries: [],
621
625
  subscriptions: [],
626
+ subscriptionSources: [],
622
627
  })
623
628
  let pluginFragment = ReventlessCore.GraphQL_Stitcher.encode({
624
629
  types: [`type MyPlugin_Item { id: ID! }`],
625
630
  mutations: [`MyPlugin_Item_Create(id: ID!): String`],
626
631
  queries: [`MyPlugin_Item(id: ID!): MyPlugin_Item`],
627
632
  subscriptions: [],
633
+ subscriptionSources: [],
628
634
  })
629
635
  let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
630
636
  ~baseFragment=emptyBase,
@@ -103,7 +103,8 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
103
103
  types: [],
104
104
  mutations: mutationFields,
105
105
  queries: queryFields,
106
- subscriptions: []
106
+ subscriptions: [],
107
+ subscriptionSources: []
107
108
  });
108
109
  let mutationEntry = (fieldNames, fieldPermissions) => ({
109
110
  fieldNames: fieldNames,
@@ -287,7 +288,8 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
287
288
  types: [],
288
289
  mutations: mutationFields,
289
290
  queries: queryFields,
290
- subscriptions: []
291
+ subscriptions: [],
292
+ subscriptionSources: []
291
293
  });
292
294
  globalThis.test("systemCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
293
295
  let entry_fieldNames = ["p_Sync"];
@@ -464,7 +466,8 @@ globalThis.describe("AppSync_Adapter — type-level dual-auth", () => {
464
466
  types: types,
465
467
  mutations: [],
466
468
  queries: queryFields,
467
- subscriptions: []
469
+ subscriptions: [],
470
+ subscriptionSources: []
468
471
  });
469
472
  let callableEntry_permission = {
470
473
  TAG: "AllowGroups",
@@ -565,7 +568,8 @@ globalThis.describe("Split mode — empty base fragment", () => {
565
568
  types: [],
566
569
  mutations: [],
567
570
  queries: [],
568
- subscriptions: []
571
+ subscriptions: [],
572
+ subscriptionSources: []
569
573
  });
570
574
  let parts = GraphQL_Stitcher$ReventlessCore.decode(emptyFragment);
571
575
  globalThis.expect(parts.types).toHaveLength(0);
@@ -577,13 +581,15 @@ globalThis.describe("Split mode — empty base fragment", () => {
577
581
  types: [],
578
582
  mutations: [],
579
583
  queries: [],
580
- subscriptions: []
584
+ subscriptions: [],
585
+ subscriptionSources: []
581
586
  });
582
587
  let pluginFragment = GraphQL_Stitcher$ReventlessCore.encode({
583
588
  types: [`type MyPlugin_Item { id: ID! }`],
584
589
  mutations: [`MyPlugin_Item_Create(id: ID!): String`],
585
590
  queries: [`MyPlugin_Item(id: ID!): MyPlugin_Item`],
586
- subscriptions: []
591
+ subscriptions: [],
592
+ subscriptionSources: []
587
593
  });
588
594
  let sdl = GraphQL_Stitcher$ReventlessCore.stitch(emptyBase, [pluginFragment]);
589
595
  globalThis.expect(sdl).toContain("MyPlugin_Item_Create");
@@ -0,0 +1,157 @@
1
+ // AppSync_SdlDecorate — push-time @aws_subscribe injection from the neutral
2
+ // subscription→mutation source metadata. Core emits directive-free SDL; the
3
+ // AWS provider appends its dialect onto the STITCHED schema here.
4
+
5
+ open JestGlobals
6
+
7
+ let sources: array<ReventlessCore.GraphQL_Stitcher.subscriptionSource> = [
8
+ {field: "onCatalog_AddProduct", mutations: ["Catalog_AddProduct"]},
9
+ {
10
+ field: "onUIFragmentChange",
11
+ mutations: [
12
+ "Platform_UIFragmentRegistered",
13
+ "Platform_UIFragmentUpdated",
14
+ "Platform_UIFragmentDeregistered",
15
+ ],
16
+ },
17
+ ]
18
+
19
+ let stitchedSdl = `type UIFragmentChangeEvent {
20
+ pluginId: ID!
21
+ }
22
+
23
+ type Query {
24
+ node(id: ID!): Node
25
+ }
26
+
27
+ type Mutation {
28
+ Catalog_AddProduct(input: AddInput): CommandResult
29
+ }
30
+
31
+ type Subscription {
32
+ onCatalog_AddProduct(id: ID): CommandResult
33
+ onUIFragmentChange: UIFragmentChangeEvent
34
+ @aws_auth(cognito_groups: ["Admin"])
35
+ onCatalogEventLog_eventAppended: CatalogEventLogEvent
36
+ }`
37
+
38
+ describe("AppSync_SdlDecorate.injectAwsSubscribe", () => {
39
+ testSync("appends @aws_subscribe on a 1:1 mutation-sourced field (with args)", () => {
40
+ let sdl = AppSync_SdlDecorate.injectAwsSubscribe(stitchedSdl, ~sources)
41
+ expect(sdl)->toContain(
42
+ `onCatalog_AddProduct(id: ID): CommandResult\n @aws_subscribe(mutations: ["Catalog_AddProduct"])`,
43
+ )
44
+ })
45
+
46
+ testSync("appends the many-mutations fan-in on a no-arg field, keeping @aws_auth", () => {
47
+ let sdl = AppSync_SdlDecorate.injectAwsSubscribe(stitchedSdl, ~sources)
48
+ expect(sdl)->toContain(
49
+ `onUIFragmentChange: UIFragmentChangeEvent\n @aws_subscribe(mutations: ["Platform_UIFragmentRegistered", "Platform_UIFragmentUpdated", "Platform_UIFragmentDeregistered"])`,
50
+ )
51
+ // The auth directive line appended by injectAwsAuthAll survives.
52
+ expect(sdl)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
53
+ })
54
+
55
+ testSync("leaves unmapped fields (Source A) and non-Subscription blocks untouched", () => {
56
+ let sdl = AppSync_SdlDecorate.injectAwsSubscribe(stitchedSdl, ~sources)
57
+ expect(sdl)->toContain(`onCatalogEventLog_eventAppended: CatalogEventLogEvent\n}`)
58
+ // The Mutation field shares its leading name with a source's mutation —
59
+ // it must not be decorated (only the Subscription block is).
60
+ expect(sdl)->toContain(`Catalog_AddProduct(input: AddInput): CommandResult\n}`)
61
+ })
62
+
63
+ testSync("no-op on empty sources or an SDL without a Subscription block", () => {
64
+ expect(AppSync_SdlDecorate.injectAwsSubscribe(stitchedSdl, ~sources=[]))->toBe(stitchedSdl)
65
+ let noSubs = `type Query {\n node(id: ID!): Node\n}`
66
+ expect(AppSync_SdlDecorate.injectAwsSubscribe(noSubs, ~sources))->toBe(noSubs)
67
+ })
68
+ })
69
+
70
+ describe("AppSync_Adapter.stitchWithAwsDirectives", () => {
71
+ testSync("assembles neutral fragments into AWS-dialect SDL", () => {
72
+ let baseFragment = ReventlessCore.GraphQL_Stitcher.encode({
73
+ types: [],
74
+ mutations: [` Platform_PluginStatusChanged(pluginId: ID!, status: PluginStatus!): PluginStatusChangeEvent`],
75
+ queries: [],
76
+ subscriptions: [` onPluginStatusChange: PluginStatusChangeEvent`],
77
+ subscriptionSources: [
78
+ {field: "onPluginStatusChange", mutations: ["Platform_PluginStatusChanged"]},
79
+ ],
80
+ })
81
+ let pluginFragment = ReventlessCore.GraphQL_Stitcher.encode({
82
+ types: [`type PluginStatusChangeEvent {\n pluginId: ID!\n}`],
83
+ mutations: [` Catalog_AddProduct(input: AddInput): CommandResult`],
84
+ queries: [],
85
+ subscriptions: [` onCatalog_AddProduct(id: ID): CommandResult`],
86
+ subscriptionSources: [{field: "onCatalog_AddProduct", mutations: ["Catalog_AddProduct"]}],
87
+ })
88
+ let sdl = AppSync_Adapter.stitchWithAwsDirectives(
89
+ ~baseFragment,
90
+ ~pluginFragments=[pluginFragment],
91
+ )
92
+ expect(sdl)->toContain(`@aws_subscribe(mutations: ["Platform_PluginStatusChanged"])`)
93
+ expect(sdl)->toContain(`@aws_subscribe(mutations: ["Catalog_AddProduct"])`)
94
+ })
95
+ })
96
+
97
+ describe("AppSync_SdlDecorate.planAwsPushes", () => {
98
+ // Neutral admin base: a system-callable mutation + a shared traversal type.
99
+ let rawAdminBase = ReventlessCore.GraphQL_Stitcher.encode({
100
+ types: [`type CommandAccepted {\n id: ID!\n}`],
101
+ mutations: [` Platform_RegisterApiFragment(input: ApiFragmentInput): CommandResult`],
102
+ queries: [],
103
+ subscriptions: [],
104
+ subscriptionSources: [],
105
+ })
106
+ let iamFieldNames = ["Platform_RegisterApiFragment"]
107
+ let mkFrag = (~mutation, ~target): AppSync_SdlDecorate.targetedFragmentInput => {
108
+ encoded: ReventlessCore.GraphQL_Stitcher.encode({
109
+ types: [],
110
+ mutations: [mutation],
111
+ queries: [],
112
+ subscriptions: [],
113
+ subscriptionSources: [],
114
+ }).encoded,
115
+ protocol: "graphql",
116
+ target,
117
+ }
118
+ let platformFrag = mkFrag(~mutation=` Inspector_Ping: CommandResult`, ~target="Platform")
119
+ let domainFrag = mkFrag(~mutation=` Catalog_AddProduct(input: AddInput): CommandResult`, ~target="Domain")
120
+
121
+ testSync("split mode: Platform API carries admin base (dual-auth) + Platform frags; Domain API excludes the admin base", () => {
122
+ let plans = AppSync_SdlDecorate.planAwsPushes(
123
+ ~rawAdminBase,
124
+ ~iamFieldNames,
125
+ ~fragments=[platformFrag, domainFrag],
126
+ ~splitApi=true,
127
+ )
128
+ expect(plans->Array.length)->toBe(2)
129
+ let platformPlan = plans->Array.find(p => p.api == "PlatformApi")->Option.getOrThrow
130
+ let domainPlan = plans->Array.find(p => p.api == "DomainApi")->Option.getOrThrow
131
+ // Admin base mutation lands on the Platform API with dual-auth (system caller).
132
+ expect(platformPlan.sdl)->toContain("Platform_RegisterApiFragment")
133
+ expect(platformPlan.sdl)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"]) @aws_iam`)
134
+ // Platform-target plugin field present on the Platform API.
135
+ expect(platformPlan.sdl)->toContain("Inspector_Ping")
136
+ // Shared traversal type stamped once on the assembled SDL.
137
+ expect(platformPlan.sdl)->toContain(`type CommandAccepted @aws_cognito_user_pools @aws_iam {`)
138
+ // Domain-target field on the Domain API; admin base absent (empty base in split mode).
139
+ expect(domainPlan.sdl)->toContain("Catalog_AddProduct")
140
+ expect(domainPlan.sdl->String.includes("Platform_RegisterApiFragment"))->toBe(false)
141
+ })
142
+
143
+ testSync("unified mode: a single Domain API carries admin base + all frags", () => {
144
+ let plans = AppSync_SdlDecorate.planAwsPushes(
145
+ ~rawAdminBase,
146
+ ~iamFieldNames,
147
+ ~fragments=[platformFrag, domainFrag],
148
+ ~splitApi=false,
149
+ )
150
+ expect(plans->Array.length)->toBe(1)
151
+ let plan = plans->Array.getUnsafe(0)
152
+ expect(plan.api)->toBe("DomainApi")
153
+ expect(plan.sdl)->toContain("Platform_RegisterApiFragment")
154
+ expect(plan.sdl)->toContain("Inspector_Ping")
155
+ expect(plan.sdl)->toContain("Catalog_AddProduct")
156
+ })
157
+ })
@@ -0,0 +1,147 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as AppSync_Adapter$ReventlessAws from "../src/components/Api/AppSync_Adapter.res.mjs";
5
+ import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
6
+ import * as AppSync_SdlDecorate$ReventlessAws from "../src/components/Api/AppSync_SdlDecorate.res.mjs";
7
+
8
+ let sources = [
9
+ {
10
+ field: "onCatalog_AddProduct",
11
+ mutations: ["Catalog_AddProduct"]
12
+ },
13
+ {
14
+ field: "onUIFragmentChange",
15
+ mutations: [
16
+ "Platform_UIFragmentRegistered",
17
+ "Platform_UIFragmentUpdated",
18
+ "Platform_UIFragmentDeregistered"
19
+ ]
20
+ }
21
+ ];
22
+
23
+ let stitchedSdl = `type UIFragmentChangeEvent {
24
+ pluginId: ID!
25
+ }
26
+
27
+ type Query {
28
+ node(id: ID!): Node
29
+ }
30
+
31
+ type Mutation {
32
+ Catalog_AddProduct(input: AddInput): CommandResult
33
+ }
34
+
35
+ type Subscription {
36
+ onCatalog_AddProduct(id: ID): CommandResult
37
+ onUIFragmentChange: UIFragmentChangeEvent
38
+ @aws_auth(cognito_groups: ["Admin"])
39
+ onCatalogEventLog_eventAppended: CatalogEventLogEvent
40
+ }`;
41
+
42
+ globalThis.describe("AppSync_SdlDecorate.injectAwsSubscribe", () => {
43
+ globalThis.test("appends @aws_subscribe on a 1:1 mutation-sourced field (with args)", () => {
44
+ let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
45
+ globalThis.expect(sdl).toContain(`onCatalog_AddProduct(id: ID): CommandResult\n @aws_subscribe(mutations: ["Catalog_AddProduct"])`);
46
+ });
47
+ globalThis.test("appends the many-mutations fan-in on a no-arg field, keeping @aws_auth", () => {
48
+ let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
49
+ globalThis.expect(sdl).toContain(`onUIFragmentChange: UIFragmentChangeEvent\n @aws_subscribe(mutations: ["Platform_UIFragmentRegistered", "Platform_UIFragmentUpdated", "Platform_UIFragmentDeregistered"])`);
50
+ globalThis.expect(sdl).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
51
+ });
52
+ globalThis.test("leaves unmapped fields (Source A) and non-Subscription blocks untouched", () => {
53
+ let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
54
+ globalThis.expect(sdl).toContain(`onCatalogEventLog_eventAppended: CatalogEventLogEvent\n}`);
55
+ globalThis.expect(sdl).toContain(`Catalog_AddProduct(input: AddInput): CommandResult\n}`);
56
+ });
57
+ globalThis.test("no-op on empty sources or an SDL without a Subscription block", () => {
58
+ globalThis.expect(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, [])).toBe(stitchedSdl);
59
+ let noSubs = `type Query {\n node(id: ID!): Node\n}`;
60
+ globalThis.expect(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(noSubs, sources)).toBe(noSubs);
61
+ });
62
+ });
63
+
64
+ globalThis.describe("AppSync_Adapter.stitchWithAwsDirectives", () => {
65
+ globalThis.test("assembles neutral fragments into AWS-dialect SDL", () => {
66
+ let baseFragment = GraphQL_Stitcher$ReventlessCore.encode({
67
+ types: [],
68
+ mutations: [` Platform_PluginStatusChanged(pluginId: ID!, status: PluginStatus!): PluginStatusChangeEvent`],
69
+ queries: [],
70
+ subscriptions: [` onPluginStatusChange: PluginStatusChangeEvent`],
71
+ subscriptionSources: [{
72
+ field: "onPluginStatusChange",
73
+ mutations: ["Platform_PluginStatusChanged"]
74
+ }]
75
+ });
76
+ let pluginFragment = GraphQL_Stitcher$ReventlessCore.encode({
77
+ types: [`type PluginStatusChangeEvent {\n pluginId: ID!\n}`],
78
+ mutations: [` Catalog_AddProduct(input: AddInput): CommandResult`],
79
+ queries: [],
80
+ subscriptions: [` onCatalog_AddProduct(id: ID): CommandResult`],
81
+ subscriptionSources: [{
82
+ field: "onCatalog_AddProduct",
83
+ mutations: ["Catalog_AddProduct"]
84
+ }]
85
+ });
86
+ let sdl = AppSync_Adapter$ReventlessAws.stitchWithAwsDirectives(baseFragment, [pluginFragment]);
87
+ globalThis.expect(sdl).toContain(`@aws_subscribe(mutations: ["Platform_PluginStatusChanged"])`);
88
+ globalThis.expect(sdl).toContain(`@aws_subscribe(mutations: ["Catalog_AddProduct"])`);
89
+ });
90
+ });
91
+
92
+ globalThis.describe("AppSync_SdlDecorate.planAwsPushes", () => {
93
+ let rawAdminBase = GraphQL_Stitcher$ReventlessCore.encode({
94
+ types: [`type CommandAccepted {\n id: ID!\n}`],
95
+ mutations: [` Platform_RegisterApiFragment(input: ApiFragmentInput): CommandResult`],
96
+ queries: [],
97
+ subscriptions: [],
98
+ subscriptionSources: []
99
+ });
100
+ let iamFieldNames = ["Platform_RegisterApiFragment"];
101
+ let mkFrag = (mutation, target) => ({
102
+ encoded: GraphQL_Stitcher$ReventlessCore.encode({
103
+ types: [],
104
+ mutations: [mutation],
105
+ queries: [],
106
+ subscriptions: [],
107
+ subscriptionSources: []
108
+ }).encoded,
109
+ protocol: "graphql",
110
+ target: target
111
+ });
112
+ let platformFrag = mkFrag(` Inspector_Ping: CommandResult`, "Platform");
113
+ let domainFrag = mkFrag(` Catalog_AddProduct(input: AddInput): CommandResult`, "Domain");
114
+ globalThis.test("split mode: Platform API carries admin base (dual-auth) + Platform frags; Domain API excludes the admin base", () => {
115
+ let plans = AppSync_SdlDecorate$ReventlessAws.planAwsPushes(rawAdminBase, iamFieldNames, [
116
+ platformFrag,
117
+ domainFrag
118
+ ], true);
119
+ globalThis.expect(plans.length).toBe(2);
120
+ let platformPlan = Stdlib_Option.getOrThrow(plans.find(p => p.api === "PlatformApi"), undefined);
121
+ let domainPlan = Stdlib_Option.getOrThrow(plans.find(p => p.api === "DomainApi"), undefined);
122
+ globalThis.expect(platformPlan.sdl).toContain("Platform_RegisterApiFragment");
123
+ globalThis.expect(platformPlan.sdl).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"]) @aws_iam`);
124
+ globalThis.expect(platformPlan.sdl).toContain("Inspector_Ping");
125
+ globalThis.expect(platformPlan.sdl).toContain(`type CommandAccepted @aws_cognito_user_pools @aws_iam {`);
126
+ globalThis.expect(domainPlan.sdl).toContain("Catalog_AddProduct");
127
+ globalThis.expect(domainPlan.sdl.includes("Platform_RegisterApiFragment")).toBe(false);
128
+ });
129
+ globalThis.test("unified mode: a single Domain API carries admin base + all frags", () => {
130
+ let plans = AppSync_SdlDecorate$ReventlessAws.planAwsPushes(rawAdminBase, iamFieldNames, [
131
+ platformFrag,
132
+ domainFrag
133
+ ], false);
134
+ globalThis.expect(plans.length).toBe(1);
135
+ let plan = plans[0];
136
+ globalThis.expect(plan.api).toBe("DomainApi");
137
+ globalThis.expect(plan.sdl).toContain("Platform_RegisterApiFragment");
138
+ globalThis.expect(plan.sdl).toContain("Inspector_Ping");
139
+ globalThis.expect(plan.sdl).toContain("Catalog_AddProduct");
140
+ });
141
+ });
142
+
143
+ export {
144
+ sources,
145
+ stitchedSdl,
146
+ }
147
+ /* Not a pure module */