Skip to content

Commit

Permalink
Rework value type implementation
Browse files Browse the repository at this point in the history
This commit implements value types as a core part of composition
and query planning. With this work, we now:
* build another metadata map for tracking value types
* attach value type metadata to schema type nodes
* make query planning decisions based on value type metadata
  • Loading branch information
trevor-scheer committed Aug 23, 2019
1 parent f6bc588 commit ba1da07
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { composeAndValidate } from '../composeAndValidate';
import gql from 'graphql-tag';
import { GraphQLObjectType } from 'graphql';
import { GraphQLObjectType, DocumentNode } from 'graphql';
import {
astSerializer,
typeSerializer,
Expand Down Expand Up @@ -276,65 +276,111 @@ it('errors on invalid usages of default operation names', () => {
`);
});

describe('value types integration tests', () => {
it('handles valid value types correctly', () => {
const duplicatedValueTypes = gql`
scalar Date
describe('composition of value types', () => {
function getSchemaWithValueType(valueType: DocumentNode) {
const serviceA = {
typeDefs: gql`
${valueType}
union CatalogItem = Couch | Mattress
type Query {
filler: String
}
`,
name: 'serviceA',
};

interface Product {
sku: ID!
}
const serviceB = {
typeDefs: valueType,
name: 'serviceB',
};

input NewProductInput {
sku: ID!
type: CatalogItemEnum
}
return composeAndValidate([serviceA, serviceB]);
}

enum CatalogItemEnum {
COUCH
MATTRESS
}
describe('success', () => {
it('scalars', () => {
const { errors, schema } = getSchemaWithValueType(
gql`
scalar Date
`,
);
expect(errors).toHaveLength(0);
expect(schema.getType('Date')).toMatchInlineSnapshot(`scalar Date`);
});

type Couch implements Product {
it('unions and object types', () => {
const { errors, schema } = getSchemaWithValueType(
gql`
union CatalogItem = Couch | Mattress
type Couch {
sku: ID!
material: String!
}
type Mattress {
sku: ID!
size: String!
}
`,
);
expect(errors).toHaveLength(0);
expect(schema.getType('CatalogItem')).toMatchInlineSnapshot(
`union CatalogItem = Couch | Mattress`,
);
expect(schema.getType('Couch')).toMatchInlineSnapshot(`
type Couch {
sku: ID!
material: String!
}
`);
});

type Mattress implements Product {
it('input types', () => {
const { errors, schema } = getSchemaWithValueType(gql`
input NewProductInput {
sku: ID!
type: String
}
`);
expect(errors).toHaveLength(0);
expect(schema.getType('NewProductInput')).toMatchInlineSnapshot(`
input NewProductInput {
sku: ID!
size: String!
type: String
}
`;
`);
});

const serviceA = {
typeDefs: gql`
type Query {
product: Product
it('interfaces', () => {
const { errors, schema } = getSchemaWithValueType(gql`
interface Product {
sku: ID!
}
${duplicatedValueTypes}
`,
name: 'serviceA',
};
`);
expect(errors).toHaveLength(0);
expect(schema.getType('Product')).toMatchInlineSnapshot(`
interface Product {
sku: ID!
}
`);
});

const serviceB = {
typeDefs: gql`
type Query {
topProducts: [Product]
it('enums', () => {
const { errors, schema } = getSchemaWithValueType(gql`
enum CatalogItemEnum {
COUCH
MATTRESS
}
${duplicatedValueTypes}
`,
name: 'serviceB',
};

const { errors, schema } = composeAndValidate([serviceA, serviceB]);

// A value type doesn't need any federation metadata to resolve
const couchType = schema.getType('Couch');
expect(couchType.federation.serviceName).toEqual(null);

expect(errors).toHaveLength(0);
`);
expect(errors).toHaveLength(0);
expect(schema.getType('CatalogItemEnum')).toMatchInlineSnapshot(`
enum CatalogItemEnum {
COUCH
MATTRESS
}
`);
});
});

describe('errors', () => {
Expand Down
40 changes: 37 additions & 3 deletions packages/apollo-federation/src/composition/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
parseSelections,
mapFieldNamesToServiceName,
stripExternalFieldsFromTypeDefs,
diffTypeNodes,
} from './utils';
import {
ServiceDefinition,
Expand Down Expand Up @@ -99,6 +100,14 @@ interface TypeToServiceMap {
export interface KeyDirectivesMap {
[typeName: string]: ServiceNameToKeyDirectivesMap;
}

/**
* A map for tracking which types have been determined to be a value type, a type
* shared across at least 2 services.
*/
export interface ValueTypesMap {
[typeName: string]: boolean;
}
/**
* Loop over each service and process its typeDefs (`definitions`)
* - build up typeToServiceMap
Expand All @@ -110,6 +119,7 @@ export function buildMapsFromServiceList(serviceList: ServiceDefinition[]) {
const typeToServiceMap: TypeToServiceMap = Object.create(null);
const externalFields: ExternalFieldDefinition[] = [];
const keyDirectivesMap: KeyDirectivesMap = Object.create(null);
const valueTypesMap: ValueTypesMap = Object.create(null);

for (const { typeDefs, name: serviceName } of serviceList) {
// Build a new SDL with @external fields removed, as well as information about
Expand Down Expand Up @@ -170,9 +180,27 @@ export function buildMapsFromServiceList(serviceList: ServiceDefinition[]) {

/**
* If this type already exists in the definitions map, push this definition to the array (newer defs
* take precedence). If not, create the definitions array and add it to the definitionsMap.
* take precedence). If the types are determined to be identical, add the type name
* to the valueTypesMap.
*
* If not, create the definitions array and add it to the definitionsMap.
*/
if (definitionsMap[typeName]) {
const { name, kind, fields, unionTypes } = diffTypeNodes(
definitionsMap[typeName][definitionsMap[typeName].length - 1],
definition,
);

const isValueType =
name.length === 0 &&
kind.length === 0 &&
Object.keys(fields).length === 0 &&
Object.keys(unionTypes).length === 0;

if (isValueType) {
valueTypesMap[typeName] = true;
}

definitionsMap[typeName].push({ ...definition, serviceName });
} else {
definitionsMap[typeName] = [{ ...definition, serviceName }];
Expand Down Expand Up @@ -260,6 +288,7 @@ export function buildMapsFromServiceList(serviceList: ServiceDefinition[]) {
extensionsMap,
externalFields,
keyDirectivesMap,
valueTypesMap,
};
}

Expand Down Expand Up @@ -300,19 +329,21 @@ export function buildSchemaFromDefinitionsAndExtensions({
}

/**
* Using the typeToServiceMap, augment the passed in `schema` to add `federation` metadata to the types and
* fields
* Using the various information we've collected about the schema, augment the
* `schema` itself with `federation` metadata to the types and fields
*/
export function addFederationMetadataToSchemaNodes({
schema,
typeToServiceMap,
externalFields,
keyDirectivesMap,
valueTypesMap,
}: {
schema: GraphQLSchema;
typeToServiceMap: TypeToServiceMap;
externalFields: ExternalFieldDefinition[];
keyDirectivesMap: KeyDirectivesMap;
valueTypesMap: ValueTypesMap;
}) {
for (const [
typeName,
Expand All @@ -329,6 +360,7 @@ export function addFederationMetadataToSchemaNodes({
...(keyDirectivesMap[typeName] && {
keys: keyDirectivesMap[typeName],
}),
isValueType: Boolean(valueTypesMap[typeName]),
};

// For object types, add metadata for all the @provides directives from its fields
Expand Down Expand Up @@ -420,6 +452,7 @@ export function composeServices(services: ServiceDefinition[]) {
extensionsMap,
externalFields,
keyDirectivesMap,
valueTypesMap,
} = buildMapsFromServiceList(services);

let { schema, errors } = buildSchemaFromDefinitionsAndExtensions({
Expand Down Expand Up @@ -465,6 +498,7 @@ export function composeServices(services: ServiceDefinition[]) {
typeToServiceMap,
externalFields,
keyDirectivesMap,
valueTypesMap,
});

/**
Expand Down
1 change: 1 addition & 0 deletions packages/apollo-federation/src/composition/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface FederationType {
externals?: {
[serviceName: string]: ExternalFieldDefinition[];
};
isValueType?: boolean;
}

export interface FederationField {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import gql from 'graphql-tag';
import { GraphQLResolverMap } from 'apollo-graphql';
import { log } from 'util';

export const name = 'books';
export const typeDefs = gql`
Expand Down Expand Up @@ -27,14 +28,23 @@ export const typeDefs = gql`
title: String
year: Int
similarBooks: [Book]!
metadata: [KeyValue]
metadata: [MetadataOrError]
}
# Value type
type KeyValue {
key: String!
value: String!
}
# Value type
type Error {
code: Int
message: String
}
# Value type
union MetadataOrError = KeyValue | Error
`;

const libraries = [{ id: '1', name: 'NYC Public Library' }];
Expand All @@ -49,7 +59,10 @@ const books = [
isbn: '0136291554',
title: 'Object Oriented Software Construction',
year: 1997,
metadata: [{ key: 'Condition', value: 'used' }],
metadata: [
{ key: 'Condition', value: 'used' },
{ code: '401', message: 'Unauthorized' },
],
},
{
isbn: '0201633612',
Expand Down Expand Up @@ -105,4 +118,9 @@ export const resolvers: GraphQLResolverMap<any> = {
return libraries.find(library => library.id === id);
},
},
MetadataOrError: {
__resolveType(object) {
return 'key' in object ? 'KeyValue' : 'Error';
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const typeDefs = gql`
name: String
price: String
brand: Brand
metadata: [KeyValue]
metadata: [MetadataOrError]
}
extend type Book implements Product @key(fields: "isbn") {
Expand All @@ -55,6 +55,15 @@ export const typeDefs = gql`
key: String!
value: String!
}
# Value type
type Error {
code: Int
message: String
}
# Value type
union MetadataOrError = KeyValue | Error
`;

const products = [
Expand Down Expand Up @@ -158,4 +167,9 @@ export const resolvers: GraphQLResolverMap<any> = {
return products.slice(0, args.first);
},
},
MetadataOrError: {
__resolveType(object) {
return 'key' in object ? 'KeyValue' : 'Error';
},
},
};
Loading

0 comments on commit ba1da07

Please sign in to comment.