Skip to content

Commit

Permalink
implement supergraph schema endpoint, make routingUrl mandatory
Browse files Browse the repository at this point in the history
  • Loading branch information
StarpTech committed May 23, 2021
1 parent 906d869 commit 2067bcd
Show file tree
Hide file tree
Showing 29 changed files with 447 additions and 99 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ State: Experimental
- Create multiple graphs (for example, staging and production, or different development branches)
- Stores versioned schemas for all GraphQL-federated services
- Serves schema for GraphQL gateway based on provided services & their versions
- Serves a supergraph schema for the GraphQL gateway
- Validates new schema to be compatible with other running services
- Validates that all client operations are supported by your schema
- Produce a diff between your proposed schema and the current registry state to detect breaking changes and more
Expand Down
2 changes: 2 additions & 0 deletions benchmark/composed-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function setup() {
let data = {
typeDefs: 'type Query { hello: String }',
version: '1',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
graphName: 'my_graph',
serviceName: 'foo',
}
Expand All @@ -36,6 +37,7 @@ export function setup() {
data = {
typeDefs: 'type Query { world: String }',
version: '1',
routingUrl: 'http://localhost:3001/api/graphql',
graphName: 'my_graph',
serviceName: 'bar',
}
Expand Down
19 changes: 18 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ POST - `/schema/push` Creates a new graph and schema for a service. If you omit
"graphName": "my_graph",
"serviceName": "foo",
"version": "1", // optional, uses "current" by default
"routingUrl": "http://products-graphql.svc.cluster.local:4001/graphql" // optional, for federation
"routingUrl": "http://products-graphql.svc.cluster.local:4001/graphql"
}
```

Expand All @@ -76,6 +76,23 @@ POST - `/schema/compose` Returns the last registered schema definition of all se
</p>
</details>

### Get supergraph schema

POST - `/schema/supergraph` Returns the supergraph schema definition of all registered services.

<details>
<summary>Example Request</summary>
<p>

```jsonc
{
"graphName": "my_graph"
}
```

</p>
</details>

### Deactivate a schema

PUT - `/schema/deactivate` Deactivates a schema by id. The schema will no longer be part of any result. You can re-activate it by registering.
Expand Down
34 changes: 13 additions & 21 deletions examples/apollo-managed-federation/gateway.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
const { ApolloServer } = require('apollo-server')
const { ApolloGateway } = require('@apollo/gateway')
const { get } = require('httpie')
const { parse } = require('graphql')
const { composeAndValidate } = require('@apollo/federation')

// TODO: registry should return supgergraph sdl
async function fetchServices() {
const res = await get(`http://localhost:3000/schema/latest?graphName=my_graph`)

return res.data.data.map((svc) => {
return {
name: svc.serviceName,
url: svc.routingUrl,
typeDefs: parse(svc.typeDefs),
}
const { post } = require('httpie')

async function getSupergraph() {
const res = await post(`http://localhost:3000/schema/supergraph`, {
body: {
graphName: 'my_graph',
},
})

return {
supergraphSdl: res.data.data.supergraphSdl,
id: res.data.data.compositionId,
}
}

async function startServer() {
Expand All @@ -23,13 +21,7 @@ async function startServer() {
experimental_pollInterval: 30000,

async experimental_updateSupergraphSdl() {
const services = await fetchServices()
const { supergraphSdl } = composeAndValidate(services)
return {
// TODO: registry should return compositionId
id: services.map((s) => s.version).join('-'), // supergraph only updates when id changes
supergraphSdl,
}
return getSupergraph()
},

// Experimental: Enabling this enables the query plan view in Playground.
Expand Down
2 changes: 1 addition & 1 deletion examples/apollo-managed-federation/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion examples/apollo-managed-federation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"concurrently": "latest"
},
"dependencies": {
"@apollo/federation": "^0.25.0",
"@apollo/gateway": "^0.28.1",
"apollo-server": "^2.24.1"
}
Expand Down
2 changes: 1 addition & 1 deletion insomnia.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/core/basic-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ test('Should return 200 because credentials are valid', async (t) => {
payload: {
typeDefs: `type Query { world: String }`,
version: '2',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_bar`,
graphName: `${t.context.graphName}`,
},
Expand All @@ -44,6 +45,7 @@ test('Should support multiple secrets comma separated', async (t) => {
payload: {
typeDefs: `type Query { world: String }`,
version: '3',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_bar`,
graphName: `${t.context.graphName}`,
},
Expand Down
6 changes: 6 additions & 0 deletions src/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ export const InvalidServiceScopeError = createError(
`You are not authorized to access service "%s"`,
401,
)
export const DuplicateServiceUrlError = createError(
'GR_DUPLICATE_SERVICE_URL',
`Service "%s" already use the routingUrl "%s"`,
400,
)
export const InvalidGraphNameError = createError(
'GR_INVALID_GRAPH_NAME',
`Graph with name "%s" does not exist`,
400,
)
export const SchemaCompositionError = createError('GR_SCHEMA_COMPOSITION', `%s`, 400)
export const SchemaVersionLookupError = createError('GR_SCHEMA_VERSION_LOOKUP', `%s`, 400)
export const SupergraphCompositionError = createError('GR_SUPERGRAPH_COMPOSITION', `%s`, 400)
export const SchemaNotFoundError = createError(
'GR_SCHEMA_NOT_FOUND',
`Could not find schema with id "%s"`,
Expand Down
33 changes: 24 additions & 9 deletions src/core/federation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parse } from 'graphql'
import { GraphQLSchema, parse } from 'graphql'
import { composeAndValidate } from '@apollo/federation'

export interface ServiceSchema {
Expand All @@ -7,9 +7,18 @@ export interface ServiceSchema {
url?: string
}

export function composeAndValidateSchema(servicesSchemaMap: ServiceSchema[]) {
let schema
let error = null
export interface CompositionResult {
error: string | null
schema: GraphQLSchema | null
supergraphSdl: string | undefined
}

export function composeAndValidateSchema(servicesSchemaMap: ServiceSchema[]): CompositionResult {
let result: CompositionResult = {
error: null,
schema: null,
supergraphSdl: '',
}

try {
const serviceList = servicesSchemaMap.map((schema) => {
Expand All @@ -24,14 +33,20 @@ export function composeAndValidateSchema(servicesSchemaMap: ServiceSchema[]) {
}
})

const { schema: validatedSchema, errors: validationErrors } = composeAndValidate(serviceList)
schema = validatedSchema
const {
schema: validatedSchema,
errors: validationErrors,
supergraphSdl,
} = composeAndValidate(serviceList)
if (!!validationErrors) {
error = `${validationErrors[0]}`
result.error = `${validationErrors[0]}`
return result
}
result.schema = validatedSchema
result.supergraphSdl = supergraphSdl
} catch (err) {
error = `${err.message}`
result.error = `${err.message}`
}

return { schema, error }
return result
}
25 changes: 24 additions & 1 deletion src/core/repositories/ServiceRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ export default class ServiceRepository {
.select(`${table}.*`)
.first<ServiceDBModel>()
}
findByRoutingUrl({ graphName, routingUrl }: { graphName: string; routingUrl: string }) {
const knex = this.knex
const table = ServiceDBModel.table
return knex
.from(table)
.join(
GraphDBModel.table,
ServiceDBModel.fullName('graphId'),
'=',
GraphDBModel.fullName('id'),
)
.where({
[GraphDBModel.fullName('isActive')]: true,
[GraphDBModel.fullName('name')]: graphName,
[ServiceDBModel.fullName('routingUrl')]: routingUrl,
})
.select(`${table}.*`)
.first<ServiceDBModel>()
}
findByNames(
{ graphName }: { graphName: string },
serviceNames: string[],
Expand Down Expand Up @@ -72,7 +91,10 @@ export default class ServiceRepository {
.whereNot(ServiceDBModel.fullName('name'), exceptService)
.orderBy(ServiceDBModel.fullName('updatedAt'), 'desc')
}
findMany({ graphName }: { graphName: string }): Promise<ServiceDBModel[]> {
findMany(
{ graphName }: { graphName: string },
where: Partial<ServiceDBModel> = {},
): Promise<ServiceDBModel[]> {
const knex = this.knex
const table = ServiceDBModel.table
return knex
Expand All @@ -84,6 +106,7 @@ export default class ServiceRepository {
'=',
GraphDBModel.fullName('id'),
)
.where(where)
.where({
[GraphDBModel.fullName('isActive')]: true,
[GraphDBModel.fullName('name')]: graphName,
Expand Down
5 changes: 5 additions & 0 deletions src/core/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createHash } from 'crypto'

export function hash(data: string) {
return createHash('sha256').update(data).digest('hex')
}
6 changes: 4 additions & 2 deletions src/migrations/20210504193054_initial_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export async function up(knex: Knex): Promise<void> {
.createTable(ServiceDBModel.table, (table) => {
table.increments(ServiceDBModel.field('id')).primary()

table.string(ServiceDBModel.field('name'))
table.string(ServiceDBModel.field('name')).notNullable()
table.boolean(ServiceDBModel.field('isActive')).notNullable().defaultTo(true)
table.string(ServiceDBModel.field('routingUrl')).nullable()
table.string(ServiceDBModel.field('routingUrl')).notNullable()
table
.timestamp(ServiceDBModel.field('createdAt'), { useTz: true })
.notNullable()
Expand All @@ -46,6 +46,8 @@ export async function up(knex: Knex): Promise<void> {
.index()

table.index([ServiceDBModel.field('isActive'), ServiceDBModel.field('name')])
table.unique([ServiceDBModel.field('graphId'), ServiceDBModel.field('name')])
table.unique([ServiceDBModel.field('graphId'), ServiceDBModel.field('routingUrl')])
})
.createTable(SchemaDBModel.table, (table) => {
table.increments(SchemaDBModel.field('id')).primary()
Expand Down
3 changes: 3 additions & 0 deletions src/registry/document-validation/document-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ test('Should validate document as valid', async (t) => {
}
`,
version: '1',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_foo`,
graphName: `${t.context.graphName}`,
},
Expand All @@ -40,6 +41,7 @@ test('Should validate document as valid', async (t) => {
}
`,
version: '1',
routingUrl: `http://${t.context.testPrefix}_bar:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_bar`,
graphName: `${t.context.graphName}`,
},
Expand Down Expand Up @@ -89,6 +91,7 @@ test('Should validate document as invalid because field does not exist', async (
}
`,
version: '1',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_foo`,
graphName: `${t.context.graphName}`,
},
Expand Down
13 changes: 11 additions & 2 deletions src/registry/federation/compose-schema-versions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ test('Should return schema of two services', async (t) => {
}
`,
version: '1',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_foo`,
graphName: `${t.context.graphName}`,
},
Expand All @@ -46,6 +47,7 @@ test('Should return schema of two services', async (t) => {
}
`,
version: '2',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_foo`,
graphName: `${t.context.graphName}`,
},
Expand All @@ -62,6 +64,7 @@ test('Should return schema of two services', async (t) => {
}
`,
version: '1',
routingUrl: `http://${t.context.testPrefix}_bar:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_bar`,
graphName: `${t.context.graphName}`,
},
Expand All @@ -78,6 +81,7 @@ test('Should return schema of two services', async (t) => {
}
`,
version: '2',
routingUrl: `http://${t.context.testPrefix}_bar:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_bar`,
graphName: `${t.context.graphName}`,
},
Expand Down Expand Up @@ -205,6 +209,7 @@ test('Should return 404 when schema in version could not be found', async (t) =>
}
`,
version: '1',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_foo`,
graphName: `${t.context.graphName}`,
},
Expand Down Expand Up @@ -253,6 +258,7 @@ test('Should return 400 when schema in specified version was deactivated', async
}
`,
version: '1',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_foo`,
graphName: `${t.context.graphName}`,
},
Expand Down Expand Up @@ -313,6 +319,7 @@ test('Version "current" should always return the latest (not versioned) register
hello: String
}
`,
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_foo`,
graphName: `${t.context.graphName}`,
},
Expand All @@ -330,6 +337,7 @@ test('Version "current" should always return the latest (not versioned) register
world: String
}
`,
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
serviceName: `${t.context.testPrefix}_foo`,
graphName: `${t.context.graphName}`,
},
Expand All @@ -349,6 +357,7 @@ test('Version "current" should always return the latest (not versioned) register
`,
serviceName: `${t.context.testPrefix}_foo`,
version: '1',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
graphName: `${t.context.graphName}`,
},
})
Expand Down Expand Up @@ -403,7 +412,7 @@ test('Should include "routingUrl" of the service', async (t) => {
`,
version: '1',
serviceName: `${t.context.testPrefix}_foo`,
routingUrl: 'http://localhost:3000/api/graphql',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
graphName: `${t.context.graphName}`,
},
})
Expand Down Expand Up @@ -437,7 +446,7 @@ test('Should include "routingUrl" of the service', async (t) => {
hello: String
}
`,
routingUrl: 'http://localhost:3000/api/graphql',
routingUrl: `http://${t.context.testPrefix}_foo:3000/api/graphql`,
version: '1',
})
})
Loading

0 comments on commit 2067bcd

Please sign in to comment.