@apollo/subgraph
Version:
Apollo Subgraph Utilities
287 lines (255 loc) • 10.2 kB
text/typescript
import {
GraphQLFieldConfig,
GraphQLString,
GraphQLUnionType,
GraphQLObjectType,
GraphQLScalarType,
GraphQLNonNull,
GraphQLList,
GraphQLType,
GraphQLNamedType,
isNamedType,
isObjectType,
GraphQLResolveInfo,
isInterfaceType,
defaultTypeResolver,
GraphQLError,
GraphQLAbstractType,
GraphQLSchema,
GraphQLInterfaceType,
} from 'graphql';
import { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue';
import { maybeCacheControlFromInfo } from '@apollo/cache-control-types';
import { ApolloGraphQLInterfaceTypeExtensions, ApolloGraphQLObjectTypeExtensions, GraphQLReferenceResolver } from './schemaExtensions';
import { inspect } from 'util';
export type Maybe<T> = null | undefined | T;
export const EntityType = new GraphQLUnionType({
name: '_Entity',
types: [],
});
export const ServiceType = new GraphQLObjectType({
name: '_Service',
fields: {
sdl: {
type: GraphQLString,
description:
'The sdl representing the federated service capabilities. Includes federation directives, removes federation types, and includes rest of full schema after schema directives have been applied',
},
},
});
export const AnyType = new GraphQLScalarType({
name: '_Any',
serialize(value) {
return value;
},
});
export const LinkImportType = new GraphQLScalarType({
name: 'link__Import',
specifiedByURL: null
});
function isPromise<T>(value: PromiseOrValue<T>): value is Promise<T> {
return typeof (value as {then?: unknown})?.then === 'function';
}
async function maybeAddTypeNameToPossibleReturn<T extends { __typename?: string }>(
maybeObject: PromiseOrValue<null | T>,
typename: string,
): Promise<null | T> {
const objectOrNull = await maybeObject;
if (
objectOrNull !== null
&& typeof objectOrNull === 'object'
) {
// If the object already has a __typename assigned, we're "refining" the
// type from an interface to an interfaceObject.
if ('__typename' in objectOrNull && objectOrNull.__typename !== typename) {
// XXX There's a really interesting nuance here in this condition. At a
// first glance, it looks like the code here and below could be simplified
// to just:
// ```
// objectOrNull.__typename = typename;
// return objectOrNull;
// ```
// But in this case, something internal to `graphql-js` depends on the
// identity of the object returned here. If we mutate in this case, we end
// up with errors from `graphql-js`. This might be worth investigating at
// some point, but for now creating a new object solves the problem and
// doesn't create any new ones.
return {
...objectOrNull,
__typename: typename,
};
}
Object.defineProperty(objectOrNull, '__typename', {
value: typename,
});
}
return objectOrNull;
}
/**
* Copied and adapted from GraphQL-js to provide more tailored error messages (but the equivalent method is also not exported
* by `graphql-js`).
*
* For @key on interfaces, we need to check that we can resolve the runtime type of the object returned by the interface
* `__resolveReference`. If we cannot, and we simply don't add any `__typename` to the result, the graphQL-js will end
* erroring out, but the error will not be user friendly as it will say something along the lines of:
* ```
* Abstract type "_Entity" must resolve to an Object type at runtime for field "Query._entities". Either the "_Entity" type
* should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.
* ```
* But this is ultimately incorrect, as it is only interface type the user must use "resolveType", add a __typename, or rely
* on "isTypeOf". And so we have to somewhat copy and adapt the logic slightly (mostly to provide a more user friendly method).
*/
function ensureValidRuntimeType(
runtimeTypeName: unknown,
schema: GraphQLSchema,
returnType: GraphQLAbstractType,
result: unknown,
): GraphQLObjectType {
if (runtimeTypeName == null) {
throw new GraphQLError(
`Abstract type "${returnType.name}" \`__resolveReference\` method must resolve to an Object type at runtime. Either the object returned by "${returnType}.__resolveReference" must include a valid \`__typename\` field, or the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`,
);
}
if (typeof runtimeTypeName !== 'string') {
throw new GraphQLError(
`Abstract type "${returnType.name}" \`__resolveReference\` method must resolve to an Object type at runtime with ` +
`value ${inspect(result)}, received "${inspect(runtimeTypeName)}".`,
);
}
const runtimeType = schema.getType(runtimeTypeName);
if (runtimeType == null) {
throw new GraphQLError(
`Abstract type "${returnType.name}" \`__resolveReference\` method resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`,
);
}
if (!isObjectType(runtimeType)) {
throw new GraphQLError(
`Abstract type "${returnType.name}" \`__resolveReference\` method resolved to a non-object type "${runtimeTypeName}".`,
);
}
if (!schema.isSubType(returnType, runtimeType)) {
throw new GraphQLError(
`Runtime Object type "${runtimeType.name}" \`__resolveReference\` method is not a possible type for "${returnType.name}".`,
);
}
return runtimeType;
}
async function withResolvedType<T>({
type,
value,
context,
info,
callback,
}: {
type: GraphQLInterfaceType,
value: any,
context: any,
info: GraphQLResolveInfo,
callback: (runtimeType: GraphQLObjectType) => PromiseOrValue<T>,
}): Promise<T> {
const resolvedValue = await value;
if (resolvedValue === null) {
return resolvedValue;
}
const resolveTypeFn = type.resolveType ?? defaultTypeResolver;
const runtimeType = resolveTypeFn(resolvedValue, context, info, type);
if (isPromise(runtimeType)) {
return runtimeType.then((name) => (
callback(ensureValidRuntimeType(name, info.schema, type, resolvedValue))
));
}
return callback(ensureValidRuntimeType(runtimeType, info.schema, type, resolvedValue));
}
function definedResolveReference(type: GraphQLObjectType | GraphQLInterfaceType): GraphQLReferenceResolver<any> | undefined {
const extensions: ApolloGraphQLObjectTypeExtensions | ApolloGraphQLInterfaceTypeExtensions = type.extensions;
return extensions.apollo?.subgraph?.resolveReference;
}
export function entitiesResolver({
representations,
context,
info
}: {
representations: any,
context: any,
info: GraphQLResolveInfo
}) {
return representations.map((reference: { __typename: string } & object) => {
const { __typename } = reference;
const type = info.schema.getType(__typename);
if (!type || !(isObjectType(type) || isInterfaceType(type))) {
throw new Error(
`The _entities resolver tried to load an entity for type "${__typename}", but no object or interface type of that name was found in the schema`,
);
}
// If you're using `@apollo/subgraph` with Apollo Server v3+ (without
// disabling the cache control plugin) and the schema has a `@cacheControl`
// directive on the specific type selected by `__typename`, restrict the
// request's cache policy based on that directive. (This does not work with
// Apollo Server 2 or non-Apollo-Server GraphQL servers;
// maybeCacheControlFromInfo will return null in that case.)
const cacheControl = maybeCacheControlFromInfo(info);
if (cacheControl) {
const cacheHint = cacheControl.cacheHintFromType(type);
if (cacheHint) {
cacheControl.cacheHint.restrict(cacheHint);
}
}
const resolveReference = definedResolveReference(type);
// FIXME somehow get this to show up special in Studio traces?
const result = resolveReference ? resolveReference(reference, context, info) : reference;
if (isInterfaceType(type)) {
return withResolvedType({
type,
value: result,
context,
info,
callback: (runtimeType) => {
// If we had no interface-level __resolveReference, then we look for one on the runtime
// type itself, and call it if it exists. If that one also doesn't, we essentially end
// up using the same resolver than for object types, one that does nothing.
let finalResult = maybeAddTypeNameToPossibleReturn(result, runtimeType.name);
if (!resolveReference) {
const runtimeResolveReference = definedResolveReference(runtimeType);
if (runtimeResolveReference) {
// Note that we call the resolver on the reference with the "proper" __typename,
// and then add back the __typename again in case the resolver removed it (which
// ultimately is the behaviour we use with object type __resolveReference in
// general).
finalResult = isPromise(finalResult)
? finalResult.then((r) => runtimeResolveReference(r, context, info))
: runtimeResolveReference(finalResult, context, info);
finalResult = maybeAddTypeNameToPossibleReturn(finalResult, runtimeType.name);
}
}
return finalResult;
},
});
}
return maybeAddTypeNameToPossibleReturn(result, __typename);
});
}
export const entitiesField: GraphQLFieldConfig<any, any> = {
type: new GraphQLNonNull(new GraphQLList(EntityType)),
args: {
representations: {
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(AnyType))),
},
},
resolve(_source, { representations }, context, info) {
return entitiesResolver({ representations, context, info });
},
};
export const serviceField: GraphQLFieldConfig<any, any> = {
type: new GraphQLNonNull(ServiceType),
};
export const federationTypes: GraphQLNamedType[] = [
ServiceType,
AnyType,
EntityType,
LinkImportType,
];
export function isFederationType(type: GraphQLType): boolean {
return (
isNamedType(type) && federationTypes.some(({ name }) => name === type.name)
);
}