Skip to content

Commit 11eac72

Browse files
committed
feat(gateway): allow pinning the gateway to a specific version of the schema
1 parent a439e91 commit 11eac72

File tree

7 files changed

+776
-23
lines changed

7 files changed

+776
-23
lines changed

integration-tests/tests/api/artifacts-cdn.spec.ts

Lines changed: 335 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import {
1010
PutObjectCommand,
1111
S3Client,
1212
} from '@aws-sdk/client-s3';
13-
import { createSupergraphManager } from '@graphql-hive/apollo';
13+
import {
14+
createSchemaFetcher,
15+
createServicesFetcher,
16+
createSupergraphManager,
17+
} from '@graphql-hive/apollo';
18+
import { createSupergraphSDLFetcher } from '@graphql-hive/core';
1419
import { graphql } from '../../testkit/gql';
1520
import { execute } from '../../testkit/graphql';
1621
import { initSeed } from '../../testkit/seed';
@@ -1230,6 +1235,335 @@ function runArtifactsCDNTests(
12301235
expect(contractSupergraphResponse.headers.get('x-hive-schema-version-id')).toBe(versionId);
12311236
},
12321237
);
1238+
1239+
test.concurrent(
1240+
'createSupergraphSDLFetcher extracts schemaVersionId from response',
1241+
async ({ expect }) => {
1242+
const endpointBaseUrl = await getBaseEndpoint();
1243+
const { createOrg } = await initSeed().createOwner();
1244+
const { createProject } = await createOrg();
1245+
const { createTargetAccessToken, createCdnAccess, target } = await createProject(
1246+
ProjectType.Federation,
1247+
);
1248+
const writeToken = await createTargetAccessToken({});
1249+
1250+
// Publish Schema
1251+
await writeToken
1252+
.publishSchema({
1253+
author: 'Kamil',
1254+
commit: 'abc123',
1255+
sdl: `type Query { ping: String }`,
1256+
service: 'ping',
1257+
url: 'http://ping.com',
1258+
})
1259+
.then(r => r.expectNoGraphQLErrors());
1260+
1261+
const cdnAccessResult = await createCdnAccess();
1262+
1263+
// Use SDK fetcher without version pinning
1264+
const fetcher = createSupergraphSDLFetcher({
1265+
endpoint: endpointBaseUrl + target.id,
1266+
key: cdnAccessResult.secretAccessToken,
1267+
});
1268+
1269+
const result = await fetcher();
1270+
1271+
// Should extract schemaVersionId from header
1272+
expect(result.schemaVersionId).toBeDefined();
1273+
expect(result.supergraphSdl).toContain('Query');
1274+
expect(result.id).toBeDefined();
1275+
},
1276+
);
1277+
1278+
test.concurrent(
1279+
'createSupergraphSDLFetcher with schemaVersionId fetches pinned version',
1280+
async ({ expect }) => {
1281+
const endpointBaseUrl = await getBaseEndpoint();
1282+
const { createOrg } = await initSeed().createOwner();
1283+
const { createProject } = await createOrg();
1284+
const { createTargetAccessToken, createCdnAccess, target } = await createProject(
1285+
ProjectType.Federation,
1286+
);
1287+
const writeToken = await createTargetAccessToken({});
1288+
1289+
// Publish V1 Schema
1290+
await writeToken
1291+
.publishSchema({
1292+
author: 'Kamil',
1293+
commit: 'v1',
1294+
sdl: `type Query { ping: String }`,
1295+
service: 'ping',
1296+
url: 'http://ping.com',
1297+
})
1298+
.then(r => r.expectNoGraphQLErrors());
1299+
1300+
// Get V1 version ID
1301+
const v1Version = await writeToken.fetchLatestValidSchema();
1302+
const v1VersionId = v1Version.latestValidVersion?.id;
1303+
expect(v1VersionId).toBeDefined();
1304+
1305+
const cdnAccessResult = await createCdnAccess();
1306+
1307+
// Fetch V1 and capture content
1308+
const v1Fetcher = createSupergraphSDLFetcher({
1309+
endpoint: endpointBaseUrl + target.id,
1310+
key: cdnAccessResult.secretAccessToken,
1311+
});
1312+
const v1Result = await v1Fetcher();
1313+
expect(v1Result.schemaVersionId).toBe(v1VersionId);
1314+
1315+
// Publish V2 Schema with different content
1316+
await writeToken
1317+
.publishSchema({
1318+
author: 'Kamil',
1319+
commit: 'v2',
1320+
sdl: `type Query { ping: String, pong: String }`,
1321+
service: 'ping',
1322+
url: 'http://ping.com',
1323+
})
1324+
.then(r => r.expectNoGraphQLErrors());
1325+
1326+
// Create a pinned fetcher for V1
1327+
const pinnedFetcher = createSupergraphSDLFetcher({
1328+
endpoint: endpointBaseUrl + target.id,
1329+
key: cdnAccessResult.secretAccessToken,
1330+
schemaVersionId: v1VersionId!,
1331+
});
1332+
1333+
const pinnedResult = await pinnedFetcher();
1334+
1335+
// Should still return V1 content even after V2 was published
1336+
expect(pinnedResult.supergraphSdl).toBe(v1Result.supergraphSdl);
1337+
expect(pinnedResult.supergraphSdl).not.toContain('pong');
1338+
1339+
// Latest fetcher should return V2
1340+
const latestFetcher = createSupergraphSDLFetcher({
1341+
endpoint: endpointBaseUrl + target.id,
1342+
key: cdnAccessResult.secretAccessToken,
1343+
});
1344+
const latestResult = await latestFetcher();
1345+
expect(latestResult.supergraphSdl).toContain('pong');
1346+
expect(latestResult.schemaVersionId).not.toBe(v1VersionId);
1347+
},
1348+
);
1349+
1350+
test.concurrent(
1351+
'createSchemaFetcher extracts schemaVersionId from response',
1352+
async ({ expect }) => {
1353+
const endpointBaseUrl = await getBaseEndpoint();
1354+
const { createOrg } = await initSeed().createOwner();
1355+
const { createProject } = await createOrg();
1356+
const { createTargetAccessToken, createCdnAccess, target } = await createProject(
1357+
ProjectType.Single,
1358+
);
1359+
const writeToken = await createTargetAccessToken({});
1360+
1361+
// Publish Schema
1362+
await writeToken
1363+
.publishSchema({
1364+
author: 'Kamil',
1365+
commit: 'abc123',
1366+
sdl: `type Query { ping: String }`,
1367+
})
1368+
.then(r => r.expectNoGraphQLErrors());
1369+
1370+
const cdnAccessResult = await createCdnAccess();
1371+
1372+
// Use SDK fetcher
1373+
const fetcher = createSchemaFetcher({
1374+
endpoint: endpointBaseUrl + target.id,
1375+
key: cdnAccessResult.secretAccessToken,
1376+
});
1377+
1378+
const result = await fetcher();
1379+
1380+
// Should extract schemaVersionId from header
1381+
expect(result.schemaVersionId).toBeDefined();
1382+
expect(result.sdl).toContain('Query');
1383+
expect(result.id).toBeDefined();
1384+
},
1385+
);
1386+
1387+
test.concurrent(
1388+
'createSchemaFetcher with schemaVersionId fetches pinned version',
1389+
async ({ expect }) => {
1390+
const endpointBaseUrl = await getBaseEndpoint();
1391+
const { createOrg } = await initSeed().createOwner();
1392+
const { createProject } = await createOrg();
1393+
const { createTargetAccessToken, createCdnAccess, target } = await createProject(
1394+
ProjectType.Single,
1395+
);
1396+
const writeToken = await createTargetAccessToken({});
1397+
1398+
// Publish V1 Schema
1399+
await writeToken
1400+
.publishSchema({
1401+
author: 'Kamil',
1402+
commit: 'v1',
1403+
sdl: `type Query { ping: String }`,
1404+
})
1405+
.then(r => r.expectNoGraphQLErrors());
1406+
1407+
// Get V1 version ID
1408+
const v1Version = await writeToken.fetchLatestValidSchema();
1409+
const v1VersionId = v1Version.latestValidVersion?.id;
1410+
expect(v1VersionId).toBeDefined();
1411+
1412+
const cdnAccessResult = await createCdnAccess();
1413+
1414+
// Fetch V1 and capture content
1415+
const v1Fetcher = createSchemaFetcher({
1416+
endpoint: endpointBaseUrl + target.id,
1417+
key: cdnAccessResult.secretAccessToken,
1418+
});
1419+
const v1Result = await v1Fetcher();
1420+
expect(v1Result.schemaVersionId).toBe(v1VersionId);
1421+
1422+
// Publish V2 Schema with different content
1423+
await writeToken
1424+
.publishSchema({
1425+
author: 'Kamil',
1426+
commit: 'v2',
1427+
sdl: `type Query { ping: String, pong: String }`,
1428+
})
1429+
.then(r => r.expectNoGraphQLErrors());
1430+
1431+
// Create a pinned fetcher for V1
1432+
const pinnedFetcher = createSchemaFetcher({
1433+
endpoint: endpointBaseUrl + target.id,
1434+
key: cdnAccessResult.secretAccessToken,
1435+
schemaVersionId: v1VersionId!,
1436+
});
1437+
1438+
const pinnedResult = await pinnedFetcher();
1439+
1440+
// Should still return V1 content even after V2 was published
1441+
expect(pinnedResult.sdl).toBe(v1Result.sdl);
1442+
expect(pinnedResult.sdl).not.toContain('pong');
1443+
1444+
// Latest fetcher should return V2
1445+
const latestFetcher = createSchemaFetcher({
1446+
endpoint: endpointBaseUrl + target.id,
1447+
key: cdnAccessResult.secretAccessToken,
1448+
});
1449+
const latestResult = await latestFetcher();
1450+
expect(latestResult.sdl).toContain('pong');
1451+
expect(latestResult.schemaVersionId).not.toBe(v1VersionId);
1452+
},
1453+
);
1454+
1455+
test.concurrent(
1456+
'createServicesFetcher extracts schemaVersionId into each item',
1457+
async ({ expect }) => {
1458+
const endpointBaseUrl = await getBaseEndpoint();
1459+
const { createOrg } = await initSeed().createOwner();
1460+
const { createProject } = await createOrg();
1461+
const { createTargetAccessToken, createCdnAccess, target } = await createProject(
1462+
ProjectType.Federation,
1463+
);
1464+
const writeToken = await createTargetAccessToken({});
1465+
1466+
// Publish Schema
1467+
await writeToken
1468+
.publishSchema({
1469+
author: 'Kamil',
1470+
commit: 'abc123',
1471+
sdl: `type Query { ping: String }`,
1472+
service: 'ping',
1473+
url: 'http://ping.com',
1474+
})
1475+
.then(r => r.expectNoGraphQLErrors());
1476+
1477+
const cdnAccessResult = await createCdnAccess();
1478+
1479+
// Use SDK fetcher
1480+
const fetcher = createServicesFetcher({
1481+
endpoint: endpointBaseUrl + target.id,
1482+
key: cdnAccessResult.secretAccessToken,
1483+
});
1484+
1485+
const result = await fetcher();
1486+
1487+
// Should return array with schemaVersionId on each item
1488+
expect(result).toHaveLength(1);
1489+
expect(result[0].schemaVersionId).toBeDefined();
1490+
expect(result[0].name).toBe('ping');
1491+
expect(result[0].sdl).toContain('Query');
1492+
expect(result[0].id).toBeDefined();
1493+
},
1494+
);
1495+
1496+
test.concurrent(
1497+
'createServicesFetcher with schemaVersionId fetches pinned version',
1498+
async ({ expect }) => {
1499+
const endpointBaseUrl = await getBaseEndpoint();
1500+
const { createOrg } = await initSeed().createOwner();
1501+
const { createProject } = await createOrg();
1502+
const { createTargetAccessToken, createCdnAccess, target } = await createProject(
1503+
ProjectType.Federation,
1504+
);
1505+
const writeToken = await createTargetAccessToken({});
1506+
1507+
// Publish V1 Schema
1508+
await writeToken
1509+
.publishSchema({
1510+
author: 'Kamil',
1511+
commit: 'v1',
1512+
sdl: `type Query { ping: String }`,
1513+
service: 'ping',
1514+
url: 'http://ping.com',
1515+
})
1516+
.then(r => r.expectNoGraphQLErrors());
1517+
1518+
// Get V1 version ID
1519+
const v1Version = await writeToken.fetchLatestValidSchema();
1520+
const v1VersionId = v1Version.latestValidVersion?.id;
1521+
expect(v1VersionId).toBeDefined();
1522+
1523+
const cdnAccessResult = await createCdnAccess();
1524+
1525+
// Fetch V1 and capture content
1526+
const v1Fetcher = createServicesFetcher({
1527+
endpoint: endpointBaseUrl + target.id,
1528+
key: cdnAccessResult.secretAccessToken,
1529+
});
1530+
const v1Result = await v1Fetcher();
1531+
expect(v1Result[0].schemaVersionId).toBe(v1VersionId);
1532+
1533+
// Publish V2 Schema with different content
1534+
await writeToken
1535+
.publishSchema({
1536+
author: 'Kamil',
1537+
commit: 'v2',
1538+
sdl: `type Query { ping: String, pong: String }`,
1539+
service: 'ping',
1540+
url: 'http://ping.com',
1541+
})
1542+
.then(r => r.expectNoGraphQLErrors());
1543+
1544+
// Create a pinned fetcher for V1
1545+
const pinnedFetcher = createServicesFetcher({
1546+
endpoint: endpointBaseUrl + target.id,
1547+
key: cdnAccessResult.secretAccessToken,
1548+
schemaVersionId: v1VersionId!,
1549+
});
1550+
1551+
const pinnedResult = await pinnedFetcher();
1552+
1553+
// Should still return V1 content even after V2 was published
1554+
expect(pinnedResult[0].sdl).toBe(v1Result[0].sdl);
1555+
expect(pinnedResult[0].sdl).not.toContain('pong');
1556+
1557+
// Latest fetcher should return V2
1558+
const latestFetcher = createServicesFetcher({
1559+
endpoint: endpointBaseUrl + target.id,
1560+
key: cdnAccessResult.secretAccessToken,
1561+
});
1562+
const latestResult = await latestFetcher();
1563+
expect(latestResult[0].sdl).toContain('pong');
1564+
expect(latestResult[0].schemaVersionId).not.toBe(v1VersionId);
1565+
},
1566+
);
12331567
});
12341568
}
12351569

packages/libraries/apollo/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,23 @@ export function createSupergraphManager(args: CreateSupergraphManagerArgs) {
7979
});
8080

8181
let timer: ReturnType<typeof setTimeout> | null = null;
82+
let currentSchemaVersionId: string | undefined;
8283

8384
return {
8485
async initialize(hooks: { update(supergraphSdl: string): void }): Promise<{
8586
supergraphSdl: string;
87+
schemaVersionId?: string;
8688
cleanup?: () => Promise<void>;
8789
}> {
8890
const initialResult = await artifactsFetcher.fetch();
91+
currentSchemaVersionId = initialResult.schemaVersionId;
8992

9093
function poll() {
9194
timer = setTimeout(async () => {
9295
try {
9396
const result = await artifactsFetcher.fetch();
9497
if (result.contents) {
98+
currentSchemaVersionId = result.schemaVersionId;
9599
hooks.update?.(result.contents);
96100
}
97101
} catch (error) {
@@ -105,6 +109,7 @@ export function createSupergraphManager(args: CreateSupergraphManagerArgs) {
105109

106110
return {
107111
supergraphSdl: initialResult.contents,
112+
schemaVersionId: initialResult.schemaVersionId,
108113
cleanup: async () => {
109114
if (timer) {
110115
clearTimeout(timer);
@@ -113,6 +118,9 @@ export function createSupergraphManager(args: CreateSupergraphManagerArgs) {
113118
},
114119
};
115120
},
121+
getSchemaVersionId(): string | undefined {
122+
return currentSchemaVersionId;
123+
},
116124
};
117125
}
118126

0 commit comments

Comments
 (0)