@nestjs/graphql
Version:
Nest - modern, fast, powerful node.js web framework (@graphql)
365 lines (364 loc) • 13.9 kB
JavaScript
import { __decorate } from "tslib";
import { Injectable } from '@nestjs/common';
import { upperFirst } from 'es-toolkit';
import { DEFINITIONS_FILE_HEADER } from './graphql.constants.js';
let tsMorphLib;
function getNodeName(node) {
return node?.name?.value;
}
function getNestedTypeNode(node) {
return node?.type;
}
let GraphQLAstExplorer = class GraphQLAstExplorer {
constructor() {
this.root = ['Query', 'Mutation', 'Subscription'];
}
async explore(documentNode, outputPath, mode, options = {}) {
if (!documentNode) {
return;
}
tsMorphLib = await import('ts-morph');
const tsAstHelper = new tsMorphLib.Project({
manipulationSettings: {
newLineKind: process.platform === 'win32'
? tsMorphLib.NewLineKind.CarriageReturnLineFeed
: tsMorphLib.NewLineKind.LineFeed,
},
});
const tsFile = tsAstHelper.createSourceFile(outputPath, '', {
overwrite: true,
});
let { definitions } = documentNode;
definitions = [...definitions].sort((left, right) => left.kind.localeCompare(right.kind));
const fileStructure = tsFile.getStructure();
const header = options.additionalHeader
? `${DEFINITIONS_FILE_HEADER}\n\n${options.additionalHeader}`
: DEFINITIONS_FILE_HEADER;
fileStructure.statements = [header];
fileStructure.statements.push(...definitions
.map((item) => this.toDefinitionStructures(item, mode, options))
.filter(Boolean));
fileStructure.statements.push({
kind: tsMorphLib.StructureKind.TypeAlias,
name: 'Nullable',
isExported: false,
type: 'T | null',
typeParameters: [
{
name: 'T',
},
],
});
tsFile.set(fileStructure);
return tsFile;
}
toDefinitionStructures(item, mode, options) {
switch (item.kind) {
case 'SchemaDefinition':
return this.toRootSchemaDefinitionStructure(item.operationTypes, mode);
case 'ObjectTypeDefinition':
case 'ObjectTypeExtension':
case 'InputObjectTypeDefinition':
case 'InputObjectTypeExtension':
return this.toObjectTypeDefinitionStructure(item, mode, options);
case 'InterfaceTypeDefinition':
case 'InterfaceTypeExtension':
return this.toObjectTypeDefinitionStructure(item, 'interface', options);
case 'ScalarTypeDefinition':
case 'ScalarTypeExtension':
return this.toScalarDefinitionStructure(item, options);
case 'EnumTypeDefinition':
case 'EnumTypeExtension':
return this.toEnumDefinitionStructure(item, options);
case 'UnionTypeDefinition':
case 'UnionTypeExtension':
return this.toUnionDefinitionStructure(item, options);
}
}
toRootSchemaDefinitionStructure(operationTypes, mode) {
const structureKind = mode === 'class'
? tsMorphLib.StructureKind.Class
: tsMorphLib.StructureKind.Interface;
const properties = operationTypes
.filter(Boolean)
.map((item) => {
const tempOperationName = item.operation;
const typeName = getNodeName(item.type);
const interfaceName = typeName || tempOperationName;
return {
name: interfaceName,
type: this.addSymbolIfRoot(upperFirst(interfaceName)),
};
})
.filter(Boolean);
return {
name: 'ISchema',
isExported: true,
kind: structureKind,
properties: properties,
};
}
toObjectTypeDefinitionStructure(item, mode, options) {
const parentName = getNodeName(item);
if (!parentName) {
return;
}
const structureKind = mode === 'class'
? tsMorphLib.StructureKind.Class
: tsMorphLib.StructureKind.Interface;
const isRoot = this.root.indexOf(parentName) >= 0;
// Don't transform root type names (Query, Mutation, Subscription)
const transformedName = isRoot
? parentName
: this.getTransformedTypeName(parentName, options);
const parentStructure = {
name: this.addSymbolIfRoot(upperFirst(transformedName)),
isExported: true,
isAbstract: isRoot && mode === 'class',
kind: structureKind,
docs: this.getDescriptionDocs(item, options),
properties: [],
methods: [],
};
const interfaces = 'interfaces' in item ? item.interfaces ?? [] : [];
if (interfaces.length > 0) {
if (mode === 'class') {
parentStructure.implements = interfaces
.map((element) => {
const interfaceName = getNodeName(element);
return interfaceName
? this.getTransformedTypeName(interfaceName, options)
: null;
})
.filter(Boolean);
}
else {
parentStructure.extends = interfaces
.map((element) => {
const interfaceName = getNodeName(element);
return interfaceName
? this.getTransformedTypeName(interfaceName, options)
: null;
})
.filter(Boolean);
}
}
const isObjectType = item.kind === 'ObjectTypeDefinition';
if (isObjectType && options.emitTypenameField) {
parentStructure.properties.push({
name: '__typename',
type: `'${parentStructure.name}'`,
hasQuestionToken: true,
});
}
if (!this.isRoot(parentStructure.name) || options.skipResolverArgs) {
const properties = (item.fields || [])
.map((element) => this.toPropertyDeclarationStructure(element, options))
.filter(Boolean);
parentStructure.properties.push(...properties);
}
else {
const methods = (item.fields || [])
.map((element) => this.toMethodDeclarationStructure(element, mode, options))
.filter(Boolean);
parentStructure.methods.push(...methods);
}
return parentStructure;
}
toPropertyDeclarationStructure(item, options) {
const propertyName = getNodeName(item);
if (!propertyName) {
return undefined;
}
const federatedFields = ['_entities', '_service'];
if (federatedFields.includes(propertyName)) {
return undefined;
}
const { name: type, required } = this.getFieldTypeDefinition(item.type, options);
return {
name: propertyName,
type: this.addSymbolIfRoot(type),
hasQuestionToken: !required,
docs: this.getDescriptionDocs(item, options),
};
}
toMethodDeclarationStructure(item, mode, options) {
const propertyName = getNodeName(item);
if (!propertyName) {
return;
}
const federatedFields = ['_entities', '_service'];
if (federatedFields.includes(propertyName)) {
return;
}
const { name: type } = this.getFieldTypeDefinition(item.type, options);
return {
isAbstract: mode === 'class',
name: propertyName,
returnType: `${type} | Promise<${type}>`,
docs: this.getDescriptionDocs(item, options),
parameters: this.getFunctionParameters(item.arguments, options),
};
}
getFieldTypeDefinition(typeNode, options) {
const stringifyType = (typeNode) => {
const { type, required } = this.unwrapTypeIfNonNull(typeNode);
const isArray = type.kind === 'ListType';
if (isArray) {
const arrayType = getNestedTypeNode(type);
if (!arrayType) {
return 'unknown';
}
return required
? `${stringifyType(arrayType)}[]`
: `Nullable<${stringifyType(arrayType)}[]>`;
}
const typeName = this.addSymbolIfRoot(getNodeName(type) ?? 'unknown');
return required
? this.getType(typeName, options)
: `Nullable<${this.getType(typeName, options)}>`;
};
const { required } = this.unwrapTypeIfNonNull(typeNode);
return {
name: stringifyType(typeNode),
required,
};
}
unwrapTypeIfNonNull(type) {
const isNonNullType = type.kind === 'NonNullType';
if (isNonNullType) {
const nestedType = getNestedTypeNode(type);
return {
type: nestedType ? this.unwrapTypeIfNonNull(nestedType).type : type,
required: isNonNullType,
};
}
return { type, required: false };
}
getType(typeName, options) {
const defaults = this.getDefaultTypes(options);
const isDefault = defaults[typeName];
if (isDefault) {
return defaults[typeName];
}
const transformedName = this.getTransformedTypeName(typeName, options);
return upperFirst(transformedName);
}
getDefaultTypes(options) {
return {
String: options.defaultTypeMapping?.String ?? 'string',
Int: options.defaultTypeMapping?.Int ?? 'number',
Boolean: options.defaultTypeMapping?.Boolean ?? 'boolean',
ID: options.defaultTypeMapping?.ID ?? 'string',
Float: options.defaultTypeMapping?.Float ?? 'number',
};
}
getFunctionParameters(inputs, options) {
if (!inputs) {
return [];
}
return inputs
.map((element) => {
const { name, required } = this.getFieldTypeDefinition(element.type, options);
const elementName = getNodeName(element);
if (!elementName) {
return undefined;
}
return {
name: elementName,
type: name,
hasQuestionToken: !required,
kind: tsMorphLib.StructureKind.Parameter,
};
})
.filter(Boolean);
}
toScalarDefinitionStructure(item, options) {
const name = getNodeName(item);
if (!name || name === 'Date') {
return undefined;
}
const typeMapping = options.customScalarTypeMapping?.[name];
const mappedTypeName = typeof typeMapping === 'string' ? typeMapping : typeMapping?.name;
const transformedName = this.getTransformedTypeName(name, options);
return {
kind: tsMorphLib.StructureKind.TypeAlias,
name: transformedName,
type: mappedTypeName ?? options.defaultScalarType ?? 'any',
isExported: true,
docs: this.getDescriptionDocs(item, options),
};
}
toEnumDefinitionStructure(item, options) {
const name = getNodeName(item);
if (!name) {
return undefined;
}
const transformedName = this.getTransformedTypeName(name, options);
if (options.enumsAsTypes) {
const values = item.values.map((value) => `"${getNodeName(value)}"`);
return {
kind: tsMorphLib.StructureKind.TypeAlias,
name: transformedName,
type: values.join(' | '),
isExported: true,
docs: this.getDescriptionDocs(item, options),
};
}
const members = item.values.map((value) => ({
name: getNodeName(value),
value: getNodeName(value),
docs: this.getDescriptionDocs(value, options),
}));
return {
kind: tsMorphLib.StructureKind.Enum,
name: transformedName,
members,
isExported: true,
docs: this.getDescriptionDocs(item, options),
};
}
toUnionDefinitionStructure(item, options) {
const name = getNodeName(item);
if (!name) {
return undefined;
}
const transformedName = this.getTransformedTypeName(name, options);
const types = item.types
.map((value) => {
const typeName = getNodeName(value);
return typeName ? this.getTransformedTypeName(typeName, options) : null;
})
.filter(Boolean);
return {
kind: tsMorphLib.StructureKind.TypeAlias,
name: transformedName,
type: types.join(' | '),
isExported: true,
docs: this.getDescriptionDocs(item, options),
};
}
addSymbolIfRoot(name) {
return this.root.indexOf(name) >= 0 ? `I${name}` : name;
}
isRoot(name) {
return ['IQuery', 'IMutation', 'ISubscription'].indexOf(name) >= 0;
}
getTransformedTypeName(name, options) {
if (!options.typeName) {
return name;
}
return options.typeName(name);
}
getDescriptionDocs(item, options) {
const description = item.description?.value;
if (!options.emitDescriptions || !description) {
return undefined;
}
return [{ description }];
}
};
GraphQLAstExplorer = __decorate([
Injectable()
], GraphQLAstExplorer);
export { GraphQLAstExplorer };