@apollo/gateway 2.0.0-alpha.2 → 2.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/config.d.ts +2 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js.map +1 -1
  4. package/dist/datasources/RemoteGraphQLDataSource.d.ts.map +1 -1
  5. package/dist/datasources/RemoteGraphQLDataSource.js +4 -1
  6. package/dist/datasources/RemoteGraphQLDataSource.js.map +1 -1
  7. package/dist/executeQueryPlan.d.ts.map +1 -1
  8. package/dist/executeQueryPlan.js +1 -1
  9. package/dist/executeQueryPlan.js.map +1 -1
  10. package/dist/index.d.ts +3 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +18 -6
  13. package/dist/index.js.map +1 -1
  14. package/dist/loadSupergraphSdlFromStorage.d.ts +13 -5
  15. package/dist/loadSupergraphSdlFromStorage.d.ts.map +1 -1
  16. package/dist/loadSupergraphSdlFromStorage.js +34 -7
  17. package/dist/loadSupergraphSdlFromStorage.js.map +1 -1
  18. package/dist/outOfBandReporter.d.ts +10 -12
  19. package/dist/outOfBandReporter.d.ts.map +1 -1
  20. package/dist/outOfBandReporter.js +70 -73
  21. package/dist/outOfBandReporter.js.map +1 -1
  22. package/package.json +4 -4
  23. package/src/__mocks__/make-fetch-happen-fetcher.ts +3 -1
  24. package/src/__tests__/executeQueryPlan.test.ts +598 -0
  25. package/src/__tests__/gateway/buildService.test.ts +1 -1
  26. package/src/__tests__/gateway/composedSdl.test.ts +1 -1
  27. package/src/__tests__/gateway/executor.test.ts +1 -1
  28. package/src/__tests__/gateway/reporting.test.ts +8 -5
  29. package/src/__tests__/integration/configuration.test.ts +44 -4
  30. package/src/__tests__/integration/networkRequests.test.ts +21 -19
  31. package/src/__tests__/integration/nockMocks.ts +12 -6
  32. package/src/__tests__/loadSupergraphSdlFromStorage.test.ts +101 -452
  33. package/src/__tests__/nockAssertions.ts +20 -0
  34. package/src/config.ts +3 -1
  35. package/src/datasources/RemoteGraphQLDataSource.ts +8 -2
  36. package/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +4 -4
  37. package/src/executeQueryPlan.ts +11 -1
  38. package/src/index.ts +26 -12
  39. package/src/loadSupergraphSdlFromStorage.ts +54 -8
  40. package/src/outOfBandReporter.ts +87 -89
@@ -2138,4 +2138,602 @@ describe('executeQueryPlan', () => {
2138
2138
  }
2139
2139
  `);
2140
2140
  });
2141
+
2142
+ describe('@requires', () => {
2143
+ test('handles null in required field correctly (with nullable fields)', async () => {
2144
+ const s1_data = [
2145
+ { id: 0, f1: "foo" },
2146
+ { id: 1, f1: null },
2147
+ { id: 2, f1: "bar" },
2148
+ ];
2149
+
2150
+ const s1 = {
2151
+ name: 'S1',
2152
+ typeDefs: gql`
2153
+ type T1 @key(fields: "id") {
2154
+ id: Int!
2155
+ f1: String
2156
+ }
2157
+ `,
2158
+ resolvers: {
2159
+ T1: {
2160
+ __resolveReference(ref: {id: number}) {
2161
+ return s1_data[ref.id];
2162
+ },
2163
+ },
2164
+ }
2165
+ }
2166
+
2167
+ const s2 = {
2168
+ name: 'S2',
2169
+ typeDefs: gql`
2170
+ type Query {
2171
+ getT1s: [T1]
2172
+ }
2173
+
2174
+ type T1 @key(fields: "id") {
2175
+ id: Int!
2176
+ f1: String @external
2177
+ f2: T2 @requires(fields: "f1")
2178
+ }
2179
+
2180
+ type T2 {
2181
+ a: String
2182
+ }
2183
+ `,
2184
+ resolvers: {
2185
+ Query: {
2186
+ getT1s() {
2187
+ return [{id: 0}, {id: 1}, {id: 2}];
2188
+ },
2189
+ },
2190
+ T1: {
2191
+ __resolveReference(ref: { id: number }) {
2192
+ // the ref has already the id and f1 is a require is triggered, and we resolve f2 below
2193
+ return ref;
2194
+ },
2195
+ f2(o: { f1: string }) {
2196
+ return o.f1 === null ? null : { a: `t1:${o.f1}` };
2197
+ }
2198
+ }
2199
+ }
2200
+ }
2201
+
2202
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
2203
+
2204
+ const operation = parseOp(`
2205
+ query {
2206
+ getT1s {
2207
+ id
2208
+ f1
2209
+ f2 {
2210
+ a
2211
+ }
2212
+ }
2213
+ }
2214
+ `, schema);
2215
+ const queryPlan = buildPlan(operation, queryPlanner);
2216
+ expect(queryPlan).toMatchInlineSnapshot(`
2217
+ QueryPlan {
2218
+ Sequence {
2219
+ Fetch(service: "S2") {
2220
+ {
2221
+ getT1s {
2222
+ __typename
2223
+ id
2224
+ }
2225
+ }
2226
+ },
2227
+ Flatten(path: "getT1s.@") {
2228
+ Fetch(service: "S1") {
2229
+ {
2230
+ ... on T1 {
2231
+ __typename
2232
+ id
2233
+ }
2234
+ } =>
2235
+ {
2236
+ ... on T1 {
2237
+ f1
2238
+ }
2239
+ }
2240
+ },
2241
+ },
2242
+ Flatten(path: "getT1s.@") {
2243
+ Fetch(service: "S2") {
2244
+ {
2245
+ ... on T1 {
2246
+ __typename
2247
+ f1
2248
+ id
2249
+ }
2250
+ } =>
2251
+ {
2252
+ ... on T1 {
2253
+ f2 {
2254
+ a
2255
+ }
2256
+ }
2257
+ }
2258
+ },
2259
+ },
2260
+ },
2261
+ }
2262
+ `);
2263
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2264
+ expect(response.data).toMatchInlineSnapshot(`
2265
+ Object {
2266
+ "getT1s": Array [
2267
+ Object {
2268
+ "f1": "foo",
2269
+ "f2": Object {
2270
+ "a": "t1:foo",
2271
+ },
2272
+ "id": 0,
2273
+ },
2274
+ Object {
2275
+ "f1": null,
2276
+ "f2": null,
2277
+ "id": 1,
2278
+ },
2279
+ Object {
2280
+ "f1": "bar",
2281
+ "f2": Object {
2282
+ "a": "t1:bar",
2283
+ },
2284
+ "id": 2,
2285
+ },
2286
+ ],
2287
+ }
2288
+ `);
2289
+ expect(response.errors).toBeUndefined();
2290
+ });
2291
+
2292
+ test('handles null in required field correctly (with @require field non-nullable)', async () => {
2293
+ const s1_data = [
2294
+ { id: 0, f1: "foo" },
2295
+ { id: 1, f1: null },
2296
+ { id: 2, f1: "bar" },
2297
+ ];
2298
+
2299
+ const s1 = {
2300
+ name: 'S1',
2301
+ typeDefs: gql`
2302
+ type T1 @key(fields: "id") {
2303
+ id: Int!
2304
+ f1: String
2305
+ }
2306
+ `,
2307
+ resolvers: {
2308
+ T1: {
2309
+ __resolveReference(ref: { id: number }) {
2310
+ return s1_data[ref.id];
2311
+ },
2312
+ },
2313
+ }
2314
+ }
2315
+
2316
+ const s2 = {
2317
+ name: 'S2',
2318
+ typeDefs: gql`
2319
+ type Query {
2320
+ getT1s: [T1]
2321
+ }
2322
+
2323
+ type T1 @key(fields: "id") {
2324
+ id: Int!
2325
+ f1: String @external
2326
+ f2: T2! @requires(fields: "f1")
2327
+ }
2328
+
2329
+ type T2 {
2330
+ a: String
2331
+ }
2332
+ `,
2333
+ resolvers: {
2334
+ Query: {
2335
+ getT1s() {
2336
+ return [{id: 0}, {id: 1}, {id: 2}];
2337
+ },
2338
+ },
2339
+ T1: {
2340
+ __resolveReference(ref: { id: number }) {
2341
+ // the ref has already the id and f1 is a require is triggered, and we resolve f2 below
2342
+ return ref;
2343
+ },
2344
+ f2(o: { f1: string }) {
2345
+ return o.f1 === null ? null : { a: `t1:${o.f1}` };
2346
+ }
2347
+ }
2348
+ }
2349
+ }
2350
+
2351
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
2352
+
2353
+ const operation = parseOp(`
2354
+ query {
2355
+ getT1s {
2356
+ id
2357
+ f1
2358
+ f2 {
2359
+ a
2360
+ }
2361
+ }
2362
+ }
2363
+ `, schema);
2364
+ const queryPlan = buildPlan(operation, queryPlanner);
2365
+ expect(queryPlan).toMatchInlineSnapshot(`
2366
+ QueryPlan {
2367
+ Sequence {
2368
+ Fetch(service: "S2") {
2369
+ {
2370
+ getT1s {
2371
+ __typename
2372
+ id
2373
+ }
2374
+ }
2375
+ },
2376
+ Flatten(path: "getT1s.@") {
2377
+ Fetch(service: "S1") {
2378
+ {
2379
+ ... on T1 {
2380
+ __typename
2381
+ id
2382
+ }
2383
+ } =>
2384
+ {
2385
+ ... on T1 {
2386
+ f1
2387
+ }
2388
+ }
2389
+ },
2390
+ },
2391
+ Flatten(path: "getT1s.@") {
2392
+ Fetch(service: "S2") {
2393
+ {
2394
+ ... on T1 {
2395
+ __typename
2396
+ f1
2397
+ id
2398
+ }
2399
+ } =>
2400
+ {
2401
+ ... on T1 {
2402
+ f2 {
2403
+ a
2404
+ }
2405
+ }
2406
+ }
2407
+ },
2408
+ },
2409
+ },
2410
+ }
2411
+ `);
2412
+
2413
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2414
+ // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response.
2415
+ expect(response.data).toMatchInlineSnapshot(`
2416
+ Object {
2417
+ "getT1s": Array [
2418
+ Object {
2419
+ "f1": "foo",
2420
+ "f2": Object {
2421
+ "a": "t1:foo",
2422
+ },
2423
+ "id": 0,
2424
+ },
2425
+ null,
2426
+ Object {
2427
+ "f1": "bar",
2428
+ "f2": Object {
2429
+ "a": "t1:bar",
2430
+ },
2431
+ "id": 2,
2432
+ },
2433
+ ],
2434
+ }
2435
+ `);
2436
+
2437
+ // We returning `null` for f2 which isn't nullable, so it bubbled up and we should have an error
2438
+ expect(response.errors?.map((e) => e.message)).toStrictEqual(['Cannot return null for non-nullable field T1.f2.']);
2439
+ });
2440
+
2441
+ test('handles null in required field correctly (with non-nullable required field)', async () => {
2442
+ const s1 = {
2443
+ name: 'S1',
2444
+ typeDefs: gql`
2445
+ type T1 @key(fields: "id") {
2446
+ id: Int!
2447
+ f1: String!
2448
+ }
2449
+ `,
2450
+ resolvers: {
2451
+ T1: {
2452
+ __resolveReference(ref: { id: number}) {
2453
+ return s1_data[ref.id];
2454
+ },
2455
+ },
2456
+ }
2457
+ }
2458
+
2459
+ const s2 = {
2460
+ name: 'S2',
2461
+ typeDefs: gql`
2462
+ type Query {
2463
+ getT1s: [T1]
2464
+ }
2465
+
2466
+ type T1 @key(fields: "id") {
2467
+ id: Int!
2468
+ f1: String! @external
2469
+ f2: T2 @requires(fields: "f1")
2470
+ }
2471
+
2472
+ type T2 {
2473
+ a: String
2474
+ }
2475
+ `,
2476
+ resolvers: {
2477
+ Query: {
2478
+ getT1s() {
2479
+ return [{id: 0}, {id: 1}, {id: 2}];
2480
+ },
2481
+ },
2482
+ T1: {
2483
+ __resolveReference(ref: { id: number }) {
2484
+ // the ref has already the id and f1 is a require is triggered, and we resolve f2 below
2485
+ return ref;
2486
+ },
2487
+ f2(o: { f1: string }) {
2488
+ return o.f1 === null ? null : { a: `t1:${o.f1}` };
2489
+ }
2490
+ }
2491
+ }
2492
+ }
2493
+
2494
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
2495
+
2496
+ const s1_data = [
2497
+ { id: 0, f1: "foo" },
2498
+ { id: 1, f1: null },
2499
+ { id: 2, f1: "bar" },
2500
+ ];
2501
+
2502
+ const operation = parseOp(`
2503
+ query {
2504
+ getT1s {
2505
+ id
2506
+ f1
2507
+ f2 {
2508
+ a
2509
+ }
2510
+ }
2511
+ }
2512
+ `, schema);
2513
+ const queryPlan = buildPlan(operation, queryPlanner);
2514
+ expect(queryPlan).toMatchInlineSnapshot(`
2515
+ QueryPlan {
2516
+ Sequence {
2517
+ Fetch(service: "S2") {
2518
+ {
2519
+ getT1s {
2520
+ __typename
2521
+ id
2522
+ }
2523
+ }
2524
+ },
2525
+ Flatten(path: "getT1s.@") {
2526
+ Fetch(service: "S1") {
2527
+ {
2528
+ ... on T1 {
2529
+ __typename
2530
+ id
2531
+ }
2532
+ } =>
2533
+ {
2534
+ ... on T1 {
2535
+ f1
2536
+ }
2537
+ }
2538
+ },
2539
+ },
2540
+ Flatten(path: "getT1s.@") {
2541
+ Fetch(service: "S2") {
2542
+ {
2543
+ ... on T1 {
2544
+ __typename
2545
+ f1
2546
+ id
2547
+ }
2548
+ } =>
2549
+ {
2550
+ ... on T1 {
2551
+ f2 {
2552
+ a
2553
+ }
2554
+ }
2555
+ }
2556
+ },
2557
+ },
2558
+ },
2559
+ }
2560
+ `);
2561
+
2562
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2563
+ // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response.
2564
+ expect(response.data).toMatchInlineSnapshot(`
2565
+ Object {
2566
+ "getT1s": Array [
2567
+ Object {
2568
+ "f1": "foo",
2569
+ "f2": Object {
2570
+ "a": "t1:foo",
2571
+ },
2572
+ "id": 0,
2573
+ },
2574
+ null,
2575
+ Object {
2576
+ "f1": "bar",
2577
+ "f2": Object {
2578
+ "a": "t1:bar",
2579
+ },
2580
+ "id": 2,
2581
+ },
2582
+ ],
2583
+ }
2584
+ `);
2585
+ expect(response.errors?.map((e) => e.message)).toStrictEqual(['Cannot return null for non-nullable field T1.f1.']);
2586
+ });
2587
+
2588
+ test('handles errors in required field correctly (with nullable fields)', async () => {
2589
+ const s1 = {
2590
+ name: 'S1',
2591
+ typeDefs: gql`
2592
+ type T1 @key(fields: "id") {
2593
+ id: Int!
2594
+ f1: String
2595
+ }
2596
+ `,
2597
+ resolvers: {
2598
+ T1: {
2599
+ __resolveReference(ref: { id: number }) {
2600
+ return ref;
2601
+ },
2602
+ f1(o: { id: number }) {
2603
+ switch (o.id) {
2604
+ case 0: return "foo";
2605
+ case 1: return [ "invalid" ]; // This will effectively throw
2606
+ case 2: return "bar";
2607
+ default: throw new Error('Not handled');
2608
+ }
2609
+ }
2610
+ },
2611
+ }
2612
+ }
2613
+
2614
+ const s2 = {
2615
+ name: 'S2',
2616
+ typeDefs: gql`
2617
+ type Query {
2618
+ getT1s: [T1]
2619
+ }
2620
+
2621
+ type T1 @key(fields: "id") {
2622
+ id: Int!
2623
+ f1: String @external
2624
+ f2: T2 @requires(fields: "f1")
2625
+ }
2626
+
2627
+ type T2 {
2628
+ a: String
2629
+ }
2630
+ `,
2631
+ resolvers: {
2632
+ Query: {
2633
+ getT1s() {
2634
+ return [{id: 0}, {id: 1}, {id: 2}];
2635
+ },
2636
+ },
2637
+ T1: {
2638
+ __resolveReference(ref: { id: number }) {
2639
+ // the ref has already the id and f1 is a require is triggered, and we resolve f2 below
2640
+ return ref;
2641
+ },
2642
+ f2(o: { f1: string }) {
2643
+ return o.f1 === null ? null : { a: `t1:${o.f1}` };
2644
+ }
2645
+ }
2646
+ }
2647
+ }
2648
+
2649
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
2650
+
2651
+ const operation = parseOp(`
2652
+ query {
2653
+ getT1s {
2654
+ id
2655
+ f1
2656
+ f2 {
2657
+ a
2658
+ }
2659
+ }
2660
+ }
2661
+ `, schema);
2662
+ const queryPlan = buildPlan(operation, queryPlanner);
2663
+ expect(queryPlan).toMatchInlineSnapshot(`
2664
+ QueryPlan {
2665
+ Sequence {
2666
+ Fetch(service: "S2") {
2667
+ {
2668
+ getT1s {
2669
+ __typename
2670
+ id
2671
+ }
2672
+ }
2673
+ },
2674
+ Flatten(path: "getT1s.@") {
2675
+ Fetch(service: "S1") {
2676
+ {
2677
+ ... on T1 {
2678
+ __typename
2679
+ id
2680
+ }
2681
+ } =>
2682
+ {
2683
+ ... on T1 {
2684
+ f1
2685
+ }
2686
+ }
2687
+ },
2688
+ },
2689
+ Flatten(path: "getT1s.@") {
2690
+ Fetch(service: "S2") {
2691
+ {
2692
+ ... on T1 {
2693
+ __typename
2694
+ f1
2695
+ id
2696
+ }
2697
+ } =>
2698
+ {
2699
+ ... on T1 {
2700
+ f2 {
2701
+ a
2702
+ }
2703
+ }
2704
+ }
2705
+ },
2706
+ },
2707
+ },
2708
+ }
2709
+ `);
2710
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2711
+ expect(response.data).toMatchInlineSnapshot(`
2712
+ Object {
2713
+ "getT1s": Array [
2714
+ Object {
2715
+ "f1": "foo",
2716
+ "f2": Object {
2717
+ "a": "t1:foo",
2718
+ },
2719
+ "id": 0,
2720
+ },
2721
+ Object {
2722
+ "f1": null,
2723
+ "f2": null,
2724
+ "id": 1,
2725
+ },
2726
+ Object {
2727
+ "f1": "bar",
2728
+ "f2": Object {
2729
+ "a": "t1:bar",
2730
+ },
2731
+ "id": 2,
2732
+ },
2733
+ ],
2734
+ }
2735
+ `);
2736
+ expect(response.errors?.map((e) => e.message)).toStrictEqual(['String cannot represent value: ["invalid"]']);
2737
+ });
2738
+ });
2141
2739
  });
@@ -1,4 +1,4 @@
1
- import { fetch } from '../../__mocks__/apollo-server-env';
1
+ import { fetch } from '../../__mocks__/make-fetch-happen-fetcher';
2
2
  import { ApolloServerBase as ApolloServer } from 'apollo-server-core';
3
3
 
4
4
  import { RemoteGraphQLDataSource } from '../../datasources/RemoteGraphQLDataSource';
@@ -1,6 +1,6 @@
1
+ import { fetch } from '../../__mocks__/make-fetch-happen-fetcher';
1
2
  import { ApolloGateway } from '@apollo/gateway';
2
3
  import { ApolloServer } from 'apollo-server';
3
- import { fetch } from '../../__mocks__/apollo-server-env';
4
4
  import { getTestingSupergraphSdl } from '../execution-utils';
5
5
 
6
6
  async function getSupergraphSdlGatewayServer() {
@@ -1,8 +1,8 @@
1
+ import { fetch } from '../../__mocks__/make-fetch-happen-fetcher';
1
2
  import gql from 'graphql-tag';
2
3
  import { ApolloGateway } from '../../';
3
4
  import { fixtures } from 'apollo-federation-integration-testsuite';
4
5
  import { Logger } from 'apollo-server-types';
5
- import { fetch } from '../../__mocks__/apollo-server-env';
6
6
 
7
7
  let logger: {
8
8
  warn: jest.MockedFunction<Logger['warn']>,
@@ -5,13 +5,15 @@ import gql from 'graphql-tag';
5
5
  import { buildSubgraphSchema } from '@apollo/subgraph';
6
6
  import { ApolloServer } from 'apollo-server';
7
7
  import { ApolloServerPluginUsageReporting } from 'apollo-server-core';
8
- import { execute, toPromise } from 'apollo-link';
9
- import { createHttpLink } from 'apollo-link-http';
8
+ import { execute } from '@apollo/client/link/core';
9
+ import { toPromise } from '@apollo/client/link/utils';
10
+ import { createHttpLink } from '@apollo/client/link/http';
10
11
  import fetch from 'node-fetch';
11
12
  import { ApolloGateway } from '../..';
12
13
  import { Plugin, Config, Refs } from 'pretty-format';
13
14
  import { Report, Trace } from 'apollo-reporting-protobuf';
14
15
  import { fixtures } from 'apollo-federation-integration-testsuite';
16
+ import { nockAfterEach, nockBeforeEach } from '../nockAssertions';
15
17
 
16
18
  // Normalize specific fields that change often (eg timestamps) to static values,
17
19
  // to make snapshot testing viable. (If these helpers are more generally
@@ -89,7 +91,6 @@ describe('reporting', () => {
89
91
  let gatewayServer: ApolloServer;
90
92
  let gatewayUrl: string;
91
93
  let reportPromise: Promise<any>;
92
- let nockScope: nock.Scope;
93
94
 
94
95
  beforeEach(async () => {
95
96
  let reportResolver: (report: any) => void;
@@ -97,7 +98,8 @@ describe('reporting', () => {
97
98
  reportResolver = resolve;
98
99
  });
99
100
 
100
- nockScope = nock('https://usage-reporting.api.apollographql.com')
101
+ nockBeforeEach();
102
+ nock('https://usage-reporting.api.apollographql.com')
101
103
  .post('/api/ingress/traces')
102
104
  .reply(200, (_: any, requestBody: string) => {
103
105
  reportResolver(requestBody);
@@ -137,7 +139,8 @@ describe('reporting', () => {
137
139
  if (gatewayServer) {
138
140
  await gatewayServer.stop();
139
141
  }
140
- nockScope.done();
142
+
143
+ nockAfterEach();
141
144
  });
142
145
 
143
146
  it(`queries three services`, async () => {