@reventlessdev/reventless-local 3.0.0-alpha.249 → 3.0.0-alpha.250
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 +8 -0
- package/package.json +13 -13
- package/src/BakedManifest.res +5 -6
- package/src/EmitCapabilities.res +3 -1
- package/src/LocalPlatformRegistry.res +3 -1
- package/src/LocalPlatformStart.res +7 -6
- package/src/LocalSeedTarget.res +3 -1
- package/src/Platform.res +291 -215
- package/src/ShellConfig.res +10 -17
- package/src/UiHints.res +8 -14
- package/src/UiSlots.res +10 -17
- package/src/adapter/Api/LocalEvents_Server.res +5 -3
- package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
- package/src/adapter/Auth/LocalAuth.res +11 -15
- package/src/adapter/Auth/UserStore.res +5 -7
- package/src/adapter/Backend.res +1 -2
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
- package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
- package/src/adapter/Counter/LocalCounterHandler.res +1 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
- package/src/adapter/DomainGraphQL_Server.res +35 -34
- package/src/adapter/DomainMCP_Server.res +45 -41
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
- package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
- package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
- package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
- package/src/adapter/LocalBus.res +12 -12
- package/src/adapter/LocalCapabilities.res.mjs +1 -1
- package/src/adapter/LocalGeocodeResolvers.res +1 -4
- package/src/adapter/LocalSideEffectHandler.res +1 -1
- package/src/adapter/LocalStateChangeDescriptor.res +2 -1
- package/src/adapter/MCP_ServerInstance.res +51 -39
- package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
- package/src/adapter/PlatformGraphQL_Server.res +9 -5
- package/src/adapter/ProjectionCheckpoint.res +8 -1
- package/src/adapter/ProjectionPending.res +1 -3
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
- package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
- package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
- package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
- package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
- package/src/adapter/SqliteDriver.res +16 -4
- package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
- package/src/components/Aggregate_Builder.res +6 -1
- package/src/components/Counter_Builder.res +1 -5
- package/src/components/DcbEventLog_Builder.res +5 -3
- package/src/components/ExtensionPoint_Builder.res +13 -15
- package/src/components/InboundTranslationSlice_Builder.res +3 -2
- package/src/components/OutboundTranslationSlice_Builder.res +3 -2
- package/src/components/ReadModel_Builder.res +18 -20
- package/src/components/StateViewSlice_Builder.res +3 -2
- package/src/components/Task_Builder.res +12 -13
- package/src/reset/LocalSeedReset.res +2 -5
- package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
- package/src/test/Mocks/MockEventLogStorage.res +8 -3
- package/src/test/Mocks/MockQueryDbStorage.res +3 -2
- package/src/test/TestRunner.res +1 -1
- package/tests/BakedManifestFilesTest.res +7 -10
- package/tests/LocalEventTapTest.res +26 -21
- package/tests/LocalPlatformStartTest.res +33 -28
- package/tests/PluginEventDecodeTest.res +3 -3
- package/tests/ShellConfigTest.res +30 -21
- package/tests/SplitApiFixtures.res +34 -10
- package/tests/SplitApiTest.res +92 -62
- package/tests/adapter/BackendParityTest.res +180 -130
- package/tests/adapter/CollectNEventsTest.res +6 -4
- package/tests/adapter/CommandAuthorizationTest.res +47 -41
- package/tests/adapter/CommandTopicChannelTest.res +163 -139
- package/tests/adapter/CounterHandlerTest.res +95 -77
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
- package/tests/adapter/DcbEventLogStorageTest.res +264 -207
- package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
- package/tests/adapter/EventCollectorChannelTest.res +153 -124
- package/tests/adapter/EventHistoryResolverTest.res +46 -18
- package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
- package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
- package/tests/adapter/EventTapTest.res +37 -20
- package/tests/adapter/EventTopicPublisherTest.res +74 -50
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
- package/tests/adapter/HeartbeatRunnerTest.res +99 -81
- package/tests/adapter/InboundTranslationMutationTest.res +4 -1
- package/tests/adapter/LocalAuthHttpTest.res +79 -85
- package/tests/adapter/LocalAuthLoginTest.res +42 -50
- package/tests/adapter/LocalAuthTest.res +8 -16
- package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
- package/tests/adapter/LocalBusBoundedTest.res +187 -117
- package/tests/adapter/LocalBusPubSubTest.res +118 -76
- package/tests/adapter/LocalDispatchContextTest.res +1 -2
- package/tests/adapter/LocalEvents_ServerTest.res +242 -187
- package/tests/adapter/PgProjectionCatchupTest.res +44 -10
- package/tests/adapter/ProjectionCheckpointTest.res +261 -206
- package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
- package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
- package/tests/adapter/QueryDbListResolverTest.res +225 -208
- package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
- package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
- package/tests/adapter/QueryDbStorageTest.res +163 -117
- package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
- package/tests/adapter/QueryEngineTest.res +179 -126
- package/tests/adapter/ScheduledPublisherTest.res +171 -135
- package/tests/adapter/ServedBucketHttpTest.res +65 -59
- package/tests/adapter/SortKey_FilterTest.res +19 -3
- package/tests/adapter/SqliteDriverTest.res +13 -10
- package/tests/adapter/TaskBucketTest.res +94 -80
- package/tests/components/aggregate/AggregateTest.res +31 -18
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
- package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
- package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
- package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
- package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
- package/tests/components/commandtopic/CommandTopicTest.res +24 -15
- package/tests/components/counter/CounterFixtures.res +6 -8
- package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
- package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
- package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
- package/tests/components/dcb/DcbFixtures.res +4 -1
- package/tests/components/dcb/DcbTest.res +9 -6
- package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
- package/tests/components/eventcollector/EventCollectorTest.res +6 -3
- package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
- package/tests/components/eventlog/EventLogConflictTest.res +8 -4
- package/tests/components/eventlog/EventLogFixtures.res +3 -3
- package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
- package/tests/components/eventlog/EventLogStreamTest.res +119 -56
- package/tests/components/eventlog/EventLogTest.res +20 -4
- package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
- package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
- package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
- package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
- package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
- package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
- package/tests/components/heartbeat/HeartbeatTest.res +56 -47
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
- package/tests/components/querydb/QueryDbFixtures.res +1 -5
- package/tests/components/querydb/QueryDbTest.res +2 -4
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
- package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
- package/tests/components/readmodel/ReadModelFixtures.res +8 -8
- package/tests/components/scheduler/SchedulerTest.res +76 -58
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
- package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
- package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
- package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
- package/tests/components/task/TaskFixtures.res +5 -5
- package/tests/components/task/TaskTest.res +95 -80
- package/tests/plugin/PluginsProjection_GWT.res +3 -1
- package/tests/plugin/UiFragments_GWT.res +6 -1
- package/tests/reset/LocalSeedResetTest.res +1 -0
|
@@ -59,7 +59,7 @@ type unionCommand =
|
|
|
59
59
|
// expose one `Plugin_Command` field per constructor).
|
|
60
60
|
@schema
|
|
61
61
|
type dcbSingleCommand =
|
|
62
|
-
|
|
62
|
+
AddCategory({categoryId: @s.matches(Reventless.DcbTag.string) string, name: string})
|
|
63
63
|
|
|
64
64
|
@schema
|
|
65
65
|
type dcbMultiCommand =
|
|
@@ -184,410 +184,479 @@ let scanStateSchemaWithAnnotations = scanStateSchema->S.Metadata.set(
|
|
|
184
184
|
|
|
185
185
|
describe("GraphQL_SchemaInspector", () => {
|
|
186
186
|
describe("inspectScalar", () => {
|
|
187
|
-
testPromise(
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
testPromise(
|
|
188
|
+
"plain string derives to String",
|
|
189
|
+
async () => {
|
|
190
|
+
expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(stringSchema))->toBe("String")
|
|
191
|
+
},
|
|
192
|
+
)
|
|
190
193
|
|
|
191
|
-
testPromise(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
testPromise(
|
|
195
|
+
"tagged string derives to ID",
|
|
196
|
+
async () => {
|
|
197
|
+
expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(taggedSchema))->toBe("ID")
|
|
198
|
+
},
|
|
199
|
+
)
|
|
194
200
|
|
|
195
|
-
testPromise(
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
testPromise(
|
|
202
|
+
"float derives to Float",
|
|
203
|
+
async () => {
|
|
204
|
+
expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(numberSchema))->toBe("Float")
|
|
205
|
+
},
|
|
206
|
+
)
|
|
198
207
|
|
|
199
|
-
testPromise(
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
testPromise(
|
|
209
|
+
"bool derives to Boolean",
|
|
210
|
+
async () => {
|
|
211
|
+
expect(ReventlessCore.GraphQL_SchemaInspector.inspectScalar(boolSchema))->toBe("Boolean")
|
|
212
|
+
},
|
|
213
|
+
)
|
|
202
214
|
})
|
|
203
215
|
|
|
204
216
|
describe("inspectObjectType", () => {
|
|
205
|
-
testPromise(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
testPromise(
|
|
218
|
+
"derives type definition from object schema",
|
|
219
|
+
async () => {
|
|
220
|
+
let result = ReventlessCore.GraphQL_SchemaInspector.inspectObjectType(
|
|
221
|
+
~typeName="TestState",
|
|
222
|
+
testStateSchema,
|
|
223
|
+
)
|
|
224
|
+
expect(result->Option.isSome)->toBe(true)
|
|
225
|
+
let sdl = result->Option.getOrThrow
|
|
226
|
+
expect(sdl->String.includes("type TestState"))->toBe(true)
|
|
227
|
+
expect(sdl->String.includes("id: ID!"))->toBe(true)
|
|
228
|
+
expect(sdl->String.includes("name: String"))->toBe(true)
|
|
229
|
+
expect(sdl->String.includes("price: Float"))->toBe(true)
|
|
230
|
+
expect(sdl->String.includes("active: Boolean"))->toBe(true)
|
|
231
|
+
},
|
|
232
|
+
)
|
|
218
233
|
|
|
219
|
-
testPromise(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
234
|
+
testPromise(
|
|
235
|
+
"returns None for non-object schema",
|
|
236
|
+
async () => {
|
|
237
|
+
let result = ReventlessCore.GraphQL_SchemaInspector.inspectObjectType(
|
|
238
|
+
~typeName="Foo",
|
|
239
|
+
stringSchema,
|
|
240
|
+
)
|
|
241
|
+
expect(result->Option.isNone)->toBe(true)
|
|
242
|
+
},
|
|
243
|
+
)
|
|
226
244
|
})
|
|
227
245
|
|
|
228
246
|
describe("inspectMutationFields", () => {
|
|
229
|
-
testPromise(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
247
|
+
testPromise(
|
|
248
|
+
"derives fields from single-variant (DCB slice) command",
|
|
249
|
+
async () => {
|
|
250
|
+
let fields = ReventlessCore.GraphQL_SchemaInspector.inspectMutationFields(
|
|
251
|
+
~fieldPrefix="Catalog_AddProduct",
|
|
252
|
+
addCommandSchema,
|
|
253
|
+
)
|
|
254
|
+
expect(fields->Array.length)->toBe(1)
|
|
255
|
+
let field = fields->Array.getUnsafe(0)
|
|
256
|
+
expect(field->String.includes("Catalog_AddProduct"))->toBe(true)
|
|
257
|
+
expect(field->String.includes("productId: ID!"))->toBe(true)
|
|
258
|
+
expect(field->String.includes("name: String"))->toBe(true)
|
|
259
|
+
},
|
|
260
|
+
)
|
|
240
261
|
|
|
241
|
-
testPromise(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
testPromise(
|
|
263
|
+
"derives fields from union (aggregate) command",
|
|
264
|
+
async () => {
|
|
265
|
+
let fields = ReventlessCore.GraphQL_SchemaInspector.inspectMutationFields(
|
|
266
|
+
~fieldPrefix="App_Item",
|
|
267
|
+
unionCommandSchema,
|
|
268
|
+
)
|
|
269
|
+
expect(fields->Array.length)->toBe(2)
|
|
270
|
+
let createField = fields->Array.getUnsafe(0)
|
|
271
|
+
expect(createField->String.includes("App_Item_Create"))->toBe(true)
|
|
272
|
+
expect(createField->String.includes("itemId: ID!"))->toBe(true)
|
|
273
|
+
let renameField = fields->Array.getUnsafe(1)
|
|
274
|
+
expect(renameField->String.includes("App_Item_Rename"))->toBe(true)
|
|
275
|
+
expect(renameField->String.includes("newName: String"))->toBe(true)
|
|
276
|
+
},
|
|
277
|
+
)
|
|
254
278
|
|
|
255
|
-
testPromise(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
279
|
+
testPromise(
|
|
280
|
+
"returns empty for non-object/non-union schema",
|
|
281
|
+
async () => {
|
|
282
|
+
let fields = ReventlessCore.GraphQL_SchemaInspector.inspectMutationFields(
|
|
283
|
+
~fieldPrefix="Foo",
|
|
284
|
+
stringSchema,
|
|
285
|
+
)
|
|
286
|
+
expect(fields->Array.length)->toBe(0)
|
|
287
|
+
},
|
|
288
|
+
)
|
|
262
289
|
})
|
|
263
290
|
|
|
264
291
|
describe("inspectQueryFields", () => {
|
|
265
|
-
testPromise(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
testPromise(
|
|
293
|
+
"derives type def and query fields from state schema",
|
|
294
|
+
async () => {
|
|
295
|
+
let result = ReventlessCore.GraphQL_SchemaInspector.inspectQueryFields(
|
|
296
|
+
~name="Catalog_Product",
|
|
297
|
+
~typeName="CatalogProduct",
|
|
298
|
+
testStateSchema,
|
|
299
|
+
)
|
|
300
|
+
expect(result.typeDef->Option.isSome)->toBe(true)
|
|
301
|
+
let typeDef = result.typeDef->Option.getOrThrow
|
|
302
|
+
expect(typeDef->String.includes("type CatalogProduct"))->toBe(true)
|
|
303
|
+
expect(
|
|
304
|
+
result.singleQuery->String.includes("Catalog_Product(id: ID!, includeRetired: Boolean)"),
|
|
305
|
+
)->toBe(true)
|
|
306
|
+
expect(result.singleQuery->String.includes("CatalogProduct"))->toBe(true)
|
|
307
|
+
expect(result.listQuery->Option.isSome)->toBe(true)
|
|
308
|
+
let listQ = result.listQuery->Option.getOrThrow
|
|
309
|
+
expect(listQ->String.includes("Catalog_Products"))->toBe(true)
|
|
310
|
+
},
|
|
311
|
+
)
|
|
280
312
|
})
|
|
281
313
|
|
|
282
314
|
// ── includeIdParam Tests ──────────────────────────────────────────────────
|
|
283
315
|
|
|
284
316
|
describe("includeIdParam — ReadModel vs StateViewSlice", () => {
|
|
285
|
-
testPromise(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
317
|
+
testPromise(
|
|
318
|
+
"ReadModel fragment: query has (id: ID!) and type has injected id: ID!",
|
|
319
|
+
async () => {
|
|
320
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
321
|
+
~mutationEntries=[],
|
|
322
|
+
~queryEntries=[
|
|
323
|
+
{
|
|
324
|
+
singleFieldName: "RM_Product",
|
|
325
|
+
listFieldName: "RM_Products",
|
|
326
|
+
returnTypeName: "RMProduct",
|
|
327
|
+
stateSchema: testStateSchema->S.castToUnknown,
|
|
328
|
+
authorization: None,
|
|
329
|
+
includeIdParam: true,
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
)
|
|
333
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
334
|
+
let sdl = inspection.sdlPreview
|
|
335
|
+
expect(
|
|
336
|
+
sdl->String.includes("RM_Product(id: ID!, includeRetired: Boolean): RMProduct"),
|
|
337
|
+
)->toBe(true)
|
|
338
|
+
expect(sdl->String.includes("type RMProduct"))->toBe(true)
|
|
339
|
+
// The type should have an injected id: ID! field (first field in the type)
|
|
340
|
+
let typeLines = sdl->String.split("\n")
|
|
341
|
+
let idFieldInType =
|
|
342
|
+
typeLines->Array.some(
|
|
343
|
+
line => line->String.trim == "id: ID!" && !(line->String.includes("(")),
|
|
344
|
+
)
|
|
345
|
+
expect(idFieldInType)->toBe(true)
|
|
346
|
+
},
|
|
347
|
+
)
|
|
311
348
|
|
|
312
|
-
testPromise(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
349
|
+
testPromise(
|
|
350
|
+
"StateViewSlice fragment: query has no (id: ID!) and type has no injected id",
|
|
351
|
+
async () => {
|
|
352
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
353
|
+
~mutationEntries=[],
|
|
354
|
+
~queryEntries=[
|
|
355
|
+
{
|
|
356
|
+
singleFieldName: "SV_Item",
|
|
357
|
+
listFieldName: "SV_Items",
|
|
358
|
+
returnTypeName: "SVItem",
|
|
359
|
+
stateSchema: svStateSchema->S.castToUnknown,
|
|
360
|
+
authorization: None,
|
|
361
|
+
includeIdParam: false,
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
)
|
|
365
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
366
|
+
let sdl = inspection.sdlPreview
|
|
367
|
+
// Single query should NOT have (id: ID!) parameter
|
|
368
|
+
expect(sdl->String.includes("SV_Item: SVItem"))->toBe(true)
|
|
369
|
+
expect(sdl->String.includes("SV_Item(id: ID!)"))->toBe(false)
|
|
370
|
+
// The type should NOT have an injected id: ID! field
|
|
371
|
+
expect(sdl->String.includes("type SVItem"))->toBe(true)
|
|
372
|
+
expect(sdl->String.includes("productId: ID!"))->toBe(true)
|
|
373
|
+
// Count id: ID! occurrences — should be zero (productId uses ID! but "id: ID!" standalone should not appear)
|
|
374
|
+
let typeSection = sdl->String.split("type SVItem")->Array.get(1)->Option.getOr("")
|
|
375
|
+
let typeEnd = typeSection->String.indexOf("}")
|
|
376
|
+
let typeEnd = typeEnd >= 0 ? typeEnd : typeSection->String.length
|
|
377
|
+
let typeBody = typeSection->String.slice(~start=0, ~end=typeEnd)
|
|
378
|
+
let hasInjectedId =
|
|
379
|
+
typeBody->String.split("\n")->Array.some(line => line->String.trim == "id: ID!")
|
|
380
|
+
expect(hasInjectedId)->toBe(false)
|
|
381
|
+
},
|
|
382
|
+
)
|
|
344
383
|
|
|
345
|
-
testPromise(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
384
|
+
testPromise(
|
|
385
|
+
"default includeIdParam (omitted) behaves like ReadModel",
|
|
386
|
+
async () => {
|
|
387
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
388
|
+
~mutationEntries=[],
|
|
389
|
+
~queryEntries=[
|
|
390
|
+
{
|
|
391
|
+
singleFieldName: "Default_Thing",
|
|
392
|
+
listFieldName: "Default_Things",
|
|
393
|
+
returnTypeName: "DefaultThing",
|
|
394
|
+
stateSchema: testStateSchema->S.castToUnknown,
|
|
395
|
+
authorization: None,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
)
|
|
399
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
400
|
+
let sdl = inspection.sdlPreview
|
|
401
|
+
expect(
|
|
402
|
+
sdl->String.includes("Default_Thing(id: ID!, includeRetired: Boolean): DefaultThing"),
|
|
403
|
+
)->toBe(true)
|
|
404
|
+
},
|
|
405
|
+
)
|
|
362
406
|
})
|
|
363
407
|
|
|
364
408
|
// ── Relay compliance — implements Node + Connection types ──────────────
|
|
365
409
|
|
|
366
410
|
describe("Relay compliance", () => {
|
|
367
|
-
testPromise(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
411
|
+
testPromise(
|
|
412
|
+
"entity type includes 'implements Node' when includeIdParam is true",
|
|
413
|
+
async () => {
|
|
414
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
415
|
+
~mutationEntries=[],
|
|
416
|
+
~queryEntries=[
|
|
417
|
+
{
|
|
418
|
+
singleFieldName: "Relay_Product",
|
|
419
|
+
listFieldName: "Relay_Products",
|
|
420
|
+
returnTypeName: "RelayProduct",
|
|
421
|
+
stateSchema: testStateSchema->S.castToUnknown,
|
|
422
|
+
authorization: None,
|
|
423
|
+
includeIdParam: true,
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
)
|
|
427
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
428
|
+
let sdl = inspection.sdlPreview
|
|
429
|
+
expect(sdl->String.includes("type RelayProduct implements Node"))->toBe(true)
|
|
430
|
+
expect(sdl->String.includes("id: ID!"))->toBe(true)
|
|
431
|
+
},
|
|
432
|
+
)
|
|
386
433
|
|
|
387
|
-
testPromise(
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
434
|
+
testPromise(
|
|
435
|
+
"non-entity type does not include 'implements Node'",
|
|
436
|
+
async () => {
|
|
437
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
438
|
+
~mutationEntries=[],
|
|
439
|
+
~queryEntries=[
|
|
440
|
+
{
|
|
441
|
+
singleFieldName: "Relay_View",
|
|
442
|
+
listFieldName: "Relay_Views",
|
|
443
|
+
returnTypeName: "RelayView",
|
|
444
|
+
stateSchema: svStateSchema->S.castToUnknown,
|
|
445
|
+
authorization: None,
|
|
446
|
+
includeIdParam: false,
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
)
|
|
450
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
451
|
+
let sdl = inspection.sdlPreview
|
|
452
|
+
expect(sdl->String.includes("type RelayView implements Node"))->toBe(false)
|
|
453
|
+
expect(sdl->String.includes("type RelayView {"))->toBe(true)
|
|
454
|
+
},
|
|
455
|
+
)
|
|
406
456
|
|
|
407
|
-
testPromise(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
457
|
+
testPromise(
|
|
458
|
+
"connectionSpec generates Edge and Connection types instead of plural wrapper",
|
|
459
|
+
async () => {
|
|
460
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
461
|
+
~mutationEntries=[],
|
|
462
|
+
~queryEntries=[
|
|
463
|
+
{
|
|
464
|
+
singleFieldName: "Relay_Product",
|
|
465
|
+
listFieldName: "Relay_Products",
|
|
466
|
+
returnTypeName: "RelayProduct",
|
|
467
|
+
stateSchema: testStateSchema->S.castToUnknown,
|
|
468
|
+
authorization: None,
|
|
469
|
+
includeIdParam: true,
|
|
470
|
+
connectionSpec: true,
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
)
|
|
474
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
475
|
+
let sdl = inspection.sdlPreview
|
|
476
|
+
// Should have Edge and Connection types
|
|
477
|
+
expect(sdl->String.includes("type RelayProductEdge"))->toBe(true)
|
|
478
|
+
expect(sdl->String.includes("node: RelayProduct!"))->toBe(true)
|
|
479
|
+
expect(sdl->String.includes("cursor: String!"))->toBe(true)
|
|
480
|
+
expect(sdl->String.includes("type RelayProductConnection"))->toBe(true)
|
|
481
|
+
expect(sdl->String.includes("edges: [RelayProductEdge!]!"))->toBe(true)
|
|
482
|
+
expect(sdl->String.includes("pageInfo: PageInfo!"))->toBe(true)
|
|
483
|
+
expect(sdl->String.includes("totalCount"))->toBe(false)
|
|
484
|
+
// Query field should use filter + first/after/last/before args (Phase 4)
|
|
485
|
+
expect(
|
|
486
|
+
sdl->String.includes(
|
|
487
|
+
"Relay_Products(filter: RelayProductFilter, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): RelayProductConnection!",
|
|
488
|
+
),
|
|
489
|
+
)->toBe(true)
|
|
490
|
+
// Connection filter input with search/searchPrefix/ids
|
|
491
|
+
expect(sdl->String.includes("input RelayProductFilter"))->toBe(true)
|
|
492
|
+
expect(sdl->String.includes("search: String"))->toBe(true)
|
|
493
|
+
expect(sdl->String.includes("searchPrefix: String"))->toBe(true)
|
|
494
|
+
expect(sdl->String.includes("ids: [ID!]"))->toBe(true)
|
|
495
|
+
// Should NOT have legacy plural wrapper type
|
|
496
|
+
expect(sdl->String.includes("items: [RelayProduct!]!"))->toBe(false)
|
|
497
|
+
expect(sdl->String.includes("nextToken:"))->toBe(false)
|
|
498
|
+
},
|
|
499
|
+
)
|
|
447
500
|
|
|
448
|
-
testPromise(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
sdl->String.includes(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
501
|
+
testPromise(
|
|
502
|
+
"subIdField generates ItemsFilter (distinct from connection Filter)",
|
|
503
|
+
async () => {
|
|
504
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
505
|
+
~mutationEntries=[],
|
|
506
|
+
~queryEntries=[
|
|
507
|
+
{
|
|
508
|
+
singleFieldName: "Items_Product",
|
|
509
|
+
listFieldName: "Items_Products",
|
|
510
|
+
returnTypeName: "ItemsProduct",
|
|
511
|
+
stateSchema: testStateSchema->S.castToUnknown,
|
|
512
|
+
authorization: None,
|
|
513
|
+
includeIdParam: true,
|
|
514
|
+
connectionSpec: true,
|
|
515
|
+
subIdField: "sku",
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
)
|
|
519
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
520
|
+
let sdl = inspection.sdlPreview
|
|
521
|
+
// Connection-level filter (search/searchPrefix/ids)
|
|
522
|
+
expect(sdl->String.includes("input ItemsProductFilter"))->toBe(true)
|
|
523
|
+
// Items query gets its own filter type (prefix/from/to/eq/order) — renamed
|
|
524
|
+
// to ItemsFilter to disambiguate from the connection-level Filter.
|
|
525
|
+
expect(sdl->String.includes("input ItemsProductItemsFilter"))->toBe(true)
|
|
526
|
+
expect(
|
|
527
|
+
sdl->String.includes("Items_ProductItems(id: ID!, filter: ItemsProductItemsFilter,"),
|
|
528
|
+
)->toBe(true)
|
|
529
|
+
},
|
|
530
|
+
)
|
|
477
531
|
|
|
478
|
-
testPromise(
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
532
|
+
testPromise(
|
|
533
|
+
"explicit connectionSpec=false generates legacy plural wrapper (opt-out)",
|
|
534
|
+
async () => {
|
|
535
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
536
|
+
~mutationEntries=[],
|
|
537
|
+
~queryEntries=[
|
|
538
|
+
{
|
|
539
|
+
singleFieldName: "Legacy_Product",
|
|
540
|
+
listFieldName: "Legacy_Products",
|
|
541
|
+
returnTypeName: "LegacyProduct",
|
|
542
|
+
stateSchema: testStateSchema->S.castToUnknown,
|
|
543
|
+
authorization: None,
|
|
544
|
+
includeIdParam: true,
|
|
545
|
+
connectionSpec: false,
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
)
|
|
549
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
550
|
+
let sdl = inspection.sdlPreview
|
|
551
|
+
expect(sdl->String.includes("items: [LegacyProduct!]!"))->toBe(true)
|
|
552
|
+
expect(sdl->String.includes("nextToken: String"))->toBe(true)
|
|
553
|
+
expect(sdl->String.includes("LegacyProductEdge"))->toBe(false)
|
|
554
|
+
expect(sdl->String.includes("LegacyProductConnection"))->toBe(false)
|
|
555
|
+
},
|
|
556
|
+
)
|
|
500
557
|
|
|
501
|
-
testPromise(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
558
|
+
testPromise(
|
|
559
|
+
"stitcher injects Node interface and node query",
|
|
560
|
+
async () => {
|
|
561
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
562
|
+
~mutationEntries=[],
|
|
563
|
+
~queryEntries=[
|
|
564
|
+
{
|
|
565
|
+
singleFieldName: "Stitch_Product",
|
|
566
|
+
listFieldName: "Stitch_Products",
|
|
567
|
+
returnTypeName: "StitchProduct",
|
|
568
|
+
stateSchema: testStateSchema->S.castToUnknown,
|
|
569
|
+
authorization: None,
|
|
570
|
+
includeIdParam: true,
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
)
|
|
574
|
+
let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
|
|
575
|
+
~baseFragment=fragment,
|
|
576
|
+
~pluginFragments=[],
|
|
577
|
+
)
|
|
578
|
+
expect(sdl->String.includes("interface Node"))->toBe(true)
|
|
579
|
+
expect(sdl->String.includes("node(id: ID!): Node"))->toBe(true)
|
|
580
|
+
expect(sdl->String.includes("type PageInfo"))->toBe(true)
|
|
581
|
+
expect(sdl->String.includes("hasNextPage: Boolean!"))->toBe(true)
|
|
582
|
+
},
|
|
583
|
+
)
|
|
521
584
|
})
|
|
522
585
|
|
|
523
586
|
// ── Plugin Level — Fragment Inspector ───────────────────────────────────
|
|
524
587
|
|
|
525
588
|
describe("inspectFragment", () => {
|
|
526
|
-
testPromise(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
589
|
+
testPromise(
|
|
590
|
+
"decodes and previews a fragment",
|
|
591
|
+
async () => {
|
|
592
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
593
|
+
~mutationEntries=[
|
|
594
|
+
{
|
|
595
|
+
fieldNames: ["Test_Add"],
|
|
596
|
+
commandSchema: addCommandSchema->S.castToUnknown,
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
~queryEntries=[
|
|
600
|
+
{
|
|
601
|
+
singleFieldName: "Test_State",
|
|
602
|
+
listFieldName: "Test_States",
|
|
603
|
+
returnTypeName: "TestState",
|
|
604
|
+
stateSchema: testStateSchema->S.castToUnknown,
|
|
605
|
+
authorization: None,
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
)
|
|
609
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
610
|
+
// TestState, TestStateEdge, TestStateConnection, TestStateFilter (connection filter)
|
|
611
|
+
// + TestStateRef (the reference door's projection)
|
|
612
|
+
// + CommandResult union + CommandAccepted + CommandRejected + CommandPending
|
|
613
|
+
// (auto-injected whenever the fragment emits any mutation field).
|
|
614
|
+
expect(inspection.types->Array.length)->toBe(9)
|
|
615
|
+
expect(inspection.mutations->Array.length)->toBe(1)
|
|
616
|
+
// single Test_State(id), list Test_States(...), Test_StatesByIds(ids: [String!]!)
|
|
617
|
+
// and Test_StatesRefs(ids: [ID!]!) — the by-ids read projected to a reference.
|
|
618
|
+
expect(inspection.queries->Array.length)->toBe(4)
|
|
619
|
+
expect(inspection.sdlPreview->String.includes("type TestState"))->toBe(true)
|
|
620
|
+
expect(inspection.sdlPreview->String.includes("type TestStateEdge"))->toBe(true)
|
|
621
|
+
expect(inspection.sdlPreview->String.includes("type TestStateConnection"))->toBe(true)
|
|
622
|
+
expect(inspection.sdlPreview->String.includes("input TestStateFilter"))->toBe(true)
|
|
623
|
+
expect(inspection.sdlPreview->String.includes("edges: [TestStateEdge!]!"))->toBe(true)
|
|
624
|
+
expect(inspection.sdlPreview->String.includes("Test_Add"))->toBe(true)
|
|
625
|
+
expect(inspection.sdlPreview->String.includes("Test_State"))->toBe(true)
|
|
626
|
+
expect(inspection.sdlPreview->String.includes("TestStateConnection!"))->toBe(true)
|
|
627
|
+
expect(inspection.sdlPreview->String.includes("union CommandResult"))->toBe(true)
|
|
628
|
+
},
|
|
629
|
+
)
|
|
564
630
|
})
|
|
565
631
|
|
|
566
632
|
describe("inspectPluginEntries", () => {
|
|
567
|
-
testPromise(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
633
|
+
testPromise(
|
|
634
|
+
"formats mutation and query entries",
|
|
635
|
+
async () => {
|
|
636
|
+
let summary = ReventlessCore.GraphQL_SchemaInspector.inspectPluginEntries(
|
|
637
|
+
~mutationEntries=[
|
|
638
|
+
{
|
|
639
|
+
fieldNames: ["Plugin_Add", "Plugin_Remove"],
|
|
640
|
+
commandSchema: unionCommandSchema->S.castToUnknown,
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
~queryEntries=[
|
|
644
|
+
{
|
|
645
|
+
singleFieldName: "Plugin_Item",
|
|
646
|
+
listFieldName: "Plugin_Items",
|
|
647
|
+
returnTypeName: "PluginItem",
|
|
648
|
+
stateSchema: testStateSchema->S.castToUnknown,
|
|
649
|
+
authorization: None,
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
)
|
|
653
|
+
expect(summary->String.includes("Mutations (2)"))->toBe(true)
|
|
654
|
+
expect(summary->String.includes("Plugin_Add"))->toBe(true)
|
|
655
|
+
expect(summary->String.includes("Plugin_Remove"))->toBe(true)
|
|
656
|
+
expect(summary->String.includes("Queries (1)"))->toBe(true)
|
|
657
|
+
expect(summary->String.includes("Plugin_Item(id) -> PluginItem"))->toBe(true)
|
|
658
|
+
},
|
|
659
|
+
)
|
|
591
660
|
})
|
|
592
661
|
|
|
593
662
|
// ── Platform Level — GraphQL_Server diagnostics ─────────────────────────
|
|
@@ -600,93 +669,102 @@ describe("GraphQL_SchemaInspector", () => {
|
|
|
600
669
|
// one `*Id` field, so the key is knowable without `@id` and the queryable now
|
|
601
670
|
// gets that key's eq filter and sort. The search-only case is the one below,
|
|
602
671
|
// where nothing identifies the row.
|
|
603
|
-
testPromise(
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
672
|
+
testPromise(
|
|
673
|
+
"read model with no annotations gets its inferred key's Eq + OrderBy",
|
|
674
|
+
async () => {
|
|
675
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
676
|
+
~mutationEntries=[],
|
|
677
|
+
~queryEntries=[
|
|
678
|
+
{
|
|
679
|
+
singleFieldName: "Plain_View",
|
|
680
|
+
listFieldName: "Plain_Views",
|
|
681
|
+
returnTypeName: "PlainView",
|
|
682
|
+
stateSchema: plainStateSchema->S.castToUnknown,
|
|
683
|
+
authorization: None,
|
|
684
|
+
includeIdParam: false,
|
|
685
|
+
connectionSpec: true,
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
)
|
|
689
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
690
|
+
let sdl = inspection.sdlPreview
|
|
691
|
+
// The legacy block is untouched …
|
|
692
|
+
expect(sdl->String.includes("input PlainViewFilter"))->toBe(true)
|
|
693
|
+
expect(sdl->String.includes("search: String"))->toBe(true)
|
|
694
|
+
expect(sdl->String.includes("searchPrefix: String"))->toBe(true)
|
|
695
|
+
expect(sdl->String.includes("ids: [ID!]"))->toBe(true)
|
|
696
|
+
// … and `productId`, the sole candidate, now narrows and sorts.
|
|
697
|
+
expect(sdl->String.includes("productIdEq: ID"))->toBe(true)
|
|
698
|
+
expect(sdl->String.includes("PlainViewOrderBy"))->toBe(true)
|
|
699
|
+
expect(sdl->String.includes("PlainViewOrderField"))->toBe(true)
|
|
700
|
+
expect(sdl->String.includes("orderBy:"))->toBe(true)
|
|
701
|
+
// Only the key — an inferred key never drags the rest of the record in.
|
|
702
|
+
expect(sdl->String.includes("nameEq:"))->toBe(false)
|
|
703
|
+
},
|
|
704
|
+
)
|
|
633
705
|
|
|
634
706
|
// The remaining empty-capability case: no `*Id` field, so nothing says which
|
|
635
707
|
// field identifies a row and the ladder declines rather than guessing.
|
|
636
|
-
testPromise(
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
708
|
+
testPromise(
|
|
709
|
+
"read model whose key cannot be resolved stays search-only",
|
|
710
|
+
async () => {
|
|
711
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
712
|
+
~mutationEntries=[],
|
|
713
|
+
~queryEntries=[
|
|
714
|
+
{
|
|
715
|
+
singleFieldName: "Keyless_View",
|
|
716
|
+
listFieldName: "Keyless_Views",
|
|
717
|
+
returnTypeName: "KeylessView",
|
|
718
|
+
stateSchema: keylessStateSchema->S.castToUnknown,
|
|
719
|
+
authorization: None,
|
|
720
|
+
includeIdParam: false,
|
|
721
|
+
connectionSpec: true,
|
|
722
|
+
},
|
|
723
|
+
],
|
|
724
|
+
)
|
|
725
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
726
|
+
let sdl = inspection.sdlPreview
|
|
727
|
+
expect(sdl->String.includes("input KeylessViewFilter"))->toBe(true)
|
|
728
|
+
expect(sdl->String.includes("search: String"))->toBe(true)
|
|
729
|
+
expect(sdl->String.includes("Eq:"))->toBe(false)
|
|
730
|
+
expect(sdl->String.includes("KeylessViewOrderBy"))->toBe(false)
|
|
731
|
+
expect(sdl->String.includes("KeylessViewOrderField"))->toBe(false)
|
|
732
|
+
expect(sdl->String.includes("orderBy:"))->toBe(false)
|
|
733
|
+
},
|
|
734
|
+
)
|
|
660
735
|
|
|
661
|
-
testPromise(
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
736
|
+
testPromise(
|
|
737
|
+
"read model with @id + @index emits Eq filters + OrderBy + orderBy arg",
|
|
738
|
+
async () => {
|
|
739
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
740
|
+
~mutationEntries=[],
|
|
741
|
+
~queryEntries=[
|
|
742
|
+
{
|
|
743
|
+
singleFieldName: "Indexed_Product",
|
|
744
|
+
listFieldName: "Indexed_Products",
|
|
745
|
+
returnTypeName: "IndexedProduct",
|
|
746
|
+
stateSchema: indexedStateSchemaWithAnnotations->S.castToUnknown,
|
|
747
|
+
authorization: None,
|
|
748
|
+
includeIdParam: true,
|
|
749
|
+
connectionSpec: true,
|
|
750
|
+
},
|
|
751
|
+
],
|
|
752
|
+
)
|
|
753
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
754
|
+
let sdl = inspection.sdlPreview
|
|
755
|
+
// Per-field eq inputs derived from @id and @index
|
|
756
|
+
expect(sdl->String.includes("input IndexedProductFilter"))->toBe(true)
|
|
757
|
+
expect(sdl->String.includes("productIdEq: ID"))->toBe(true)
|
|
758
|
+
// ownerId is detected as an entity reference → ID (matches SchemaType.fromSury behaviour)
|
|
759
|
+
expect(sdl->String.includes("ownerIdEq: ID"))->toBe(true)
|
|
760
|
+
// OrderField enum lists the same fields
|
|
761
|
+
expect(sdl->String.includes("enum IndexedProductOrderField"))->toBe(true)
|
|
762
|
+
expect(sdl->String.includes("input IndexedProductOrderBy"))->toBe(true)
|
|
763
|
+
expect(sdl->String.includes("direction: SortOrder!"))->toBe(true)
|
|
764
|
+
// Connection field arg list includes orderBy
|
|
765
|
+
expect(sdl->String.includes("orderBy: IndexedProductOrderBy"))->toBe(true)
|
|
766
|
+
},
|
|
767
|
+
)
|
|
690
768
|
|
|
691
769
|
testPromise(
|
|
692
770
|
"read model with @scan / @scanSort folds opt-in fields into Filter / OrderBy",
|
|
@@ -763,94 +841,109 @@ describe("GraphQL_SchemaInspector", () => {
|
|
|
763
841
|
expect(warnings)->toEqual([])
|
|
764
842
|
},
|
|
765
843
|
)
|
|
766
|
-
testPromise(
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
844
|
+
testPromise(
|
|
845
|
+
"read model with @subId emits range filters + OrderBy on the sort key",
|
|
846
|
+
async () => {
|
|
847
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
848
|
+
~mutationEntries=[],
|
|
849
|
+
~queryEntries=[
|
|
850
|
+
{
|
|
851
|
+
singleFieldName: "Ordered_Item",
|
|
852
|
+
listFieldName: "Ordered_Items",
|
|
853
|
+
returnTypeName: "OrderedItem",
|
|
854
|
+
stateSchema: orderedStateSchemaWithAnnotations->S.castToUnknown,
|
|
855
|
+
authorization: None,
|
|
856
|
+
includeIdParam: true,
|
|
857
|
+
connectionSpec: true,
|
|
858
|
+
},
|
|
859
|
+
],
|
|
860
|
+
)
|
|
861
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
862
|
+
let sdl = inspection.sdlPreview
|
|
863
|
+
expect(sdl->String.includes("createdAtEq: String"))->toBe(true)
|
|
864
|
+
expect(sdl->String.includes("createdAtFrom: String"))->toBe(true)
|
|
865
|
+
expect(sdl->String.includes("createdAtTo: String"))->toBe(true)
|
|
866
|
+
expect(sdl->String.includes("enum OrderedItemOrderField"))->toBe(true)
|
|
867
|
+
expect(sdl->String.includes("orderBy: OrderedItemOrderBy"))->toBe(true)
|
|
868
|
+
},
|
|
869
|
+
)
|
|
789
870
|
})
|
|
790
871
|
|
|
791
872
|
describe("GraphQL_Server diagnostics", () => {
|
|
792
|
-
testPromise(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
873
|
+
testPromise(
|
|
874
|
+
"diagnostics detects resolver without SDL field",
|
|
875
|
+
async () => {
|
|
876
|
+
GraphQL_Server.reset()
|
|
877
|
+
let resolvers = Dict.make()
|
|
878
|
+
resolvers->Dict.set(
|
|
879
|
+
"orphanResolver",
|
|
880
|
+
async (_root, _args, _ctx) => JSON.Encode.string("ok"),
|
|
881
|
+
)
|
|
882
|
+
GraphQL_Server.registerMutations(~sdlFields=[], ~resolvers)
|
|
883
|
+
let d = GraphQL_Server.diagnostics()
|
|
884
|
+
expect(d.resolverMutationCount)->toBe(1)
|
|
885
|
+
expect(d.sdlMutationCount)->toBe(0)
|
|
886
|
+
expect(d.mismatches->Array.length)->toBe(1)
|
|
887
|
+
expect(
|
|
888
|
+
d.mismatches->Array.getUnsafe(0)->String.includes("resolver but no SDL field"),
|
|
889
|
+
)->toBe(true)
|
|
890
|
+
GraphQL_Server.reset()
|
|
891
|
+
},
|
|
892
|
+
)
|
|
806
893
|
|
|
807
|
-
testPromise(
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
(d.mismatches->Array.
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
894
|
+
testPromise(
|
|
895
|
+
"diagnostics detects SDL field without resolver",
|
|
896
|
+
async () => {
|
|
897
|
+
GraphQL_Server.reset()
|
|
898
|
+
GraphQL_Server.registerQueries(
|
|
899
|
+
~sdlFields=[" orphanField(id: ID!): String"],
|
|
900
|
+
~resolvers=Dict.make(),
|
|
901
|
+
)
|
|
902
|
+
let d = GraphQL_Server.diagnostics()
|
|
903
|
+
expect(d.sdlQueryCount)->toBe(1)
|
|
904
|
+
expect(d.resolverQueryCount)->toBe(0)
|
|
905
|
+
expect(d.mismatches->Array.length)->toBe(1)
|
|
906
|
+
expect(
|
|
907
|
+
d.mismatches->Array.getUnsafe(0)->String.includes("SDL field but no resolver"),
|
|
908
|
+
)->toBe(true)
|
|
909
|
+
GraphQL_Server.reset()
|
|
910
|
+
},
|
|
911
|
+
)
|
|
822
912
|
|
|
823
|
-
testPromise(
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
~resolvers
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
913
|
+
testPromise(
|
|
914
|
+
"diagnostics reports no mismatches when fields and resolvers match",
|
|
915
|
+
async () => {
|
|
916
|
+
GraphQL_Server.reset()
|
|
917
|
+
let resolvers = Dict.make()
|
|
918
|
+
resolvers->Dict.set("myQuery", async (_root, _args, _ctx) => JSON.Encode.string("ok"))
|
|
919
|
+
GraphQL_Server.registerQueries(~sdlFields=[" myQuery(id: ID!): String"], ~resolvers)
|
|
920
|
+
let d = GraphQL_Server.diagnostics()
|
|
921
|
+
expect(d.mismatches->Array.length)->toBe(0)
|
|
922
|
+
expect(d.sdlQueryCount)->toBe(1)
|
|
923
|
+
expect(d.resolverQueryCount)->toBe(1)
|
|
924
|
+
GraphQL_Server.reset()
|
|
925
|
+
},
|
|
926
|
+
)
|
|
837
927
|
|
|
838
|
-
testPromise(
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
928
|
+
testPromise(
|
|
929
|
+
"getFullSdl returns None before start",
|
|
930
|
+
async () => {
|
|
931
|
+
GraphQL_Server.reset()
|
|
932
|
+
expect(GraphQL_Server.getFullSdl()->Option.isNone)->toBe(true)
|
|
933
|
+
},
|
|
934
|
+
)
|
|
842
935
|
|
|
843
|
-
testPromise(
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
~resolvers=Dict.make()
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
936
|
+
testPromise(
|
|
937
|
+
"getRegisteredSdl returns Query/Mutation SDL",
|
|
938
|
+
async () => {
|
|
939
|
+
GraphQL_Server.reset()
|
|
940
|
+
GraphQL_Server.registerQueries(~sdlFields=[" hello: String"], ~resolvers=Dict.make())
|
|
941
|
+
let sdl = GraphQL_Server.getRegisteredSdl()
|
|
942
|
+
expect(sdl->String.includes("hello: String"))->toBe(true)
|
|
943
|
+
expect(sdl->String.includes("type Query"))->toBe(true)
|
|
944
|
+
GraphQL_Server.reset()
|
|
945
|
+
},
|
|
946
|
+
)
|
|
854
947
|
})
|
|
855
948
|
})
|
|
856
949
|
|
|
@@ -877,19 +970,22 @@ describe("DCB StateChangeSlice — one mutation per command constructor", () =>
|
|
|
877
970
|
expect(fieldNames)->toEqual(["Catalog_AddCategory"])
|
|
878
971
|
})
|
|
879
972
|
|
|
880
|
-
testPromise(
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
973
|
+
testPromise(
|
|
974
|
+
"multi-command slice emits one `${plugin}_${command}` field per constructor",
|
|
975
|
+
async () => {
|
|
976
|
+
// The command constructor already names the operation; callers/plans expect
|
|
977
|
+
// `${plugin}_${command}` (e.g. Platform_SyncComponent / Platform_RemoveComponent),
|
|
978
|
+
// NOT the aggregate-style `${plugin}_${slice}_${command}` doubling — which both
|
|
979
|
+
// renamed the primary command and overflowed AppSync's subscription cap.
|
|
980
|
+
let fieldNames =
|
|
981
|
+
ReventlessCore.Api_Naming.sliceMutationFields(
|
|
982
|
+
~plugin="Ordering",
|
|
983
|
+
~slice="SyncCatalogProduct",
|
|
984
|
+
~commandSchema=dcbMultiCommandSchema->S.castToUnknown,
|
|
985
|
+
)->Array.map(((f, _)) => f)
|
|
986
|
+
expect(fieldNames)->toEqual(["Ordering_SyncNewProduct", "Ordering_ChangeSyncedPrice"])
|
|
987
|
+
},
|
|
988
|
+
)
|
|
893
989
|
|
|
894
990
|
testPromise(
|
|
895
991
|
"multi-command slice: both mutations carry their own args, plus both subscriptions (each ≤ 50 chars)",
|
|
@@ -914,9 +1010,7 @@ describe("DCB StateChangeSlice — one mutation per command constructor", () =>
|
|
|
914
1010
|
),
|
|
915
1011
|
)->toBe(true)
|
|
916
1012
|
expect(
|
|
917
|
-
sdl->String.includes(
|
|
918
|
-
"Ordering_ChangeSyncedPrice(id: ID!, productId: ID!, price: Float!)",
|
|
919
|
-
),
|
|
1013
|
+
sdl->String.includes("Ordering_ChangeSyncedPrice(id: ID!, productId: ID!, price: Float!)"),
|
|
920
1014
|
)->toBe(true)
|
|
921
1015
|
|
|
922
1016
|
// Subscriptions fan out one-per-field over the same entry.
|
|
@@ -929,13 +1023,14 @@ describe("DCB StateChangeSlice — one mutation per command constructor", () =>
|
|
|
929
1023
|
expect(subSdl->String.includes("onOrdering_ChangeSyncedPrice"))->toBe(true)
|
|
930
1024
|
|
|
931
1025
|
// Every emitted subscription field name stays within AppSync's 50-char cap.
|
|
932
|
-
subs.subscriptionFields->Array.forEach(
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
sub->String.trim->String.split("(")->Array.getUnsafe(0)
|
|
937
|
-
|
|
938
|
-
|
|
1026
|
+
subs.subscriptionFields->Array.forEach(
|
|
1027
|
+
sub => {
|
|
1028
|
+
// `sub` is the full SDL line ` on<Field>(id: ID): CommandResult\n ...`;
|
|
1029
|
+
// extract the leading `on<Field>` token to length-check the field name itself.
|
|
1030
|
+
let name = sub->String.trim->String.split("(")->Array.getUnsafe(0)
|
|
1031
|
+
expect(name->String.length <= 50)->toBe(true)
|
|
1032
|
+
},
|
|
1033
|
+
)
|
|
939
1034
|
},
|
|
940
1035
|
)
|
|
941
1036
|
|
|
@@ -962,22 +1057,25 @@ describe("DCB StateChangeSlice — one mutation per command constructor", () =>
|
|
|
962
1057
|
})
|
|
963
1058
|
|
|
964
1059
|
describe("Plugin admin fragment — kind enum + kindEq filter", () => {
|
|
965
|
-
testPromise(
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1060
|
+
testPromise(
|
|
1061
|
+
"Platform_Plugin exposes a PluginKind enum and Platform_Plugins gains kindEq",
|
|
1062
|
+
async () => {
|
|
1063
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
1064
|
+
~mutationEntries=[],
|
|
1065
|
+
~queryEntries=ReventlessCore.PluginBaseFragment.queryEntries,
|
|
1066
|
+
)
|
|
1067
|
+
let sdl = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment).sdlPreview
|
|
1068
|
+
// kind is a real enum on the Plugin type (auto-derived from the payload-less variant)
|
|
1069
|
+
expect(sdl->String.includes("enum Platform_PluginKind"))->toBe(true)
|
|
1070
|
+
expect(sdl->String.includes("PlatformInfrastructure"))->toBe(true)
|
|
1071
|
+
// `kind` is `option<pluginKind>` — nullable on purpose so a kind-less legacy row
|
|
1072
|
+
// can't collapse the whole Platform_Plugins query. It must render without a `!`.
|
|
1073
|
+
expect(sdl->String.includes("kind: Platform_PluginKind"))->toBe(true)
|
|
1074
|
+
expect(sdl->String.includes("kind: Platform_PluginKind!"))->toBe(false)
|
|
1075
|
+
// @scan folds a server-side equality filter for the panel split
|
|
1076
|
+
expect(sdl->String.includes("kindEq: String"))->toBe(true)
|
|
1077
|
+
},
|
|
1078
|
+
)
|
|
981
1079
|
})
|
|
982
1080
|
|
|
983
1081
|
describe("optional enum fields preserve GraphQL nullability", () => {
|