graphql-modules
Version:
Create reusable, maintainable, testable and extendable GraphQL modules
267 lines (266 loc) • 9.7 kB
JavaScript
import { visit, Kind, concatAST, } from 'graphql';
import { moduleFactory } from '../module/factory';
import { createApplication } from '../application/application';
import { createModule } from '../module/module';
export function mockModule(testedModule, overrideConfig) {
const sourceProviders = typeof testedModule.config.providers === 'function'
? testedModule.config.providers()
: testedModule.config.providers;
const overrideProviders = typeof overrideConfig.providers === 'function'
? overrideConfig.providers()
: overrideConfig.providers;
const newModule = createModule({
...testedModule.config,
providers: [...(sourceProviders || []), ...(overrideProviders || [])],
});
newModule['ɵoriginalModule'] = testedModule;
return newModule;
}
export function testModule(testedModule, config) {
var _a;
const mod = transformModule(testedModule, config);
const modules = [mod].concat((_a = config === null || config === void 0 ? void 0 : config.modules) !== null && _a !== void 0 ? _a : []);
return createApplication({
...(config || {}),
modules,
providers: config === null || config === void 0 ? void 0 : config.providers,
middlewares: config === null || config === void 0 ? void 0 : config.middlewares,
});
}
function transformModule(mod, config) {
const transforms = [];
if (config === null || config === void 0 ? void 0 : config.replaceExtensions) {
transforms.push((m) => moduleFactory({
...m.config,
typeDefs: replaceExtensions(m.typeDefs),
}));
}
if (config === null || config === void 0 ? void 0 : config.typeDefs) {
transforms.push((m) => moduleFactory({
...m.config,
typeDefs: m.typeDefs.concat(config.typeDefs),
}));
}
if (config === null || config === void 0 ? void 0 : config.inheritTypeDefs) {
transforms.push((m) => moduleFactory({
...m.config,
typeDefs: inheritTypeDefs(m.typeDefs, config.inheritTypeDefs),
}));
}
if (config === null || config === void 0 ? void 0 : config.resolvers) {
transforms.push((m) => {
const resolvers = m.config.resolvers
? Array.isArray(m.config.resolvers)
? m.config.resolvers
: [m.config.resolvers]
: [];
return moduleFactory({
...m.config,
resolvers: resolvers.concat(config.resolvers),
});
});
}
if (config === null || config === void 0 ? void 0 : config.providers) {
transforms.push((m) => {
const sourceProviders = typeof m.config.providers === 'function'
? m.config.providers()
: m.config.providers;
const overrideProviders = typeof config.providers === 'function'
? config.providers()
: config.providers;
return moduleFactory({
...m.config,
providers: [...(sourceProviders || []), ...(overrideProviders || [])],
});
});
}
if (transforms) {
return transforms.reduce((m, transform) => transform(m), mod);
}
return mod;
}
function inheritTypeDefs(originalTypeDefs, modules) {
const original = concatAST(originalTypeDefs);
const typeDefs = treeshakeTypesDefs(original, modules.reduce((typeDefs, externalMod) => typeDefs.concat(externalMod.typeDefs), []));
return typeDefs;
}
function replaceExtensions(typeDefs) {
const types = [];
const extensions = [];
// List all object types
typeDefs.forEach((doc) => {
visit(doc, {
ObjectTypeDefinition(node) {
types.push(node.name.value);
},
});
});
// turn object type extensions into object types
return typeDefs.map((doc) => {
return visit(doc, {
ObjectTypeExtension(node) {
// only if object type doesn't exist
if (extensions.includes(node.name.value) ||
types.includes(node.name.value)) {
return node;
}
return {
...node,
kind: Kind.OBJECT_TYPE_DEFINITION,
};
},
});
});
}
function treeshakeTypesDefs(originalSource, sources) {
const namedTypes = originalSource.definitions.filter(isNamedTypeDefinition);
const typesToVisit = namedTypes.map((def) => def.name.value);
const rootFields = namedTypes.reduce((acc, node) => {
const typeName = node.name.value;
if (isRootType(typeName) && hasFields(node)) {
if (!acc[typeName]) {
acc[typeName] = [];
}
node.fields.forEach((field) => {
acc[typeName].push(field.name.value);
});
}
return acc;
}, {});
const schema = concatAST([originalSource].concat(sources));
const involvedTypes = new Set(visitTypes(schema, typesToVisit, rootFields));
return {
kind: Kind.DOCUMENT,
definitions: schema.definitions.filter((def) => {
var _a, _b;
if (isNamedTypeDefinition(def)) {
const typeName = def.name.value;
if (!involvedTypes.has(def.name.value)) {
return false;
}
if ((_a = rootFields[typeName]) === null || _a === void 0 ? void 0 : _a.length) {
const rootType = def;
if ((_b = rootType.fields) === null || _b === void 0 ? void 0 : _b.every((field) => !rootFields[typeName].includes(field.name.value))) {
return false;
}
}
}
return true;
}),
};
}
function isNamedTypeDefinition(def) {
return (!!def &&
def.kind !== Kind.SCHEMA_DEFINITION &&
def.kind !== Kind.SCHEMA_EXTENSION);
}
function visitTypes(schema, types, rootFields) {
const visitedTypes = [];
const scalars = schema.definitions
.filter((def) => def.kind === Kind.SCALAR_TYPE_DEFINITION ||
def.kind === Kind.SCALAR_TYPE_EXTENSION)
.map((def) => def.name.value);
for (const typeName of types) {
collectType(typeName);
}
return visitedTypes;
function collectField(field, parentTypeName) {
var _a;
if (parentTypeName &&
isRootType(parentTypeName) &&
((_a = rootFields[parentTypeName]) === null || _a === void 0 ? void 0 : _a.length) &&
!rootFields[parentTypeName].includes(field.name.value)) {
return;
}
collectType(resolveType(field.type));
if (field.arguments) {
field.arguments.forEach((arg) => {
collectType(resolveType(arg.type));
});
}
if (field.directives) {
field.directives.forEach((directive) => {
collectType(directive.name.value);
});
}
}
function collectType(typeName) {
if (visitedTypes.includes(typeName)) {
return;
}
if (isScalar(typeName)) {
visitedTypes.push(typeName);
return;
}
const types = findTypes(typeName);
visitedTypes.push(typeName);
types.forEach((type) => {
if (hasFields(type)) {
type.fields.forEach((field) => {
collectField(field, typeName);
});
}
if (hasTypes(type)) {
type.types.forEach((t) => {
collectType(resolveType(t));
});
}
if (hasInterfaces(type)) {
type.interfaces.forEach((i) => {
collectType(resolveType(i));
});
}
});
}
function resolveType(type) {
if (type.kind === 'ListType') {
return resolveType(type.type);
}
if (type.kind === 'NonNullType') {
return resolveType(type.type);
}
return type.name.value;
}
function isScalar(name) {
return scalars
.concat(['String', 'Boolean', 'Int', 'ID', 'Float'])
.includes(name);
}
function findTypes(typeName) {
const types = schema.definitions.filter((def) => isNamedTypeDefinition(def) && def.name.value === typeName);
if (!types.length) {
throw new Error(`Missing type "${typeName}"`);
}
return types;
}
}
function hasInterfaces(def) {
return (hasPropValue(def, 'interfaces') &&
[
Kind.OBJECT_TYPE_DEFINITION,
Kind.OBJECT_TYPE_EXTENSION,
Kind.INTERFACE_TYPE_DEFINITION,
Kind.INTERFACE_TYPE_EXTENSION,
].includes(def.kind));
}
function hasTypes(def) {
return ([Kind.UNION_TYPE_DEFINITION, Kind.UNION_TYPE_EXTENSION].includes(def.kind) && hasPropValue(def, 'types'));
}
function hasFields(def) {
return ([
Kind.OBJECT_TYPE_DEFINITION,
Kind.OBJECT_TYPE_EXTENSION,
Kind.INTERFACE_TYPE_DEFINITION,
Kind.INTERFACE_TYPE_EXTENSION,
Kind.INPUT_OBJECT_TYPE_DEFINITION,
Kind.INPUT_OBJECT_TYPE_EXTENSION,
].includes(def.kind) && hasPropValue(def, 'fields'));
}
function hasPropValue(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop) && obj[prop];
}
function isRootType(typeName) {
return (typeName === 'Query' ||
typeName === 'Mutation' ||
typeName === 'Subscription');
}