@reventlessdev/reventless-core 3.0.0-alpha.166 → 3.0.0-alpha.168
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 +20 -0
- package/package.json +3 -3
- package/src/Projection.res +11 -6
- package/src/Projection.res.mjs +27 -25
- package/src/admin/AdminApi.res +9 -62
- package/src/admin/AdminApi.res.mjs +6 -28
- package/src/components/Api/GraphQL_Stitcher.res +4 -112
- package/src/components/Api/GraphQL_Stitcher.res.mjs +0 -90
- package/src/components/Dcb/Dcb_Builder.res +38 -21
- package/src/components/Dcb/Dcb_Builder.res.mjs +13 -4
- package/src/components/ReadModel/ReadModel_Callback.res +1 -1
- package/src/components/ReadModel/ReadModel_Callback.res.mjs +1 -1
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +1 -1
- package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +1 -1
- package/src/components/StateViewSlice/StateViewSlice_Callback.res +1 -1
- package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +1 -1
- package/src/plugin/component/Plugin_Builder.res +5 -7
- package/src/plugin/component/Plugin_Builder.res.mjs +3 -6
- package/src/util/EffectLogger.res +5 -3
- package/src/util/EffectLogger.res.mjs +3 -10
- package/src/util/Logger.res +41 -11
- package/src/util/Logger.res.mjs +36 -10
- package/tests/api/GraphQL_StitcherTest.res +0 -198
- package/tests/api/GraphQL_StitcherTest.res.mjs +0 -168
- package/src/admin/ApiFragmentRegistry/ApiFragmentRegistryBehavior.res +0 -108
- package/src/admin/ApiFragmentRegistry/ApiFragmentRegistryBehavior.res.mjs +0 -190
- package/src/admin/ApiFragmentRegistry/ApiFragmentRegistrySpec.res +0 -71
- package/src/admin/ApiFragmentRegistry/ApiFragmentRegistrySpec.res.mjs +0 -92
- package/src/admin/ApiFragmentRegistry/ApiFragmentsProjection.res +0 -63
- package/src/admin/ApiFragmentRegistry/ApiFragmentsProjection.res.mjs +0 -108
- package/src/admin/ApiFragmentRegistry/ApiFragmentsReadModelSpec.res +0 -23
- package/src/admin/ApiFragmentRegistry/ApiFragmentsReadModelSpec.res.mjs +0 -43
- package/src/admin/Platform_ApiFragmentsApi.res +0 -32
- package/src/admin/Platform_ApiFragmentsApi.res.mjs +0 -52
- package/src/components/Api/Api_Builder.res +0 -40
- package/src/components/Api/Api_Builder.res.mjs +0 -33
- package/src/components/Api/Api_Operations.res +0 -13
- package/src/components/Api/Api_Operations.res.mjs +0 -12
- package/src/components/Api/GraphQL_PushPlanner.res +0 -65
- package/src/components/Api/GraphQL_PushPlanner.res.mjs +0 -46
- package/tests/admin/Platform_ApiFragmentsApiTest.res +0 -66
- package/tests/admin/Platform_ApiFragmentsApiTest.res.mjs +0 -68
- package/tests/api/GraphQL_PushPlannerTest.res +0 -62
- package/tests/api/GraphQL_PushPlannerTest.res.mjs +0 -80
|
@@ -1,204 +1,6 @@
|
|
|
1
1
|
open JestGlobals
|
|
2
2
|
open GraphQL_Stitcher
|
|
3
3
|
|
|
4
|
-
// A representative stitched schema: admin base (Platform_*) plus two domain
|
|
5
|
-
// plugins (Catalog_*, Ordering_*). 4 Mutation + 4 Query root fields.
|
|
6
|
-
let fullSchemaSdl = `type Node {
|
|
7
|
-
id: ID!
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type Query {
|
|
11
|
-
node(id: ID!): Node
|
|
12
|
-
Platform_plugins: [Plugin]
|
|
13
|
-
Catalog_products: [Product]
|
|
14
|
-
Ordering_orders: [Order]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type Mutation {
|
|
18
|
-
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
19
|
-
Catalog_addProduct(input: AddInput): Product
|
|
20
|
-
Catalog_updateProduct(input: UpdateInput): Product
|
|
21
|
-
Ordering_placeOrder(input: OrderInput): Order
|
|
22
|
-
}`
|
|
23
|
-
|
|
24
|
-
// The exact failure observed in alpha: a lifecycle window collapses the live
|
|
25
|
-
// schema back to admin-base only — every Catalog_*/Ordering_* field gone.
|
|
26
|
-
// 1 Mutation + 2 Query root fields.
|
|
27
|
-
let collapsedSchemaSdl = `type Node {
|
|
28
|
-
id: ID!
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type Query {
|
|
32
|
-
node(id: ID!): Node
|
|
33
|
-
Platform_plugins: [Plugin]
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type Mutation {
|
|
37
|
-
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
38
|
-
}`
|
|
39
|
-
|
|
40
|
-
// Real stitched shape: injectAwsAuthAll appends @aws_auth on its own indented
|
|
41
|
-
// line after each Mutation field, so a field spans two physical lines.
|
|
42
|
-
let authAnnotatedSdl = `type Mutation {
|
|
43
|
-
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
44
|
-
@aws_auth(cognito_groups: ["Admin"])
|
|
45
|
-
Catalog_addProduct(input: AddInput): Product
|
|
46
|
-
@aws_auth(cognito_groups: ["User"])
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
type Query {
|
|
50
|
-
node(id: ID!): Node @aws_auth(cognito_groups: ["Admin"])
|
|
51
|
-
}`
|
|
52
|
-
|
|
53
|
-
describe("GraphQL_Stitcher.countRootTypeFields", () => {
|
|
54
|
-
testSync("counts Mutation fields", () => {
|
|
55
|
-
expect(countRootTypeFields(~sdl=fullSchemaSdl, ~typeName="Mutation"))->toBe(4)
|
|
56
|
-
})
|
|
57
|
-
testSync("counts Query fields", () => {
|
|
58
|
-
expect(countRootTypeFields(~sdl=fullSchemaSdl, ~typeName="Query"))->toBe(4)
|
|
59
|
-
})
|
|
60
|
-
testSync("ignores @aws_auth directive lines, counts each Mutation field once", () => {
|
|
61
|
-
expect(countRootTypeFields(~sdl=authAnnotatedSdl, ~typeName="Mutation"))->toBe(2)
|
|
62
|
-
})
|
|
63
|
-
testSync("ignores @aws_auth directive lines, counts each Query field once", () => {
|
|
64
|
-
expect(countRootTypeFields(~sdl=authAnnotatedSdl, ~typeName="Query"))->toBe(1)
|
|
65
|
-
})
|
|
66
|
-
testSync("missing type returns 0", () => {
|
|
67
|
-
expect(countRootTypeFields(~sdl=fullSchemaSdl, ~typeName="Subscription"))->toBe(0)
|
|
68
|
-
})
|
|
69
|
-
testSync("empty SDL returns 0", () => {
|
|
70
|
-
expect(countRootTypeFields(~sdl="", ~typeName="Mutation"))->toBe(0)
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
describe("GraphQL_Stitcher.isCatastrophicSchemaShrink", () => {
|
|
75
|
-
testSync("rejects the all-disconnected collapse (8 → 3 root fields, < 50%)", () => {
|
|
76
|
-
expect(
|
|
77
|
-
isCatastrophicSchemaShrink(~currentSdl=fullSchemaSdl, ~newSdl=collapsedSchemaSdl, ~threshold=0.5),
|
|
78
|
-
)->toBe(true)
|
|
79
|
-
})
|
|
80
|
-
testSync("allows an intentional small removal above threshold (8 → 7)", () => {
|
|
81
|
-
let minusOneSdl = `type Query {
|
|
82
|
-
node(id: ID!): Node
|
|
83
|
-
Platform_plugins: [Plugin]
|
|
84
|
-
Catalog_products: [Product]
|
|
85
|
-
Ordering_orders: [Order]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
type Mutation {
|
|
89
|
-
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
90
|
-
Catalog_addProduct(input: AddInput): Product
|
|
91
|
-
Ordering_placeOrder(input: OrderInput): Order
|
|
92
|
-
}`
|
|
93
|
-
expect(
|
|
94
|
-
isCatastrophicSchemaShrink(~currentSdl=fullSchemaSdl, ~newSdl=minusOneSdl, ~threshold=0.5),
|
|
95
|
-
)->toBe(false)
|
|
96
|
-
})
|
|
97
|
-
testSync("allows the first push when there is no live schema to protect", () => {
|
|
98
|
-
expect(
|
|
99
|
-
isCatastrophicSchemaShrink(~currentSdl="", ~newSdl=fullSchemaSdl, ~threshold=0.5),
|
|
100
|
-
)->toBe(false)
|
|
101
|
-
})
|
|
102
|
-
testSync("allows an unchanged re-push (same SDL)", () => {
|
|
103
|
-
expect(
|
|
104
|
-
isCatastrophicSchemaShrink(~currentSdl=fullSchemaSdl, ~newSdl=fullSchemaSdl, ~threshold=0.5),
|
|
105
|
-
)->toBe(false)
|
|
106
|
-
})
|
|
107
|
-
testSync("allows a purely additive push even below threshold cardinality", () => {
|
|
108
|
-
// A tiny live schema growing into a big one drops nothing → never a clobber,
|
|
109
|
-
// regardless of the ratio.
|
|
110
|
-
expect(
|
|
111
|
-
isCatastrophicSchemaShrink(~currentSdl=collapsedSchemaSdl, ~newSdl=fullSchemaSdl, ~threshold=0.5),
|
|
112
|
-
)->toBe(false)
|
|
113
|
-
})
|
|
114
|
-
testSync("includes Subscription fields when judging a shrink", () => {
|
|
115
|
-
let withSubs = `type Query {
|
|
116
|
-
node(id: ID!): Node
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
type Mutation {
|
|
120
|
-
Ordering_placeOrder(input: OrderInput): Order
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
type Subscription {
|
|
124
|
-
onOrdering_Customer_Register: Customer
|
|
125
|
-
onOrdering_Customer_UpdateAddress: Customer
|
|
126
|
-
onCatalog_Product_Added: Product
|
|
127
|
-
}`
|
|
128
|
-
// Dropping every Subscription field (and keeping Mutation/Query) is a shrink
|
|
129
|
-
// the Mutation+Query-only guard would have missed.
|
|
130
|
-
let subsGone = `type Query {
|
|
131
|
-
node(id: ID!): Node
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
type Mutation {
|
|
135
|
-
Ordering_placeOrder(input: OrderInput): Order
|
|
136
|
-
}`
|
|
137
|
-
expect(
|
|
138
|
-
isCatastrophicSchemaShrink(~currentSdl=withSubs, ~newSdl=subsGone, ~threshold=0.5),
|
|
139
|
-
)->toBe(true)
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
describe("GraphQL_Stitcher.rootTypeFieldNames", () => {
|
|
144
|
-
testSync("lists Mutation field names", () => {
|
|
145
|
-
expect(rootTypeFieldNames(~sdl=fullSchemaSdl, ~typeName="Mutation"))->toEqual([
|
|
146
|
-
"Platform_connectPlugin",
|
|
147
|
-
"Catalog_addProduct",
|
|
148
|
-
"Catalog_updateProduct",
|
|
149
|
-
"Ordering_placeOrder",
|
|
150
|
-
])
|
|
151
|
-
})
|
|
152
|
-
testSync("strips args and @aws_auth directive lines", () => {
|
|
153
|
-
expect(rootTypeFieldNames(~sdl=authAnnotatedSdl, ~typeName="Mutation"))->toEqual([
|
|
154
|
-
"Platform_connectPlugin",
|
|
155
|
-
"Catalog_addProduct",
|
|
156
|
-
])
|
|
157
|
-
})
|
|
158
|
-
testSync("missing type returns []", () => {
|
|
159
|
-
expect(rootTypeFieldNames(~sdl=fullSchemaSdl, ~typeName="Subscription"))->toEqual([])
|
|
160
|
-
})
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
describe("GraphQL_Stitcher.missingRootFields", () => {
|
|
164
|
-
testSync("reports plugin fields absent from an admin-base clobber", () => {
|
|
165
|
-
// The alpha 2026-07-08 clobber: live schema is admin-base only. Every
|
|
166
|
-
// Catalog_*/Ordering_* field the deploy expects is missing → repair fires.
|
|
167
|
-
let missing = missingRootFields(~expectedSdl=fullSchemaSdl, ~liveSdl=collapsedSchemaSdl)
|
|
168
|
-
expect(missing->Array.includes("Catalog_addProduct"))->toBe(true)
|
|
169
|
-
expect(missing->Array.includes("Ordering_placeOrder"))->toBe(true)
|
|
170
|
-
expect(missing->Array.includes("Ordering_orders"))->toBe(true)
|
|
171
|
-
})
|
|
172
|
-
testSync("empty when live schema is a superset (intact)", () => {
|
|
173
|
-
expect(missingRootFields(~expectedSdl=fullSchemaSdl, ~liveSdl=fullSchemaSdl))->toEqual([])
|
|
174
|
-
})
|
|
175
|
-
testSync("detects an equal-cardinality field swap the count test misses", () => {
|
|
176
|
-
// Same number of root fields, but one Mutation field replaced by a foreign
|
|
177
|
-
// one — a swap. countRootTypeFields would report no drift; the name-set diff
|
|
178
|
-
// catches it.
|
|
179
|
-
let swapped = `type Query {
|
|
180
|
-
node(id: ID!): Node
|
|
181
|
-
Platform_plugins: [Plugin]
|
|
182
|
-
Catalog_products: [Product]
|
|
183
|
-
Ordering_orders: [Order]
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
type Mutation {
|
|
187
|
-
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
188
|
-
Catalog_addProduct(input: AddInput): Product
|
|
189
|
-
Catalog_updateProduct(input: UpdateInput): Product
|
|
190
|
-
Foreign_strayField(input: OrderInput): Order
|
|
191
|
-
}`
|
|
192
|
-
// Same count on both sides.
|
|
193
|
-
expect(countRootTypeFields(~sdl=swapped, ~typeName="Mutation"))->toBe(
|
|
194
|
-
countRootTypeFields(~sdl=fullSchemaSdl, ~typeName="Mutation"),
|
|
195
|
-
)
|
|
196
|
-
// But the expected Ordering_placeOrder is missing from the live (swapped) schema.
|
|
197
|
-
let missing = missingRootFields(~expectedSdl=fullSchemaSdl, ~liveSdl=swapped)
|
|
198
|
-
expect(missing->Array.includes("Ordering_placeOrder"))->toBe(true)
|
|
199
|
-
})
|
|
200
|
-
})
|
|
201
|
-
|
|
202
4
|
describe("GraphQL_Stitcher.subscriptionSources", () => {
|
|
203
5
|
testSync("encode/decode round-trips the subscription→mutation source mapping", () => {
|
|
204
6
|
let fragment = encode({
|
|
@@ -3,169 +3,6 @@
|
|
|
3
3
|
import * as AdminApi$ReventlessCore from "../../src/admin/AdminApi.res.mjs";
|
|
4
4
|
import * as GraphQL_Stitcher$ReventlessCore from "../../src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
5
5
|
|
|
6
|
-
let fullSchemaSdl = `type Node {
|
|
7
|
-
id: ID!
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type Query {
|
|
11
|
-
node(id: ID!): Node
|
|
12
|
-
Platform_plugins: [Plugin]
|
|
13
|
-
Catalog_products: [Product]
|
|
14
|
-
Ordering_orders: [Order]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type Mutation {
|
|
18
|
-
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
19
|
-
Catalog_addProduct(input: AddInput): Product
|
|
20
|
-
Catalog_updateProduct(input: UpdateInput): Product
|
|
21
|
-
Ordering_placeOrder(input: OrderInput): Order
|
|
22
|
-
}`;
|
|
23
|
-
|
|
24
|
-
let collapsedSchemaSdl = `type Node {
|
|
25
|
-
id: ID!
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type Query {
|
|
29
|
-
node(id: ID!): Node
|
|
30
|
-
Platform_plugins: [Plugin]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
type Mutation {
|
|
34
|
-
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
35
|
-
}`;
|
|
36
|
-
|
|
37
|
-
let authAnnotatedSdl = `type Mutation {
|
|
38
|
-
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
39
|
-
@aws_auth(cognito_groups: ["Admin"])
|
|
40
|
-
Catalog_addProduct(input: AddInput): Product
|
|
41
|
-
@aws_auth(cognito_groups: ["User"])
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type Query {
|
|
45
|
-
node(id: ID!): Node @aws_auth(cognito_groups: ["Admin"])
|
|
46
|
-
}`;
|
|
47
|
-
|
|
48
|
-
globalThis.describe("GraphQL_Stitcher.countRootTypeFields", () => {
|
|
49
|
-
globalThis.test("counts Mutation fields", () => {
|
|
50
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.countRootTypeFields(fullSchemaSdl, "Mutation")).toBe(4);
|
|
51
|
-
});
|
|
52
|
-
globalThis.test("counts Query fields", () => {
|
|
53
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.countRootTypeFields(fullSchemaSdl, "Query")).toBe(4);
|
|
54
|
-
});
|
|
55
|
-
globalThis.test("ignores @aws_auth directive lines, counts each Mutation field once", () => {
|
|
56
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.countRootTypeFields(authAnnotatedSdl, "Mutation")).toBe(2);
|
|
57
|
-
});
|
|
58
|
-
globalThis.test("ignores @aws_auth directive lines, counts each Query field once", () => {
|
|
59
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.countRootTypeFields(authAnnotatedSdl, "Query")).toBe(1);
|
|
60
|
-
});
|
|
61
|
-
globalThis.test("missing type returns 0", () => {
|
|
62
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.countRootTypeFields(fullSchemaSdl, "Subscription")).toBe(0);
|
|
63
|
-
});
|
|
64
|
-
globalThis.test("empty SDL returns 0", () => {
|
|
65
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.countRootTypeFields("", "Mutation")).toBe(0);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
globalThis.describe("GraphQL_Stitcher.isCatastrophicSchemaShrink", () => {
|
|
70
|
-
globalThis.test("rejects the all-disconnected collapse (8 → 3 root fields, < 50%)", () => {
|
|
71
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.isCatastrophicSchemaShrink(fullSchemaSdl, collapsedSchemaSdl, 0.5)).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
globalThis.test("allows an intentional small removal above threshold (8 → 7)", () => {
|
|
74
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.isCatastrophicSchemaShrink(fullSchemaSdl, `type Query {
|
|
75
|
-
node(id: ID!): Node
|
|
76
|
-
Platform_plugins: [Plugin]
|
|
77
|
-
Catalog_products: [Product]
|
|
78
|
-
Ordering_orders: [Order]
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
type Mutation {
|
|
82
|
-
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
83
|
-
Catalog_addProduct(input: AddInput): Product
|
|
84
|
-
Ordering_placeOrder(input: OrderInput): Order
|
|
85
|
-
}`, 0.5)).toBe(false);
|
|
86
|
-
});
|
|
87
|
-
globalThis.test("allows the first push when there is no live schema to protect", () => {
|
|
88
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.isCatastrophicSchemaShrink("", fullSchemaSdl, 0.5)).toBe(false);
|
|
89
|
-
});
|
|
90
|
-
globalThis.test("allows an unchanged re-push (same SDL)", () => {
|
|
91
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.isCatastrophicSchemaShrink(fullSchemaSdl, fullSchemaSdl, 0.5)).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
globalThis.test("allows a purely additive push even below threshold cardinality", () => {
|
|
94
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.isCatastrophicSchemaShrink(collapsedSchemaSdl, fullSchemaSdl, 0.5)).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
globalThis.test("includes Subscription fields when judging a shrink", () => {
|
|
97
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.isCatastrophicSchemaShrink(`type Query {
|
|
98
|
-
node(id: ID!): Node
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
type Mutation {
|
|
102
|
-
Ordering_placeOrder(input: OrderInput): Order
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
type Subscription {
|
|
106
|
-
onOrdering_Customer_Register: Customer
|
|
107
|
-
onOrdering_Customer_UpdateAddress: Customer
|
|
108
|
-
onCatalog_Product_Added: Product
|
|
109
|
-
}`, `type Query {
|
|
110
|
-
node(id: ID!): Node
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
type Mutation {
|
|
114
|
-
Ordering_placeOrder(input: OrderInput): Order
|
|
115
|
-
}`, 0.5)).toBe(true);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
globalThis.describe("GraphQL_Stitcher.rootTypeFieldNames", () => {
|
|
120
|
-
globalThis.test("lists Mutation field names", () => {
|
|
121
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.rootTypeFieldNames(fullSchemaSdl, "Mutation")).toEqual([
|
|
122
|
-
"Platform_connectPlugin",
|
|
123
|
-
"Catalog_addProduct",
|
|
124
|
-
"Catalog_updateProduct",
|
|
125
|
-
"Ordering_placeOrder"
|
|
126
|
-
]);
|
|
127
|
-
});
|
|
128
|
-
globalThis.test("strips args and @aws_auth directive lines", () => {
|
|
129
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.rootTypeFieldNames(authAnnotatedSdl, "Mutation")).toEqual([
|
|
130
|
-
"Platform_connectPlugin",
|
|
131
|
-
"Catalog_addProduct"
|
|
132
|
-
]);
|
|
133
|
-
});
|
|
134
|
-
globalThis.test("missing type returns []", () => {
|
|
135
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.rootTypeFieldNames(fullSchemaSdl, "Subscription")).toEqual([]);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
globalThis.describe("GraphQL_Stitcher.missingRootFields", () => {
|
|
140
|
-
globalThis.test("reports plugin fields absent from an admin-base clobber", () => {
|
|
141
|
-
let missing = GraphQL_Stitcher$ReventlessCore.missingRootFields(fullSchemaSdl, collapsedSchemaSdl);
|
|
142
|
-
globalThis.expect(missing.includes("Catalog_addProduct")).toBe(true);
|
|
143
|
-
globalThis.expect(missing.includes("Ordering_placeOrder")).toBe(true);
|
|
144
|
-
globalThis.expect(missing.includes("Ordering_orders")).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
globalThis.test("empty when live schema is a superset (intact)", () => {
|
|
147
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.missingRootFields(fullSchemaSdl, fullSchemaSdl)).toEqual([]);
|
|
148
|
-
});
|
|
149
|
-
globalThis.test("detects an equal-cardinality field swap the count test misses", () => {
|
|
150
|
-
let swapped = `type Query {
|
|
151
|
-
node(id: ID!): Node
|
|
152
|
-
Platform_plugins: [Plugin]
|
|
153
|
-
Catalog_products: [Product]
|
|
154
|
-
Ordering_orders: [Order]
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
type Mutation {
|
|
158
|
-
Platform_connectPlugin(input: ConnectInput): ConnectResult
|
|
159
|
-
Catalog_addProduct(input: AddInput): Product
|
|
160
|
-
Catalog_updateProduct(input: UpdateInput): Product
|
|
161
|
-
Foreign_strayField(input: OrderInput): Order
|
|
162
|
-
}`;
|
|
163
|
-
globalThis.expect(GraphQL_Stitcher$ReventlessCore.countRootTypeFields(swapped, "Mutation")).toBe(GraphQL_Stitcher$ReventlessCore.countRootTypeFields(fullSchemaSdl, "Mutation"));
|
|
164
|
-
let missing = GraphQL_Stitcher$ReventlessCore.missingRootFields(fullSchemaSdl, swapped);
|
|
165
|
-
globalThis.expect(missing.includes("Ordering_placeOrder")).toBe(true);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
6
|
globalThis.describe("GraphQL_Stitcher.subscriptionSources", () => {
|
|
170
7
|
globalThis.test("encode/decode round-trips the subscription→mutation source mapping", () => {
|
|
171
8
|
let fragment = GraphQL_Stitcher$ReventlessCore.encode({
|
|
@@ -272,9 +109,4 @@ globalThis.describe("GraphQL_Stitcher.stitchStandalone", () => {
|
|
|
272
109
|
});
|
|
273
110
|
});
|
|
274
111
|
|
|
275
|
-
export {
|
|
276
|
-
fullSchemaSdl,
|
|
277
|
-
collapsedSchemaSdl,
|
|
278
|
-
authAnnotatedSdl,
|
|
279
|
-
}
|
|
280
112
|
/* Not a pure module */
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
@@reventless.behavior(ApiFragmentRegistrySpec)
|
|
2
|
-
|
|
3
|
-
// NOTE: no `open Reventless.Plugin` — it would shadow the `encoded`/`protocol` labels reused
|
|
4
|
-
// below when building the fragment snapshot (warning 45). The two Plugin types are qualified.
|
|
5
|
-
|
|
6
|
-
// The current fragment for a plugin name plus its target API and the last recorded push
|
|
7
|
-
// outcome (kept only to dedupe at-least-once redelivery of RecordApiFragmentPush).
|
|
8
|
-
type registryEntry = {
|
|
9
|
-
fragment: Reventless.Plugin.apiSchemaFragment,
|
|
10
|
-
apiTarget: Reventless.Plugin.apiTarget,
|
|
11
|
-
lastPush: option<(bool, string, string)>, // (ok, message, at)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Singleton aggregate: the WHOLE registry, keyed by pluginId, in one consistency boundary.
|
|
15
|
-
type state = {entries: dict<registryEntry>}
|
|
16
|
-
|
|
17
|
-
let initialState = {entries: Dict.make()}
|
|
18
|
-
|
|
19
|
-
// Framework-internal aggregate outside an Aggregate/ folder — the Behavior.T snapshot/counter
|
|
20
|
-
// fields are satisfied manually (mirrors PluginBehavior). The registry's history is short
|
|
21
|
-
// (deploy-frequency writes), so persisted snapshots would buy nothing.
|
|
22
|
-
let snapshot = None
|
|
23
|
-
let atomicCounter = None
|
|
24
|
-
|
|
25
|
-
// Immutable dict updates (no reliance on Dict.copy/Dict.delete): rebuild via toArray/fromArray
|
|
26
|
-
// so evolve never mutates the replayed state.
|
|
27
|
-
let entriesWith = (entries: dict<registryEntry>, pluginId, entry) =>
|
|
28
|
-
entries
|
|
29
|
-
->Dict.toArray
|
|
30
|
-
->Array.filter(((k, _)) => k != pluginId)
|
|
31
|
-
->Array.concat([(pluginId, entry)])
|
|
32
|
-
->Dict.fromArray
|
|
33
|
-
|
|
34
|
-
let entriesWithout = (entries: dict<registryEntry>, pluginId) =>
|
|
35
|
-
entries->Dict.toArray->Array.filter(((k, _)) => k != pluginId)->Dict.fromArray
|
|
36
|
-
|
|
37
|
-
let evolve = (state, event) =>
|
|
38
|
-
switch event {
|
|
39
|
-
| ApiFragmentRegistered({pluginId, fragment, apiTarget}) => {
|
|
40
|
-
entries: state.entries->entriesWith(pluginId, {fragment, apiTarget, lastPush: None}),
|
|
41
|
-
}
|
|
42
|
-
| ApiFragmentUpdated({pluginId, newFragment, apiTarget}) => {
|
|
43
|
-
entries: state.entries->entriesWith(pluginId, {fragment: newFragment, apiTarget, lastPush: None}),
|
|
44
|
-
}
|
|
45
|
-
| ApiFragmentDeregistered({pluginId}) => {entries: state.entries->entriesWithout(pluginId)}
|
|
46
|
-
| ApiFragmentPushRecorded({pluginId, ok, message, at}) =>
|
|
47
|
-
switch state.entries->Dict.get(pluginId) {
|
|
48
|
-
| Some(entry) => {
|
|
49
|
-
entries: state.entries->entriesWith(pluginId, {...entry, lastPush: Some((ok, message, at))}),
|
|
50
|
-
}
|
|
51
|
-
| None => state
|
|
52
|
-
}
|
|
53
|
-
// Derived echo — no fold change.
|
|
54
|
-
| ApiSchemaComputed(_) => state
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// The consistent registry snapshot AFTER the triggering change — one entry per registered plugin.
|
|
58
|
-
let snapshotOf = (entries: dict<registryEntry>): array<ApiFragmentRegistrySpec.fragmentSnapshotEntry> =>
|
|
59
|
-
entries
|
|
60
|
-
->Dict.toArray
|
|
61
|
-
->Array.map(((pluginId, e)) => {
|
|
62
|
-
ApiFragmentRegistrySpec.pluginId,
|
|
63
|
-
encoded: e.fragment.encoded,
|
|
64
|
-
protocol: e.fragment.protocol,
|
|
65
|
-
apiTarget: e.apiTarget,
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// Emit the fact event plus ApiSchemaComputed{snapshot} folded from the POST-change state, so the
|
|
69
|
-
// reactive push SideEffect stitches from consistent state. Idempotent no-ops emit nothing (and
|
|
70
|
-
// therefore no push). RecordApiFragmentPush records the outcome only — it changes no fragments,
|
|
71
|
-
// so it emits no ApiSchemaComputed (which also avoids a recompute loop).
|
|
72
|
-
let decide = (state, command) => {
|
|
73
|
-
let withSchema = (fact: ApiFragmentRegistrySpec.event) => {
|
|
74
|
-
let next = evolve(state, fact)
|
|
75
|
-
Ok([fact, ApiSchemaComputed({snapshot: snapshotOf(next.entries)})])
|
|
76
|
-
}
|
|
77
|
-
switch command {
|
|
78
|
-
| RegisterApiFragment({pluginId, fragment, apiTarget, at}) =>
|
|
79
|
-
switch state.entries->Dict.get(pluginId) {
|
|
80
|
-
| None => withSchema(ApiFragmentRegistered({pluginId, fragment, apiTarget, at}))
|
|
81
|
-
// Idempotent: an unchanged re-registration (same fragment AND same target — a redeploy
|
|
82
|
-
// carrying the same schema, or a version supersession that didn't change the SDL) emits
|
|
83
|
-
// nothing. A target change with an identical fragment IS meaningful (the fields move between
|
|
84
|
-
// the Domain and Platform APIs), so it is not a no-op.
|
|
85
|
-
| Some({fragment: current, apiTarget: currentTarget})
|
|
86
|
-
if current == fragment && currentTarget == apiTarget =>
|
|
87
|
-
Ok([])
|
|
88
|
-
| Some({fragment: current}) =>
|
|
89
|
-
withSchema(
|
|
90
|
-
ApiFragmentUpdated({pluginId, previousFragment: current, newFragment: fragment, apiTarget, at}),
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
| DeregisterApiFragment({pluginId}) =>
|
|
94
|
-
switch state.entries->Dict.get(pluginId) {
|
|
95
|
-
| None => Ok([]) // idempotent — nothing registered
|
|
96
|
-
| Some(_) => withSchema(ApiFragmentDeregistered({pluginId: pluginId}))
|
|
97
|
-
}
|
|
98
|
-
| RecordApiFragmentPush({pluginId, ok, message, at}) =>
|
|
99
|
-
switch state.entries->Dict.get(pluginId) {
|
|
100
|
-
| None => Ok([]) // fragment deregistered in the meantime — nowhere to record
|
|
101
|
-
// Idempotent: an at-least-once redelivery carries the identical payload.
|
|
102
|
-
| Some({lastPush: Some((lastOk, lastMessage, lastAt))})
|
|
103
|
-
if lastOk == ok && lastMessage == message && lastAt == at =>
|
|
104
|
-
Ok([])
|
|
105
|
-
| Some(_) => Ok([ApiFragmentPushRecorded({pluginId, ok, message, at})])
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
-
|
|
3
|
-
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
4
|
-
|
|
5
|
-
let initialState = {
|
|
6
|
-
entries: {}
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
function entriesWith(entries, pluginId, entry) {
|
|
10
|
-
return Object.fromEntries(Object.entries(entries).filter(param => param[0] !== pluginId).concat([[
|
|
11
|
-
pluginId,
|
|
12
|
-
entry
|
|
13
|
-
]]));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function entriesWithout(entries, pluginId) {
|
|
17
|
-
return Object.fromEntries(Object.entries(entries).filter(param => param[0] !== pluginId));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function evolve(state, event) {
|
|
21
|
-
switch (event.TAG) {
|
|
22
|
-
case "ApiFragmentRegistered" :
|
|
23
|
-
return {
|
|
24
|
-
entries: entriesWith(state.entries, event.pluginId, {
|
|
25
|
-
fragment: event.fragment,
|
|
26
|
-
apiTarget: event.apiTarget,
|
|
27
|
-
lastPush: undefined
|
|
28
|
-
})
|
|
29
|
-
};
|
|
30
|
-
case "ApiFragmentUpdated" :
|
|
31
|
-
return {
|
|
32
|
-
entries: entriesWith(state.entries, event.pluginId, {
|
|
33
|
-
fragment: event.newFragment,
|
|
34
|
-
apiTarget: event.apiTarget,
|
|
35
|
-
lastPush: undefined
|
|
36
|
-
})
|
|
37
|
-
};
|
|
38
|
-
case "ApiFragmentDeregistered" :
|
|
39
|
-
return {
|
|
40
|
-
entries: entriesWithout(state.entries, event.pluginId)
|
|
41
|
-
};
|
|
42
|
-
case "ApiFragmentPushRecorded" :
|
|
43
|
-
let pluginId = event.pluginId;
|
|
44
|
-
let entry = state.entries[pluginId];
|
|
45
|
-
if (entry !== undefined) {
|
|
46
|
-
return {
|
|
47
|
-
entries: entriesWith(state.entries, pluginId, {
|
|
48
|
-
fragment: entry.fragment,
|
|
49
|
-
apiTarget: entry.apiTarget,
|
|
50
|
-
lastPush: [
|
|
51
|
-
event.ok,
|
|
52
|
-
event.message,
|
|
53
|
-
event.at
|
|
54
|
-
]
|
|
55
|
-
})
|
|
56
|
-
};
|
|
57
|
-
} else {
|
|
58
|
-
return state;
|
|
59
|
-
}
|
|
60
|
-
case "ApiSchemaComputed" :
|
|
61
|
-
return state;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function snapshotOf(entries) {
|
|
66
|
-
return Object.entries(entries).map(param => {
|
|
67
|
-
let e = param[1];
|
|
68
|
-
return {
|
|
69
|
-
pluginId: param[0],
|
|
70
|
-
encoded: e.fragment.encoded,
|
|
71
|
-
protocol: e.fragment.protocol,
|
|
72
|
-
apiTarget: e.apiTarget
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function decide(state, command) {
|
|
78
|
-
let withSchema = fact => {
|
|
79
|
-
let next = evolve(state, fact);
|
|
80
|
-
return {
|
|
81
|
-
TAG: "Ok",
|
|
82
|
-
_0: [
|
|
83
|
-
fact,
|
|
84
|
-
{
|
|
85
|
-
TAG: "ApiSchemaComputed",
|
|
86
|
-
snapshot: snapshotOf(next.entries)
|
|
87
|
-
}
|
|
88
|
-
]
|
|
89
|
-
};
|
|
90
|
-
};
|
|
91
|
-
switch (command.TAG) {
|
|
92
|
-
case "RegisterApiFragment" :
|
|
93
|
-
let at = command.at;
|
|
94
|
-
let apiTarget = command.apiTarget;
|
|
95
|
-
let fragment = command.fragment;
|
|
96
|
-
let pluginId = command.pluginId;
|
|
97
|
-
let match = state.entries[pluginId];
|
|
98
|
-
if (match === undefined) {
|
|
99
|
-
return withSchema({
|
|
100
|
-
TAG: "ApiFragmentRegistered",
|
|
101
|
-
pluginId: pluginId,
|
|
102
|
-
fragment: fragment,
|
|
103
|
-
apiTarget: apiTarget,
|
|
104
|
-
at: at
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
let current = match.fragment;
|
|
108
|
-
if (Primitive_object.equal(current, fragment) && match.apiTarget === apiTarget) {
|
|
109
|
-
return {
|
|
110
|
-
TAG: "Ok",
|
|
111
|
-
_0: []
|
|
112
|
-
};
|
|
113
|
-
} else {
|
|
114
|
-
return withSchema({
|
|
115
|
-
TAG: "ApiFragmentUpdated",
|
|
116
|
-
pluginId: pluginId,
|
|
117
|
-
previousFragment: current,
|
|
118
|
-
newFragment: fragment,
|
|
119
|
-
apiTarget: apiTarget,
|
|
120
|
-
at: at
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
case "DeregisterApiFragment" :
|
|
124
|
-
let pluginId$1 = command.pluginId;
|
|
125
|
-
let match$1 = state.entries[pluginId$1];
|
|
126
|
-
if (match$1 !== undefined) {
|
|
127
|
-
return withSchema({
|
|
128
|
-
TAG: "ApiFragmentDeregistered",
|
|
129
|
-
pluginId: pluginId$1
|
|
130
|
-
});
|
|
131
|
-
} else {
|
|
132
|
-
return {
|
|
133
|
-
TAG: "Ok",
|
|
134
|
-
_0: []
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
case "RecordApiFragmentPush" :
|
|
138
|
-
let at$1 = command.at;
|
|
139
|
-
let message = command.message;
|
|
140
|
-
let ok = command.ok;
|
|
141
|
-
let pluginId$2 = command.pluginId;
|
|
142
|
-
let match$2 = state.entries[pluginId$2];
|
|
143
|
-
if (match$2 === undefined) {
|
|
144
|
-
return {
|
|
145
|
-
TAG: "Ok",
|
|
146
|
-
_0: []
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
let match$3 = match$2.lastPush;
|
|
150
|
-
if (match$3 !== undefined && match$3[0] === ok && match$3[1] === message && match$3[2] === at$1) {
|
|
151
|
-
return {
|
|
152
|
-
TAG: "Ok",
|
|
153
|
-
_0: []
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
TAG: "Ok",
|
|
158
|
-
_0: [{
|
|
159
|
-
TAG: "ApiFragmentPushRecorded",
|
|
160
|
-
pluginId: pluginId$2,
|
|
161
|
-
ok: ok,
|
|
162
|
-
message: message,
|
|
163
|
-
at: at$1
|
|
164
|
-
}]
|
|
165
|
-
};
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
let Spec;
|
|
171
|
-
|
|
172
|
-
let snapshot;
|
|
173
|
-
|
|
174
|
-
let atomicCounter;
|
|
175
|
-
|
|
176
|
-
let moduleUrl = "@reventlessdev/reventless-core/src/admin/ApiFragmentRegistry/ApiFragmentRegistryBehavior.res.mjs";
|
|
177
|
-
|
|
178
|
-
export {
|
|
179
|
-
Spec,
|
|
180
|
-
initialState,
|
|
181
|
-
snapshot,
|
|
182
|
-
atomicCounter,
|
|
183
|
-
entriesWith,
|
|
184
|
-
entriesWithout,
|
|
185
|
-
evolve,
|
|
186
|
-
snapshotOf,
|
|
187
|
-
decide,
|
|
188
|
-
moduleUrl,
|
|
189
|
-
}
|
|
190
|
-
/* No side effect */
|