@reventlessdev/reventless-core 3.0.0-alpha.243 → 3.0.0-alpha.244

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/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
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.244 (2026-08-20)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **api:** name a union after itself, not after the keyword ([58ab956](https://github.com/ReventlessDev/reventless-core/commit/58ab956b8fa310c60f118595768e40e750d61784))
11
+
12
+
6
13
  # 3.0.0-alpha.243 (2026-08-20)
7
14
 
8
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.243",
3
+ "version": "3.0.0-alpha.244",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -35,9 +35,9 @@
35
35
  "@reventlessdev/rescript-node": "2.0.0-alpha.8",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
37
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.8",
38
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
38
39
  "@reventlessdev/reventless-infra": "3.0.0-alpha.148",
39
40
  "@reventlessdev/reventless-spec": "3.0.0-alpha.120",
40
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
41
41
  "@reventlessdev/reventless-interop": "3.0.0-alpha.32"
42
42
  },
43
43
  "devDependencies": {
@@ -122,17 +122,19 @@ let collectSubscriptionSources = (
122
122
  // Extract the first identifier from a SDL field/type string.
123
123
  // e.g. " fieldName(args): Type" → "fieldName"
124
124
  // "type TypeName {" → "TypeName"
125
+ // Every SDL keyword that introduces a named definition. `union` was missing while
126
+ // `CommandResult` was the only union in the schema: a keyword left in place makes
127
+ // every declaration of that kind name itself after the keyword, so the second one
128
+ // deduped against the first and vanished — AppSync then rejected the schema for a
129
+ // type that was never emitted.
130
+ let definitionKeywords = ["type ", "enum ", "input ", "union ", "interface ", "scalar "]
131
+
125
132
  let extractLeadingName = (str: string): string => {
126
133
  let trimmed = str->String.trim
127
- // Remove "type ", "enum ", or "input " prefix if present
128
- let afterType = if trimmed->String.startsWith("type ") {
129
- trimmed->String.slice(~start=5, ~end=trimmed->String.length)->String.trim
130
- } else if trimmed->String.startsWith("enum ") {
131
- trimmed->String.slice(~start=5, ~end=trimmed->String.length)->String.trim
132
- } else if trimmed->String.startsWith("input ") {
133
- trimmed->String.slice(~start=6, ~end=trimmed->String.length)->String.trim
134
- } else {
135
- trimmed
134
+ let afterType = switch definitionKeywords->Array.find(kw => trimmed->String.startsWith(kw)) {
135
+ | Some(kw) =>
136
+ trimmed->String.slice(~start=kw->String.length, ~end=trimmed->String.length)->String.trim
137
+ | None => trimmed
136
138
  }
137
139
  // Split on "(" first to remove arg list, then on " ", "{", and ":" for the name.
138
140
  // The ":" split is load-bearing for ARG-LESS fields: `Foo: [Bar!]!` has no "(" to
@@ -115,11 +115,19 @@ function collectSubscriptionSources(baseFragment, pluginFragments) {
115
115
  });
116
116
  }
117
117
 
118
+ let definitionKeywords = [
119
+ "type ",
120
+ "enum ",
121
+ "input ",
122
+ "union ",
123
+ "interface ",
124
+ "scalar "
125
+ ];
126
+
118
127
  function extractLeadingName(str) {
119
128
  let trimmed = str.trim();
120
- let afterType = trimmed.startsWith("type ") || trimmed.startsWith("enum ") ? trimmed.slice(5, trimmed.length).trim() : (
121
- trimmed.startsWith("input ") ? trimmed.slice(6, trimmed.length).trim() : trimmed
122
- );
129
+ let kw = definitionKeywords.find(kw => trimmed.startsWith(kw));
130
+ let afterType = kw !== undefined ? trimmed.slice(kw.length, trimmed.length).trim() : trimmed;
123
131
  return Stdlib_Option.getOr(Stdlib_Option.getOr(Stdlib_Option.getOr(Stdlib_Option.getOr(afterType.split("(")[0], "").trim().split(" ")[0], "").trim().split("{")[0], "").trim().split(":")[0], "").trim();
124
132
  }
125
133
 
@@ -231,6 +239,7 @@ export {
231
239
  encode,
232
240
  decode,
233
241
  collectSubscriptionSources,
242
+ definitionKeywords,
234
243
  extractLeadingName,
235
244
  relayBaseTypes,
236
245
  relayBaseQueries,
@@ -64,6 +64,51 @@ describe("GraphQL_Stitcher.subscriptionSources", () => {
64
64
  })
65
65
  })
66
66
 
67
+ // Two unions in one schema is what caught this: every definition keyword the name
68
+ // extractor does not strip makes each declaration of that kind name itself after
69
+ // the keyword, so the second dedupes against the first and is dropped. With
70
+ // `CommandResult` the only union, nothing showed it. The local path does not use
71
+ // this dedup, so a golden carrying both unions still passed while AppSync rejected
72
+ // the schema for a type that was never emitted.
73
+ describe("GraphQL_Stitcher.extractLeadingName", () => {
74
+ testSync("names a union after the union, not after the keyword", () => {
75
+ expect(extractLeadingName("union Geolocation = GeolocationLocated | GeolocationPending"))->toBe(
76
+ "Geolocation",
77
+ )
78
+ expect(extractLeadingName("union CommandResult = CommandAccepted | CommandRejected"))->toBe(
79
+ "CommandResult",
80
+ )
81
+ })
82
+
83
+ testSync("names an interface and a scalar after themselves", () => {
84
+ expect(extractLeadingName("interface Node { id: ID! }"))->toBe("Node")
85
+ expect(extractLeadingName("scalar AWSDateTime"))->toBe("AWSDateTime")
86
+ })
87
+
88
+ // The cases that already worked, kept so a rewrite of the keyword list has to
89
+ // keep them working.
90
+ testSync("still names types, enums and inputs", () => {
91
+ expect(extractLeadingName("type Ordering_Customer implements Node { id: ID! }"))->toBe(
92
+ "Ordering_Customer",
93
+ )
94
+ expect(extractLeadingName("enum SortOrder { ASC DESC }"))->toBe("SortOrder")
95
+ expect(extractLeadingName("input Ordering_CustomerFilter { id: ID }"))->toBe(
96
+ "Ordering_CustomerFilter",
97
+ )
98
+ })
99
+
100
+ // Fields go through the same extractor, and an arg-less one has no "(" to cleave
101
+ // the type off.
102
+ testSync("still names fields, with and without args", () => {
103
+ expect(extractLeadingName(" Catalog_AddProduct(input: AddInput): CommandResult"))->toBe(
104
+ "Catalog_AddProduct",
105
+ )
106
+ expect(extractLeadingName(" Platform_ApiFragments: [Fragment!]!"))->toBe(
107
+ "Platform_ApiFragments",
108
+ )
109
+ })
110
+ })
111
+
67
112
  describe("GraphQL_Stitcher.stitchStandalone", () => {
68
113
  let pluginFragment = encode({
69
114
  types: [
@@ -77,6 +77,26 @@ globalThis.describe("GraphQL_Stitcher.subscriptionSources", () => {
77
77
  });
78
78
  });
79
79
 
80
+ globalThis.describe("GraphQL_Stitcher.extractLeadingName", () => {
81
+ globalThis.test("names a union after the union, not after the keyword", () => {
82
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.extractLeadingName("union Geolocation = GeolocationLocated | GeolocationPending")).toBe("Geolocation");
83
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.extractLeadingName("union CommandResult = CommandAccepted | CommandRejected")).toBe("CommandResult");
84
+ });
85
+ globalThis.test("names an interface and a scalar after themselves", () => {
86
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.extractLeadingName("interface Node { id: ID! }")).toBe("Node");
87
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.extractLeadingName("scalar AWSDateTime")).toBe("AWSDateTime");
88
+ });
89
+ globalThis.test("still names types, enums and inputs", () => {
90
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.extractLeadingName("type Ordering_Customer implements Node { id: ID! }")).toBe("Ordering_Customer");
91
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.extractLeadingName("enum SortOrder { ASC DESC }")).toBe("SortOrder");
92
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.extractLeadingName("input Ordering_CustomerFilter { id: ID }")).toBe("Ordering_CustomerFilter");
93
+ });
94
+ globalThis.test("still names fields, with and without args", () => {
95
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.extractLeadingName(" Catalog_AddProduct(input: AddInput): CommandResult")).toBe("Catalog_AddProduct");
96
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.extractLeadingName(" Platform_ApiFragments: [Fragment!]!")).toBe("Platform_ApiFragments");
97
+ });
98
+ });
99
+
80
100
  globalThis.describe("GraphQL_Stitcher.stitchStandalone", () => {
81
101
  let pluginFragment = GraphQL_Stitcher$ReventlessCore.encode({
82
102
  types: [