diff --git a/integration-tests/fixtures/federation-init.graphql b/integration-tests/fixtures/federation-init.graphql new file mode 100644 index 000000000..51bd199aa --- /dev/null +++ b/integration-tests/fixtures/federation-init.graphql @@ -0,0 +1,8 @@ +type Query { + users: [User!]! +} + +type User { + id: ID! + email: String! +} diff --git a/integration-tests/testkit/flow.ts b/integration-tests/testkit/flow.ts index 4e3fc0219..73b2cad8d 100644 --- a/integration-tests/testkit/flow.ts +++ b/integration-tests/testkit/flow.ts @@ -573,6 +573,28 @@ export async function fetchSchemaFromCDN(selector: TargetSelectorInput, token: s }; } +export async function fetchSupergraphFromCDN(selector: TargetSelectorInput, token: string) { + const cdnAccessResult = await createCdnAccess(selector, token); + + if (cdnAccessResult.body.errors) { + throw new Error(cdnAccessResult.body.errors[0].message); + } + + const cdn = cdnAccessResult.body.data!.createCdnToken; + + const res = await axios.get(`${cdn.url}/supergraph`, { + headers: { + 'X-Hive-CDN-Key': cdn.token, + }, + responseType: 'text', + }); + + return { + body: res.data, + status: res.status, + }; +} + export async function fetchMetadataFromCDN(selector: TargetSelectorInput, token: string) { const cdnAccessResult = await createCdnAccess(selector, token); diff --git a/integration-tests/tests/cli/schema.spec.ts b/integration-tests/tests/cli/schema.spec.ts index d66773bd2..e62ba1e19 100644 --- a/integration-tests/tests/cli/schema.spec.ts +++ b/integration-tests/tests/cli/schema.spec.ts @@ -1,7 +1,13 @@ import { TargetAccessScope, ProjectType } from '@app/gql/graphql'; import { schemaPublish, schemaCheck } from '../../testkit/cli'; import { authenticate } from '../../testkit/auth'; -import { createOrganization, joinOrganization, createProject, createToken } from '../../testkit/flow'; +import { + createOrganization, + joinOrganization, + createProject, + createToken, + fetchSupergraphFromCDN, +} from '../../testkit/flow'; test('can publish and check a schema with target:registry:read access', async () => { const { access_token: owner_access_token } = await authenticate('main'); @@ -62,3 +68,72 @@ test('can publish and check a schema with target:registry:read access', async () 'EXIT: 1' ); }); + +test('service url should be available in supergraph', async () => { + const { access_token: owner_access_token } = await authenticate('main'); + const orgResult = await createOrganization( + { + name: 'foo', + }, + owner_access_token + ); + const org = orgResult.body.data!.createOrganization.ok!.createdOrganizationPayload.organization; + const code = org.inviteCode; + + // Join + const { access_token: member_access_token } = await authenticate('extra'); + await joinOrganization(code, member_access_token); + + const projectResult = await createProject( + { + organization: org.cleanId, + type: ProjectType.Federation, + name: 'foo', + }, + owner_access_token + ); + + const project = projectResult.body.data!.createProject.ok!.createdProject; + const target = projectResult.body.data!.createProject.ok!.createdTarget; + + // Create a token with write rights + const writeTokenResult = await createToken( + { + name: 'test', + organization: org.cleanId, + project: project.cleanId, + target: target.cleanId, + organizationScopes: [], + projectScopes: [], + targetScopes: [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite], + }, + owner_access_token + ); + expect(writeTokenResult.body.errors).not.toBeDefined(); + const writeToken = writeTokenResult.body.data!.createToken.ok!.secret; + + await schemaPublish([ + '--token', + writeToken, + '--author', + 'Kamil', + '--commit', + 'abc123', + '--service', + 'users', + '--url', + 'https://api.com/users-subgraph', + 'fixtures/federation-init.graphql', + ]); + + const supergraph = await fetchSupergraphFromCDN( + { + organization: org.cleanId, + project: project.cleanId, + target: target.cleanId, + }, + writeToken + ); + + expect(supergraph.body).toMatch('(name: "users" url: "https://api.com/users-subgraph")'); +}); diff --git a/packages/services/api/src/modules/schema/providers/schema-publisher.ts b/packages/services/api/src/modules/schema/providers/schema-publisher.ts index a53503424..6ad5118af 100644 --- a/packages/services/api/src/modules/schema/providers/schema-publisher.ts +++ b/packages/services/api/src/modules/schema/providers/schema-publisher.ts @@ -447,7 +447,7 @@ export class SchemaPublisher { url: incomingSchema.url ?? null, }); - this.publishToCDN({ + await this.publishToCDN({ valid, target, project, @@ -457,7 +457,7 @@ export class SchemaPublisher { } if (incomingSchema.metadata && latest.version && previousSchema) { - this.publishToCDN({ + await this.publishToCDN({ valid, target, project, @@ -505,7 +505,7 @@ export class SchemaPublisher { errors, initial: isInitialSchema, }); - this.publishToCDN({ + await this.publishToCDN({ valid, target, project, @@ -619,7 +619,7 @@ export class SchemaPublisher { }) { try { if (valid) { - this.updateCDN({ + await this.updateCDN({ target, project, schemas, diff --git a/packages/services/cdn-worker/src/handler.ts b/packages/services/cdn-worker/src/handler.ts index c438ed9c2..d8134a066 100644 --- a/packages/services/cdn-worker/src/handler.ts +++ b/packages/services/cdn-worker/src/handler.ts @@ -141,6 +141,9 @@ export async function handleRequest(request: Request, keyValidator: typeof isKey }); } } else { + console.log( + `CDN Artifact not found for targetId=${targetId}, artifactType=${artifactType}, storageKeyType=${storageKeyType}` + ); return new CDNArtifactNotFound(artifactType, targetId); } } diff --git a/packages/services/schema/src/orchestrators.ts b/packages/services/schema/src/orchestrators.ts index 66c30df7b..6f969aa78 100644 --- a/packages/services/schema/src/orchestrators.ts +++ b/packages/services/schema/src/orchestrators.ts @@ -85,13 +85,14 @@ interface CompositionFailure { } const createFederation: (redis: RedisInstance, logger: FastifyLoggerInstance) => Orchestrator = (redis, logger) => { - const compose = reuse( + const compose = reuse( async schemas => { const result = composeAndValidate( schemas.map(schema => { return { typeDefs: trimDescriptions(parse(schema.raw)), name: schema.source, + url: 'url' in schema && typeof schema.url === 'string' ? schema.url : undefined, }; }) );