Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .changeset/strange-wolves-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@graphql-mesh/fusion-runtime': minor
'@graphql-tools/federation': minor
'@graphql-hive/gateway-runtime': minor
---

Support promises in `progressiveOverride` option

```ts
import { defineConfig } from '@graphql-hive/gateway';
export const gatewayConfig = defineConfig({
async progressiveOverride(label: string, context: GatewayContext) {
if (label === 'my_label') {
const serviceResponse = await fetch('http://example.com/should_override', {
headers: {
'x-some-header': context.headers['x-some-header'],
}
});
const result = await serviceResponse.json();
return result?.override;
}
return false;
}
})
```
1 change: 1 addition & 0 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
- subscriptions-with-transforms
- type-merging-batching
- operation-field-permissions
- launchdarkly-override
# TODO: support containers before enabling (we use nats image in e2e)
# - event-driven-federated-subscriptions
name: Convert ${{matrix.e2e}}
Expand Down
82 changes: 82 additions & 0 deletions e2e/launchdarkly-override/gateway.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { defineConfig } from '@graphql-hive/gateway';
import ld from '@launchdarkly/node-server-sdk';

const {
LAUNCH_DARKLY_PROJECT_ID,
LAUNCH_DARKLY_ENVIRONMENT,
LAUNCH_DARKLY_REST_API_KEY,
LAUNCH_DARKLY_SDK_KEY,
} = process.env;

const LABEL_PREFIX = 'launchDarkly:';

let flagValues: Record<string, number> = {};
listenForFlagUpdates((name, value) => {
flagValues[name] = value;
});

export const gatewayConfig = defineConfig({
progressiveOverride(label) {
// ignore labels that don't start with our prefix
if (!label.startsWith(LABEL_PREFIX)) return false;
// remove prefix from label
const flagKey = label.substring(LABEL_PREFIX.length);
// find flagKey in flagValues and roll the dice to see if we should override
const flagValue = flagValues[flagKey];
if (!flagValue) return false;
return Math.random() * 100 < flagValue;
},
});

export async function listenForFlagUpdates(
listener: (name: string, value: number) => void,
) {
if (
!LAUNCH_DARKLY_SDK_KEY ||
!LAUNCH_DARKLY_REST_API_KEY ||
!LAUNCH_DARKLY_PROJECT_ID ||
!LAUNCH_DARKLY_ENVIRONMENT
) {
throw new Error('LaunchDarkly environment variables are not set');
}
const ldClient = ld.init(LAUNCH_DARKLY_SDK_KEY);
await ldClient.waitForInitialization();

const allFlagsResult = await (
await fetch(
`https://app.launchdarkly.com/api/v2/flags/${LAUNCH_DARKLY_PROJECT_ID}?env=${LAUNCH_DARKLY_ENVIRONMENT}`,
{
headers: {
Authorization: LAUNCH_DARKLY_REST_API_KEY,
},
},
)
).json();

for (const flag of allFlagsResult.items) {
const ffKey = flag.key;
const variations =
flag.environments[LAUNCH_DARKLY_ENVIRONMENT]._summary.variations;
if (Object.keys(variations).length === 2 && variations['0'].rollout) {
listener(ffKey, variations['0'].rollout / 1000);
}
}

ldClient.on('update', async (param) => {
const updatedFlag = await (
await fetch(
`https://app.launchdarkly.com/api/v2/flags/${LAUNCH_DARKLY_PROJECT_ID}/${param.key}?env=${LAUNCH_DARKLY_ENVIRONMENT}`,
{
headers: {
Authorization: LAUNCH_DARKLY_REST_API_KEY,
},
},
)
).json();
listener(
param.key,
updatedFlag.environments[LAUNCH_DARKLY_ENVIRONMENT].fallthrough.rollout
.variations[0].weight / 1000,
);
});
}
11 changes: 11 additions & 0 deletions e2e/launchdarkly-override/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@e2e/launchdarkly-override-example",
"private": true,
"dependencies": {
"@apollo/subgraph": "2.11.3",
"@launchdarkly/node-server-sdk": "9.10.2",
"graphql": "16.11.0",
"graphql-yoga": "5.16.0",
"tslib": "^2.8.1"
}
}
39 changes: 39 additions & 0 deletions e2e/launchdarkly-override/services/inventory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createServer } from 'http';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { Opts } from '@internal/testing';
import { parse } from 'graphql';
import { createYoga } from 'graphql-yoga';

const port = Opts(process.argv).getServicePort('inventory');

createServer(
createYoga({
schema: buildSubgraphSchema({
typeDefs: parse(/* GraphQL */ `
type Product @key(fields: "id") {
id: ID!
inStock: Boolean
@override(from: "products", label: "use_inventory_service")
count: Int
}

extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.7"
import: ["@key", "@override"]
)
`),
resolvers: {
Product: {
__resolveReference(reference: { id: string }) {
return {
id: reference.id,
};
},
inStock: () => true,
count: () => 42,
},
},
}),
}),
).listen(port);
43 changes: 43 additions & 0 deletions e2e/launchdarkly-override/services/products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createServer } from 'http';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { Opts } from '@internal/testing';
import { parse } from 'graphql';
import { createYoga } from 'graphql-yoga';

const port = Opts(process.argv).getServicePort('products');

createServer(
createYoga({
schema: buildSubgraphSchema({
typeDefs: parse(/* GraphQL */ `
type Product @key(fields: "id") {
id: ID!
name: String
price: Float
inStock: Boolean
}

type Query {
product(id: ID!): Product
}
`),
resolvers: {
Product: {
__resolveReference(reference: { id: string }) {
return {
id: reference.id,
};
},
name: (parent: { id: string }) => `Product ${parent.id}`,
price: () => 9.99,
inStock: () => false,
},
Query: {
product(_: any, args: { id: string }) {
return { id: args.id };
},
},
},
}),
}),
).listen(port);
39 changes: 39 additions & 0 deletions e2e/launchdarkly-override/services/reviews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createServer } from 'http';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { Opts } from '@internal/testing';
import { parse } from 'graphql';
import { createYoga } from 'graphql-yoga';

const port = Opts(process.argv).getServicePort('reviews');

createServer(
createYoga({
schema: buildSubgraphSchema({
typeDefs: parse(/* GraphQL */ `
type Review @key(fields: "id") {
id: ID!
content: String
product: Product
}

type Query {
reviews: [Review]
}

type Product @key(fields: "id") {
id: ID!
}
`),
resolvers: {
Query: {
reviews() {
return [
{ id: '1', content: 'Great product!', product: { id: '101' } },
{ id: '2', content: 'Not bad', product: { id: '102' } },
];
},
},
},
}),
}),
).listen(port);
21 changes: 15 additions & 6 deletions e2e/progressive-override/gateway.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { defineConfig, GatewayContext } from '@graphql-hive/gateway';

const labelServiceUrl = process.env['LABEL_SERVICE_URL'];

if (!labelServiceUrl) {
throw new Error('LABEL_SERVICE_URL environment variable is not defined');
}

export const gatewayConfig = defineConfig({
progressiveOverride(label, context: GatewayContext) {
if (
label === 'use_inventory_service' &&
context.request.headers.get('x-use-inventory-service') === 'true'
) {
return true;
async progressiveOverride(label, context: GatewayContext) {
if (label === 'use_inventory_service') {
const serviceRes = await fetch(labelServiceUrl, {
headers: {
'x-use-inventory-service':
context.headers['x-use-inventory-service'] || 'false',
},
}).then((res) => res.text());
return serviceRes === 'use_inventory_service';
}
return false;
},
Expand Down
27 changes: 24 additions & 3 deletions e2e/progressive-override/progressive-override.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { createTenv } from '@internal/e2e';
import { createTenv, handleDockerHostNameInURLOrAtPath } from '@internal/e2e';
import { describe, expect, it } from 'vitest';

const { service, gateway } = createTenv(__dirname);
const { service, gateway, gatewayRunner } = createTenv(__dirname);
describe('Progressive Override E2E', async () => {
it('overrides products if the header exists', async () => {
const labelService = await service('label');
let labelServiceUrl = `http://localhost:${labelService.port}`;
if (gatewayRunner.includes('docker')) {
labelServiceUrl = await handleDockerHostNameInURLOrAtPath(
labelServiceUrl,
[],
);
}
const gw = await gateway({
supergraph: {
with: 'apollo',
Expand All @@ -13,6 +21,9 @@ describe('Progressive Override E2E', async () => {
await service('reviews'),
],
},
env: {
LABEL_SERVICE_URL: labelServiceUrl,
},
});
const result = await gw.execute({
query: /* GraphQL */ `
Expand Down Expand Up @@ -58,8 +69,15 @@ describe('Progressive Override E2E', async () => {
});
});
it('does not override products if the header does not exist', async () => {
const labelService = await service('label');
let labelServiceUrl = `http://localhost:${labelService.port}`;
if (gatewayRunner.includes('docker')) {
labelServiceUrl = await handleDockerHostNameInURLOrAtPath(
labelServiceUrl,
[],
);
}
const gw = await gateway({
pipeLogs: 'gw.log',
supergraph: {
with: 'apollo',
services: [
Expand All @@ -68,6 +86,9 @@ describe('Progressive Override E2E', async () => {
await service('reviews'),
],
},
env: {
LABEL_SERVICE_URL: labelServiceUrl,
},
});
const result = await gw.execute({
query: /* GraphQL */ `
Expand Down
15 changes: 15 additions & 0 deletions e2e/progressive-override/services/label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createServer } from 'http';
import { Opts } from '@internal/testing';

const port = Opts(process.argv).getServicePort('label');

createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
if (req.headers['x-use-inventory-service'] === 'true') {
res.end('use_inventory_service');
} else {
res.end('do_not_use_inventory_service');
}
}).listen(port, () => {
console.log(`Label service is running at http://localhost:${port}/`);
});
Loading
Loading